5 Commits

172 changed files with 1824 additions and 7439 deletions
+30 -2
View File
@@ -42,6 +42,34 @@ The conductor points toward Eibenreith.
The bracket value is the visible term to find. The parenthesized value is the note shown on hover/focus. The renderer marks every matching instance of the term in the same right-page block. The tag is not displayed and is not sent to TTS. Avoid raw Ink control characters in the explanation; `|`, `{`, and `}` must be escaped in Ink as `\|`, `\{`, and `\}` if they are needed literally.
## TTS Reading Instructions
TTS instruction tags are story tags scoped to the paragraph/block they belong to. They are not rendered, and they are only sent to TTS providers that support per-request reading instructions. Currently this means OpenAI with `gpt-4o-mini-tts`.
```ink
„Ich habe nichts gesehen“, sagt Viktor.
#tts[Read softly, with controlled unease.]
```
The default form omits a provider and is the preferred authoring style. Providers that support instructions may consume it; providers that do not support instructions silently ignore it. Provider-specific instructions are only needed when two providers should receive different direction, or when an instruction must be hidden from all but one provider. They use the tag parameter position:
```ink
„Ich habe nichts gesehen“, sagt Viktor.
#tts[openai](Read softly, with controlled unease.)
```
The shorthand `#tts-openai[...]` is also accepted. `#tts(...)` is equivalent to providerless `#tts[...]` if parentheses read better in a local context. `tts-1` and `tts-1-hd` ignore these instructions because the OpenAI speech endpoint only supports the `instructions` request parameter for `gpt-4o-mini-tts`.
Keep instructions short and describe performance rather than content. OpenAI's TTS guide recommends using `gpt-4o-mini-tts` when you need controllable delivery; useful instruction targets include tone, emotional range, intonation, speaking speed, accent, impressions, and whispering. Good examples:
```ink
#tts[Speak with restrained concern and a slower pace.]
#tts[Whisper the line with controlled urgency.]
#tts-openai[Use a dry, formal tone; avoid melodrama.]
```
Avoid repeating the full dialogue in the instruction. Put the words to be spoken in the story text, and use `#tts` only to describe how the provider should read that block.
## Choice Metadata
Choice tags are placed on the Ink choice they belong to:
@@ -56,9 +84,9 @@ Implemented choice metadata:
- `#key:x`: reserves keyboard key `X` for the choice.
- `#letter[x]`: older equivalent for reserving keyboard key `X`.
- `#action:group` or `#action[group]`: stores a category/template hint.
- `#action:group` or `#action[group]`: assigns the choice to an invisible action group.
The current UI renders all choices in one list. Explicit keys are assigned first; choices without explicit keys receive `1` through `0`, then `A` through `Z` in visible order while skipping explicit keys. `#optional` choices are displayed italic. Grouping columns, stable shuffling, `#gated[...]`, and `#sort[...]` are documented authoring conventions or future metadata, not fully implemented UI behavior yet.
The current UI renders all choices in one visible list. Choices are first grouped by `#action` in the order each new action group appears in the authored choice list. Choices inside each group are randomized. Choices without `#action` form one final unlabelled group shown after all tagged groups. Explicit keys are assigned before automatic keys; choices without explicit keys receive `1` through `0`, then `A` through `Z` in final visible order while skipping explicit keys. `#optional` choices are displayed italic. Grouping columns, `#gated[...]`, and `#sort[...]` are documented authoring conventions or future metadata, not fully implemented UI behavior yet.
## Popup And End-State Tags
+13 -2
View File
@@ -61,7 +61,7 @@ Environment variables are loaded from `.env`.
- `OPENROUTER_API_KEY`: API key for LLM command interpretation.
- `OPENROUTER_MODEL`: OpenRouter model name.
TTS provider settings are configured in the browser options menu and persisted in browser storage. Providers currently include `none`, browser speech synthesis, Kokoro, ElevenLabs, and OpenAI. Production should not assume a universal TTS default; the game or player state selects the active mode, and `none` is the safe fallback.
TTS provider settings are configured in the browser options menu and persisted in browser storage. Providers currently include `none`, browser speech synthesis, Kokoro, ElevenLabs, OpenAI, and local OpenAI-compatible servers. Production should not assume a universal TTS default; the game or player state selects the active mode, and `none` is the safe fallback.
## Starting A Game
@@ -116,6 +116,17 @@ The train stops at Eibenreith.
Glossary markup is a normal story tag scoped to the paragraph/block it is attached to. The UI finds every matching visible instance of the term in that right-page block and adds a hover/focus note. The tag itself is not displayed, is not sent to TTS, and is ignored by choices and command history. Avoid raw Ink control characters in the explanation; `|`, `{`, and `}` must be escaped in Ink as `\|`, `\{`, and `\}` if they are needed literally.
TTS reading instructions:
```text
„Ich habe nichts gesehen“, sagt Viktor.
#tts[Read softly, with controlled unease.]
```
`#tts[...]` is scoped to the paragraph/block it is attached to and is sent only to providers that support per-request reading instructions. This providerless form is the normal authoring style; `#tts(...)` is equivalent if parentheses read better. Provider-specific forms are also accepted for overrides, for example `#tts[openai](Read softly.)` or `#tts-openai[Read softly.]`. Currently only OpenAI `gpt-4o-mini-tts` consumes the instruction.
Write TTS instructions as concise performance direction: tone, emotion, intonation, pace, accent, or whispering/singing style. Keep the spoken words in the paragraph itself and use the tag only to guide delivery.
Canonical block/media/control tags use Ink-style `#` syntax. In Ink these are real Ink tags. In YAML and Z-code narrative output, leading `#...` lines are parsed by the server into the same structured `StoryTag` objects before reaching the client. The browser only consumes structured `TurnResult` objects.
Tag format:
@@ -127,7 +138,7 @@ Tag format:
#key:value
```
For Ink choices, put choice-local tags under the choice they belong to. Explicit keyboard letters are supported with `# letter[x]`, `#letter[x]`, or the colon form `#key:x`; the client reserves those keys first, then assigns the remaining visible choices from `1` through `0`, then `A` through `Z` in order. `#optional` renders the choice in italic. `# action[name]` or `#action:name` is parsed as a category/template hint for future choice layouts, although the current UI displays all choices in one list.
For Ink choices, put choice-local tags under the choice they belong to. Explicit keyboard letters are supported with `# letter[x]`, `#letter[x]`, or the colon form `#key:x`; the client reserves those keys first, then assigns the remaining visible choices from `1` through `0`, then `A` through `Z` in visible order. `#optional` renders the choice in italic. `# action[name]` or `#action:name` assigns an invisible action group: group order follows the first appearance of each action tag in the authored list, entries inside each group are randomized, and choices without an action tag are grouped last.
Chapter:
+7 -1
View File
@@ -209,6 +209,9 @@ Supported story tags include:
- `#sfx[file](max=8 fade fade-duration=2)`
- `#music[file](crossfade loop lead=4)`
- `#gloss[term](definition)`
- `#tts[instruction]`
- `#tts(instruction)`
- `#tts[provider](instruction)` / `#tts-openai[instruction]`
- `#score[...]`
- `#error[...]`
- `#achievement[...]`
@@ -222,6 +225,9 @@ Choice tags:
- `#action[name]`
The active choice UI is one list. Explicit keys are reserved first, then remaining choices receive `1` through `0`, then `A` through `Z`.
Before key assignment, choices are ordered by invisible `#action` groups. The first appearance of each action group in the authored list determines group order. Choices inside each group are randomized for presentation. Choices without an action group form one final group shown last. Group labels are not displayed.
TTS instruction tags are paragraph/block metadata. They are ignored by renderers and by providers that do not support per-request reading instructions. Providerless `#tts[...]` and `#tts(...)` are the default authoring forms; provider-specific forms are optional filters for provider overrides. OpenAI consumes matching instructions only for `gpt-4o-mini-tts`, where they are sent as the Speech API `instructions` field. Instructions should describe delivery, such as tone, emotion, intonation, pace, accent, whispering, humming, or singing style.
Markdown emphasis:
@@ -233,7 +239,7 @@ Markdown emphasis:
## Audio, TTS, And Media
TTS providers currently include `none`, Browser Speech, Kokoro, ElevenLabs, and OpenAI. Provider modules exist, but Browser Speech and Kokoro need focused validation before being considered production-ready.
TTS providers currently include `none`, Browser Speech, Kokoro, ElevenLabs, OpenAI, and local OpenAI-compatible servers. Provider modules exist, but Browser Speech and Kokoro need focused validation before being considered production-ready.
TTS cache keys include provider, voice, provider speed value, language, and exact normalized TTS string. Fast-forward must accelerate visible animation and fade/stop active TTS without cancelling background generations unless the foreground block has been waiting long enough.
+2 -2
View File
@@ -22,7 +22,7 @@ This is the active implementation checklist. Architecture lives in `SPECIFICATIO
- [x] Right-page `#gloss[term](definition)` hover/focus notes.
- [x] Image rendering for landscape, square, and portrait cases, with history/save restoration.
- [x] Sound effect and music playback, including music lead-in, loop/once, and ducking.
- [x] TTS `none`, OpenAI, ElevenLabs, Browser Speech, and Kokoro provider modules.
- [x] TTS `none`, OpenAI, local OpenAI-compatible, ElevenLabs, Browser Speech, and Kokoro provider modules.
- [x] TTS cache keys include provider, voice, speed, language, and exact normalized string.
- [x] Persisted speech enable state, provider, voice, speed, language, and volume preferences.
- [x] Fast-forward for text animation and active TTS fade/stop.
@@ -48,7 +48,7 @@ This is the active implementation checklist. Architecture lives in `SPECIFICATIO
- [ ] Get Browser TTS working reliably.
- [ ] Get Kokoro.js TTS working for English-language games.
- [ ] Get Kokoro.js TTS working for German-language games.
- [ ] Add a TTS module for self-hosted or local OpenAI-compatible servers.
- [x] Add a TTS module for self-hosted or local OpenAI-compatible servers.
- [ ] Test every documented `#tag` parameter and effect against parser, server, client rendering, playback, and save/load behavior.
- [ ] Remove local file paths and diff-comments from third-party license markdown, refresh included third-party licenses/material, update external libraries where possible, and move any local modifications into our code.
- [ ] Improve credits page layout with more window height, a larger notices markdown pane, and a Hollywood-style title scroll for creative credits.
+95 -77
View File
@@ -1,19 +1,19 @@
// eibenreith_01_zug.ink
// Kapitel: Die Reise / Zugabteil.
// Kapitel: Das Abteil.
// Enthält Charaktergenerator, Abteil-Weave, Viktor-Beobachtung und Missionsbriefing.
=== intro_train ===
Der Zug lässt Wien hinter sich, doch Wien gibt dich noch nicht frei. #chapter[Die Reise] #music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)
#chapter[Das Abteil] #music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)
Es hängt noch am schwarzen Glanz deiner Reisestiefel, am Schnitt deines Mantels, am engen kleinen Gefängnis deiner Handschuhe. Es liegt im Siegel des Schreibens, das in deinem Ridikül ruht, im Geruch von Kohlenrauch, der sich selbst in die Polster der ersten Klasse geschlichen hat, und in der Tatsache, dass Herr Viktor Nowak dir gegenübersitzt, als wäre dieses Abteil kein mit Samt, Messing und poliertem Holz ausgekleideter Reiseraum, sondern ein provisorisches Amtszimmer auf Rädern. #gloss[Ridikül](Kleine Damenhandtasche oder Handarbeitsbeutel des achtzehnten und neunzehnten Jahrhunderts.) #gloss[erste Klasse](Vornehmste Wagenklasse der Eisenbahn mit besserer Polsterung mehr Raum und höherem Fahrpreis.) #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.) #gloss[Amtszimmer](Zimmer einer Behörde oder Kanzlei in dem Amtsgeschäfte geführt werden.)
Der Zug lässt Wien hinter sich, doch Wien hat dich noch nicht freigegeben. Es bleibt an dir haften, im Puder am Kragen, im Kohlenrauch im Haar, im Druck des Korsetts unter dem Reisekleid und im leisen Schwitzen unter den Handschuhen. Im Ridikül ruht das versiegelte Schreiben des Hofes; in den Polstern der ersten Klasse ruht der Geruch fremder Reisen. Herr Viktor Nowak sitzt dir gegenüber, unbewegt, korrekt, aufmerksam. Kein Blick von ihm ist unhöflich. Keiner ist unschuldig. Das Abteil fährt nach Süden, doch für einen Augenblick scheint es weniger ein Reiseraum als ein gepolstertes Amtszimmer, in dem selbst dein Atem Haltung annehmen muss. #tts[Begin in a low, intimate narrative voice. Keep the pace unhurried and formal, with a thread of contained unease.] #gloss[Ridikül](Kleine Damenhandtasche oder Handarbeitsbeutel. Klein genug um harmlos zu wirken, groß genug für Briefe, Schlüssel und jene Dinge, von denen Herren später behaupten, sie hätten nichts davon gewusst.) #gloss[erste Klasse](Die teuerste Wagenklasse der Eisenbahn. Sie bietet weichere Polster, bessere Luft und vor allem weniger Menschen, was im bürgerlichen Fortschrittsglauben oft dasselbe bedeutet.) #gloss[Korsett](Formendes Mieder für Taille und Oberkörper. Es stützt Haltung, beschränkt Atem und beweist, dass weibliche Selbstbeherrschung zuerst am Körper verlangt wird.) #gloss[Amtszimmer](Zimmer der amtlichen Ordnung. Dort wird ein Mensch selten lauter unterworfen, als wenn alles ganz höflich und schriftlich geschieht.)
-> train_compartment
=== train_compartment ===
{not tut_choice_intro:
#alert[Jede Wahl beginnt mit einem hervorgehobenen Verb. Es nennt die Handlung, die du in diesem Augenblick ausführst: schauen, untersuchen, greifen, fragen, antworten oder warten. Einige vertraute Abenteuerbefehle besitzen feste Tasten, etwa L für Schaue und X für Untersuche.]
#alert[Links erscheinen deine Entscheidungen. Du kannst sie mit der Maus wählen oder die angezeigte Taste drücken. Das hervorgehobene Wort nennt die Handlung. Einige Kürzel folgen der alten Textadventure-Gewohnheit: L für Schaue, X für Untersuche.]
~ tut_choice_intro = true
}
@@ -45,19 +45,19 @@ Es hängt noch am schwarzen Glanz deiner Reisestiefel, am Schnitt deines Mantels
=== next_compartment_definition ===
{not define_class_and_name:
Was immer du im Abteil betrachtest, führt auf dieselbe unausgesprochene Frage zurück: Aus welcher Ordnung kommst du, dass Wien dich nun in eine andere schicken kann? #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.)
Was immer du im Abteil betrachtest, führt auf dieselbe unausgesprochene Frage zurück: Aus welcher Ordnung kommst du, dass Wien dich nun in eine andere schicken kann?
-> define_class_and_name ->
->->
}
{not define_religion_and_supernatural:
Die Fahrt nach Hohenreith ist kein bloßer Auftrag. Sie berührt Kirche, Aberglauben, Spiritismus und die Frage, welche unsichtbaren Dinge du überhaupt für möglich hältst. #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.) #gloss[Spiritismus](Lehre und Praxis des Verkehrs mit Geistern oder Verstorbenen.)
Die Fahrt nach Hohenreith ist kein bloßer Auftrag. Sie berührt Kirche, Aberglauben, Spiritismus und die Frage, welche unsichtbaren Dinge du überhaupt für möglich hältst.
-> define_religion_and_supernatural ->
->->
}
{not define_appearance:
Der Zug fährt in einen Tunnel. Das Land draußen verschwindet; die Scheibe verliert die Berge, die Felder, den Himmel, und behält nur noch das Abteil. #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.)
Der Zug fährt in einen Tunnel. Das Land draußen verschwindet; die Scheibe verliert die Berge, die Felder, den Himmel, und behält nur noch das Abteil. #tts[Drop the volume slightly and slow down. Make the tunnel feel like the room closing around the listener.]
-> define_appearance ->
->->
}
@@ -66,29 +66,29 @@ Es hängt noch am schwarzen Glanz deiner Reisestiefel, am Schnitt deines Mantels
=== compartment_room ===
Das Abteil bleibt, was es war: Polster, Messing, Glas, Bedienung, Abstand. Ein fahrender kleiner Salon, zusammengesetzt aus Dingen, die Menschen benutzen, ohne sie selbst zu putzen, zu polieren oder zu bezahlen. #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.) #gloss[Salon](Vornehmer Empfangsraum oder gesellschaftlicher Raum.)
Das Abteil bleibt, was es war: Polster, Messing, Glas, Bedienung, Abstand. Ein fahrender kleiner Salon, zusammengesetzt aus Dingen, die Menschen benutzen, ohne sie selbst zu putzen, zu polieren oder zu bezahlen. #gloss[Salon](Ein Raum, in dem Gesellschaft sich selbst für Geist hält. Wer dort spricht, spricht selten nur mit der Person vor sich.)
->->
=== compartment_letter ===
Deine Hand findet das Ridikül. Unter Stoff, Verschluss und Handschuh liegt das Schreiben des Hofes, schwerer durch Bedeutung als durch Papier. #gloss[Ridikül](Kleine Damenhandtasche oder Handarbeitsbeutel des achtzehnten und neunzehnten Jahrhunderts.)
Deine Hand findet das Ridikül. Unter Stoff, Verschluss und Handschuh liegt das Schreiben des Hofes, schwerer durch Bedeutung als durch Papier. #tts[Read more quietly here, as if the letter has physical weight. Put a slight pause before "schwerer".] #gloss[Ridikül](Kleine Damenhandtasche oder Handarbeitsbeutel. Klein genug um harmlos zu wirken, groß genug für Briefe, Schlüssel und jene Dinge, von denen Herren später behaupten, sie hätten nichts davon gewusst.)
{define_religion_and_supernatural:
Du kennst seinen Ton nun, seine Auslassungen, seine Vorsicht und die Art, wie eine Kanzlei das Unheimliche in anständige Grammatik kleidet. #gloss[Kanzlei](Amtliche Schreib und Verwaltungsstelle für Schreiben Erlässe und Akten.)
Du kennst seinen Ton nun, seine Auslassungen, seine Vorsicht und die Art, wie eine Kanzlei das Unheimliche in anständige Grammatik kleidet. #gloss[Kanzlei](Amtliche Schreibstube. Sie verwandelt Furcht in Formulierungen, Schuld in Zuständigkeit und Menschen in Aktenlagen.)
}
->->
=== look_out_window ===
Draußen zerfallen die letzten Ränder der Stadt in winterbraune Felder und Dörfer, deren Kirchtürme gegen den Pfiff der Lokomotive nichts auszurichten haben. Die Schienen nehmen sich das Land, ohne um Erlaubnis zu fragen. Dämme schneiden durch Obstgärten. Telegraphenstangen gleiten in regelmäßigen Abständen vorbei, eine nach der anderen, wie Gedanken, die man zu rasch verworfen hat. #gloss[Telegraphenstangen](Holzstangen für Leitungen des elektrischen Telegraphen.) #sfx[steam-whistle.ogg]
Draußen zerfallen die letzten Ränder der Stadt in winterbraune Felder und Dörfer, deren Kirchtürme gegen den Pfiff der Lokomotive nichts auszurichten haben. Die Schienen nehmen sich das Land, ohne um Erlaubnis zu fragen. Dämme schneiden durch Obstgärten. Telegraphenstangen gleiten in regelmäßigen Abständen vorbei, eine nach der anderen, wie Gedanken, die man zu rasch verworfen hat. #tts[Let the rhythm lightly suggest train motion without becoming sing-song. Build a little momentum through the list of passing objects.] #gloss[Telegraphenstangen](Holzstangen der elektrischen Nachrichten. Die Monarchie liebt Drähte, weil sie Gerüchte schneller machen und zugleich wie Ordnung aussehen.) #sfx[steam-whistle.ogg]
Du erwartest, dass sich die Eisenbahn wie ein Sieg des Jahrhunderts anfühlt.
Stattdessen fühlt sie sich wie ein Streit an. #image[suedbahn.png](landscape)
Stattdessen fühlt sie sich wie ein Streit an. #image[train_cabin.png](landscape)
Die Maschine wirft sich mit einer Gewalt nach Süden, die gute Gesellschaft niemals offen bewundert hätte. Die Lampen zittern in ihren Fassungen. Deine Tasse schlägt leise gegen die Untertasse. Jenseits der Scheibe beginnt das Land zu steigen, zuerst beinahe höflich, dann mit festerem Willen, bis die Bahnlinie selbst mit den Bergen zu verhandeln scheint: durch Steinbögen, schwarze Tunnel und Viadukte, die mit dem ganzen Selbstvertrauen kaiserlicher Ingenieurskunst über Schluchten gesetzt sind. #gloss[Viadukte](Große Brückenbauwerke über Täler und Schluchten.)
Die Maschine wirft sich mit einer Gewalt nach Süden, die gute Gesellschaft niemals offen bewundert hätte. Die Lampen zittern in ihren Fassungen. Deine Tasse schlägt leise gegen die Untertasse. Jenseits der Scheibe beginnt das Land zu steigen, zuerst beinahe höflich, dann mit festerem Willen, bis die Bahnlinie selbst mit den Bergen zu verhandeln scheint: durch Steinbögen, schwarze Tunnel und Viadukte, die mit dem ganzen Selbstvertrauen kaiserlicher Ingenieurskunst über Schluchten gesetzt sind. #tts[Give this paragraph more force and breath than the previous one. Make the machinery feel powerful, then widen into awe at the viaducts.] #gloss[Viadukte](Große Brücken der Eisenbahn über Täler und Schluchten. Ingenieurskunst hat den schönen Vorteil, dass sie Abgründe nicht leugnet, sondern überquert.)
->->
@@ -96,28 +96,28 @@ Die Maschine wirft sich mit einer Gewalt nach Süden, die gute Gesellschaft niem
Viktor wirkt noch immer von nichts beeindruckt.
Seine Zivilkleidung ist korrekt genug, um keinen Widerspruch hervorzurufen: dunkler Gehrock, nüchterne Weste, Handschuhe, tadelloser Kragen, dazu die Haltung eines Mannes, der selbst im Sitzen nie ganz aufhört, im Dienst zu sein. Doch kein Schneider der Monarchie kann Disziplin verbergen. #gloss[Zivilkleidung](Bürgerliche Kleidung im Gegensatz zur Uniform.) #gloss[Gehrock](Vornehmer Herrenrock mit langen Schößen.)
Seine Zivilkleidung ist korrekt genug, um keinen Widerspruch hervorzurufen: dunkler Gehrock, nüchterne Weste, Handschuhe, tadelloser Kragen, dazu die Haltung eines Mannes, der selbst im Sitzen nie ganz aufhört, im Dienst zu sein. Doch kein Schneider der Monarchie kann Disziplin verbergen. #tts[Use a precise, observant tone. Let the clothing list feel clipped and controlled, then sharpen the final sentence.] #gloss[Zivilkleidung](Bürgerliche Kleidung im Gegensatz zur Uniform. Manche Männer legen damit nur den Stoff ab, nicht den Befehlston.) #gloss[Gehrock](Langer Herrenrock für korrekte Tageskleidung. Ein Kleidungsstück, das Männern erlaubt, bürgerlich, amtlich und moralisch zugleich auszusehen.)
* [__Untersuche__: Viktors Haltung.] #action:orientation
Sie bleibt in seinen Schultern, in der Sparsamkeit seiner Bewegungen, in der Art, wie er selbst im Sitzen nie ganz aufhört, einen Raum zu sichern.
* [__Schaue__: Viktors Blick nach.] #action:orientation
Seine Augen messen Türen, Fenster, Gepäcknetz, Korridor, dein Gesicht und wieder die Tür. Nicht gierig. Nicht unhöflich. Nur vollständig. #gloss[Gepäcknetz](Netz oder Ablage im Eisenbahnabteil für kleinere Gepäckstücke.) #gloss[Korridor](Seitengang oder Verbindungsgang eines Wagens oder Gebäudes.)
Seine Augen messen Türen, Fenster, Gepäcknetz, Korridor, dein Gesicht und wieder die Tür. Nicht gierig. Nicht unhöflich. Nur vollständig. #gloss[Gepäcknetz](Ablage über den Sitzen des Eisenbahnabteils. Dort reisen Hüte, Schachteln und kleine Lügen, die zu leicht für den Koffer sind.)
* [__Untersuche__: Viktors Kleidung.] #action:orientation
Auf dem Papier ist er dein Sekretär und Reisebegleiter. #gloss[Sekretär](Schreib und Vertrauensbeamter oder privater Gehilfe.)
Auf dem Papier ist er dein Sekretär und Reisebegleiter. #gloss[Sekretär](Schreib und Vertrauensgehilfe. Ein Mann dieses Namens ordnet Papier, Termine und manchmal auch die Wahrheit, bis sie in ein vorzeigbares Format passt.)
In Wahrheit ist er ein Offizier, den man einer heiklen Angelegenheit beigegeben hat; aus Kanälen, die Namen haben, aber sie nicht unnötig gebrauchen. Sein wirklicher Rang bleibt im Jagdhaus Hohenreith ungenannt: Rittmeister Viktor Alois Nowak. #gloss[Jagdhaus](Ländliches Haus oder kleineres Schloss für Jagdaufenthalte.) #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.) #gloss[Rittmeister](Offiziersrang der Kavallerie ungefähr dem Hauptmann entsprechend.)
In Wahrheit ist er ein Offizier, den man einer heiklen Angelegenheit beigegeben hat; aus Kanälen, die Namen haben, aber sie nicht unnötig gebrauchen. Sein wirklicher Rang bleibt im Jagdhaus Hohenreith ungenannt: Rittmeister Viktor Alois Nowak. #gloss[Jagdhaus](Adeliger Landsitz für Jagden und kurze Aufenthalte. Weniger Hauptsitz als Bühne für Gäste, Förster, Gewehre und Geheimnisse.) #gloss[Rittmeister](Kavallerieoffizier im Rang eines Hauptmanns. Ein solcher Mann kann Zivilkleidung tragen, aber nur selten Zivilist werden.)
-
Die Menschen, zu denen ihr reist, haben nicht nach einer Ermittlerin verlangt. Nicht offiziell. Die Kabinettskanzlei hat dich geschickt. Das Militär hat ihn geschickt, damit aus dir kein Skandal wird, ehe du nützlich werden kannst. #gloss[Kabinettskanzlei](Hof und Staatsstelle für unmittelbare Schreiben Eingaben und Weisungen im Umkreis des Monarchen.)
Die Menschen, zu denen ihr reist, haben nicht nach einer Ermittlerin verlangt. Nicht offiziell. Die Kabinettskanzlei hat dich geschickt. Das Militär hat ihn geschickt, damit aus dir kein Skandal wird, ehe du nützlich werden kannst. #gloss[Kabinettskanzlei](Stelle nahe am Monarchen, wo Bitten, Berichte und Befehle in jene Sprache gebracht werden, in der Macht wie Papier aussieht.)
->->
=== define_class_and_name ===
{not tut_character_intro:
#alert[Manche Entscheidungen legen fest, wer du bist. Herkunft, Glaube, Fähigkeiten, Aussehen und Auftreten können später Türen öffnen oder schließen.]
#alert[Einige Entscheidungen beschreiben nicht nur, was du tust, sondern wer du bist. Herkunft, Glaube, Auftreten und Ruf werden dadurch festgelegt und können später wieder Bedeutung gewinnen.]
~ tut_character_intro = true
}
@@ -127,15 +127,15 @@ Die Menschen, zu denen ihr reist, haben nicht nach einer Ermittlerin verlangt. N
~ class_confidence += 2
~ court_loyalty += 1
Nicht der Luxus beunruhigt dich. Luxus ist nur Holz, Stoff, Messing, Bedienung, Stille. Entscheidend ist, ob die Diener zweimal hinsehen, ob der Schaffner die Stimme senkt, ob ein anderer Reisender deine Handschuhe prüft und beschließt, nicht nach deinem Auftrag zu fragen. #gloss[Schaffner](Eisenbahnbediensteter für Reisende Fahrkarten und Ordnung im Zug.)
Nicht der Luxus beunruhigt dich. Luxus ist nur Holz, Stoff, Messing, Bedienung, Stille. Entscheidend ist, ob die Diener zweimal hinsehen, ob der Schaffner die Stimme senkt, ob ein anderer Reisender deine Handschuhe prüft und beschließt, nicht nach deinem Auftrag zu fragen.
Du wurdest unter Menschen geboren, die solche Dinge früher verstanden als Freundlichkeit.
Du hast früh gelernt, dass jedes Zimmer einen Hof enthält, auch wenn kein Kaiser anwesend ist. Ein Mädchen deines Ranges wird darin unterrichtet, einzutreten, sich zu verneigen, vorgestellt, platziert und wieder vergessen zu werden; nur genug zu sprechen, mehr zu verstehen, als es zugibt, und zu wissen, dass ein Familienname zugleich Schlüssel und Kette sein kann.
Deine eigene Familie besitzt keinen großen Sitz, keine Schar von Verwaltern, kein altes Recht, Provinzen zu befehlen. Doch dein Name öffnete Türen in Wiener Salons, und sobald du in diesen Zimmern warst, lerntest du, Menschen Geschichten wiederholen zu lassen, die sie nur hatten andeuten wollen. #gloss[Verwaltern](Beauftragte für Güter Einkünfte Personal und wirtschaftliche Angelegenheiten.) #gloss[Salons](Gesellschaftliche Empfangsräume und Zusammenkünfte.)
Deine eigene Familie besitzt keinen großen Sitz, keine Schar von Verwaltern, kein altes Recht, Provinzen zu befehlen. Doch dein Name öffnete Türen in Wiener Salons, und sobald du in diesen Zimmern warst, lerntest du, Menschen Geschichten wiederholen zu lassen, die sie nur hatten andeuten wollen. #gloss[Verwaltern](Männer, die Besitz in Ordnung halten. Ein Gut ohne Verwalter ist Adel als Romantik, ein Gut mit Verwaltern ist Adel als Rechnung.) #gloss[Salons](Gesellschaftliche Räume für Geist, Klavier, Ruf und Eheanbahnung. Man tritt ein, um zu sprechen, und wird oft erst beim Schweigen beurteilt.)
Dein Ruf als Medium ist nicht vom Himmel gefallen. Er wurde zusammengesetzt aus Halblicht, richtigen Vermutungen, sorgsamen Pausen und der Bereitschaft besser geborener Toren, Aufführung für Offenbarung zu halten. #gloss[Medium](Person der man Verkehr mit Geistern oder verborgenen Kräften zuschreibt.)
Dein Ruf als Medium ist nicht vom Himmel gefallen. Er wurde zusammengesetzt aus Halblicht, richtigen Vermutungen, sorgsamen Pausen und der Bereitschaft besser geborener Toren, Aufführung für Offenbarung zu halten. #gloss[Medium](Person, durch die Geister oder verborgene Kräfte sprechen sollen. Gesellschaftlich besonders brauchbar, weil eine Frau so Dinge sagen darf, die man ihr als eigene Meinung übel nähme.)
Bevor der Hof dich benutzen konnte, musste die Gesellschaft dich erst erfinden.
@@ -171,9 +171,9 @@ Die Menschen, zu denen ihr reist, haben nicht nach einer Ermittlerin verlangt. N
Das war dein erster Vorteil.
Eine Dienstmagd weiß, welche Tür wichtig ist, weil sie die anderen benutzt. Eine Näherin lernt Körper, weil sie sie misst. Eine Zofe lernt Geheimnisse, weil feine Leute ihre Seelen wie Handschuhe liegen lassen, gewiss, dass niemand unter ihnen Hände hat. #gloss[Dienstmagd](Weibliche Hausbedienstete für niedere Arbeiten im Haushalt.) #gloss[Näherin](Frau die berufsmäßig Kleidungsstücke näht oder ausbessert.) #gloss[Zofe](Persönliche weibliche Dienerin einer Dame.)
Eine Dienstmagd weiß, welche Tür wichtig ist, weil sie die anderen benutzt. Eine Näherin lernt Körper, weil sie sie misst. Eine Zofe lernt Geheimnisse, weil feine Leute ihre Seelen wie Handschuhe liegen lassen, gewiss, dass niemand unter ihnen Hände hat. #gloss[Dienstmagd](Weibliche Hausbedienstete für niedere Arbeiten. Niedrig heißt dabei nur die Stellung, nicht die Menge dessen, was sie sieht.) #gloss[Näherin](Frau, die Kleidung näht, ändert und ausbessert. Wer Körper ausmisst, lernt mehr über Stand und Eitelkeit, als ein Schneider je zugeben muss.) #gloss[Zofe](Persönliche Dienerin einer Dame. Zuständig für Haar, Kleidung, kleine Notfälle und jene Wahrheiten, die eine Gesellschaft nur deshalb nicht kennt, weil Dienerinnen selten gefragt werden.)
Du stiegst auf durch Begabung, Protektion, Nachahmung, Nervenstärke und die furchtbare Bequemlichkeit, für harmlos gehalten zu werden. Als Wien zu flüstern begann, du sähest mehr als anständige Leute sehen, hattest du schon Jahre damit verbracht, das zu sehen, was anständige Leute übersahen. #gloss[Protektion](Förderung durch Schutz oder Empfehlung einer höherstehenden Person.)
Du stiegst auf durch Begabung, Protektion, Nachahmung, Nervenstärke und die furchtbare Bequemlichkeit, für harmlos gehalten zu werden. Als Wien zu flüstern begann, du sähest mehr als anständige Leute sehen, hattest du schon Jahre damit verbracht, das zu sehen, was anständige Leute übersahen. #gloss[Protektion](Förderung durch eine höherstehende Person. Eine verwerfliche Begünstigung, sobald sie anderen nützt, und eine notwendige Verbindung, sobald sie einem selbst nützt.)
-> choose_name_working
@@ -210,9 +210,9 @@ Wien kannte dich unter dem Namen, den die Gesellschaft brauchbar gemacht hatte.
=== choose_surname_noble ===
Dein Titel ist durch Geburt und durch die vorsichtige Bescheidenheit deiner Familie bestimmt: keine Gräfin, keine Fürstin, keiner jener glänzenden Namen, die Botschafter und Gläubiger wie Staub anziehen. #gloss[Gräfin](Frau gräflichen Ranges.) #gloss[Fürstin](Frau fürstlichen Ranges.)
Dein Titel ist durch Geburt und durch die vorsichtige Bescheidenheit deiner Familie bestimmt: keine Gräfin, keine Fürstin, keiner jener glänzenden Namen, die Botschafter und Gläubiger wie Staub anziehen. #gloss[Gräfin](Frau gräflichen Ranges. Der Titel öffnet Türen, aber auch Erwartungen, Schulden, Blicke und Verwandtschaften.) #gloss[Fürstin](Frau fürstlichen Ranges. So hoch, dass selbst Fehler zunächst als Eigenart erscheinen dürfen.)
Eine Freiin. Freiherrlicher Rang. Brauchbar. Zugelassen, aber nicht thronend. #gloss[Freiin](Unverheiratete Tochter eines Freiherrn oder Dame freiherrlichen Ranges.)
Eine Freiin. Freiherrlicher Rang. Brauchbar. Zugelassen, aber nicht thronend. #gloss[Freiin](Unverheiratete Dame freiherrlichen Ranges. Hoch genug, um angekündigt zu werden, niedrig genug, um von höheren Häusern übersehen werden zu dürfen.)
* [__Führe den Namen__: Freiin von Rauhenfels] #action:thinking
~ title_part = "Freiin von"
@@ -268,7 +268,7 @@ Die Salons, die zuerst über dich lachten und dich dann wieder einluden, lernten
=== choose_surname_middle ===
Dein Familienname enthält kein Partikel, das den Aufstieg abfedert. Er muss allein aufrecht stehen. #gloss[Partikel](Adelspartikel im Namen wie von oder zu.)
Dein Familienname enthält kein Partikel, das den Aufstieg abfedert. Er muss allein aufrecht stehen. #gloss[Partikel](Das kleine von oder zu im Namen. Ein winziges Wort, das so viel Gewicht tragen darf, wie andere Menschen mit Arbeit füllen müssen.)
* [__Führe den Namen__: Leitner] #action:thinking
~ title_part = "Fräulein"
@@ -362,9 +362,9 @@ Ein einfacher Name kann in Wien eine Last sein. Er sagt den Leuten, wie wenig Ac
=== assemble_full_name ===
{birth_class == "noble":
Auf Visitenkarten, in Briefen, in den vorsichtigen Mündern der Dienerschaft bist du {given_names} {title_part} {surname}. #gloss[Visitenkarten](Gedruckte Besuchskarten mit Namen und Titel.) #gloss[Dienerschaft](Gesamtheit der Bediensteten eines Hauses oder Gutes.)
Auf Visitenkarten, in Briefen, in den vorsichtigen Mündern der Dienerschaft bist du {given_names} {title_part} {surname}. #gloss[Visitenkarten](Gedruckte Besuchskarten mit Namen und Titel. Eine Dame gibt damit nicht nur ihre Anwesenheit ab, sondern eine kleine, korrekt geschnittene Behauptung.) #gloss[Dienerschaft](Die Bediensteten eines Hauses. Offiziell Teil der Ordnung, praktisch ihr Gedächtnis.)
- else:
In Bahndokumenten, Hotelbüchern und auf den Zungen von Menschen, die noch nicht entschieden haben, wie viel Achtung du verdienst, bist du {title_part} {given_names} {surname}. #gloss[Bahndokumenten](Schriftstücke der Eisenbahn wie Fahrkarten oder Gepäckscheine.) #gloss[Hotelbüchern](Gästebücher oder Meldebücher eines Gasthauses oder Hotels.)
In Bahndokumenten, Hotelbüchern und auf den Zungen von Menschen, die noch nicht entschieden haben, wie viel Achtung du verdienst, bist du {title_part} {given_names} {surname}. #gloss[Bahndokumenten](Papiere der Eisenbahn. Sie beweisen, wohin man fahren darf, was man mitführt und dass selbst Bewegung ein Formular verlangt.) #gloss[Hotelbüchern](Gästebücher der Herbergen und Hotels. Wer reist, hinterlässt darin nicht nur einen Namen, sondern eine Spur für Wirte, Polizei und Neugier.)
}
Aber in der privaten Kammer, in der ein Name zuerst beantwortet wird, ehe er gespielt werden muss, bist du {common_name}.
@@ -373,21 +373,21 @@ Aber in der privaten Kammer, in der ein Name zuerst beantwortet wird, ehe er ges
=== define_religion_and_supernatural ===
Du berührst das Ridikül, ohne es sofort zu öffnen. #gloss[Ridikül](Kleine Damenhandtasche oder Handarbeitsbeutel des achtzehnten und neunzehnten Jahrhunderts.)
Du berührst das Ridikül, ohne es sofort zu öffnen. #gloss[Ridikül](Kleine Damenhandtasche oder Handarbeitsbeutel. Klein genug um harmlos zu wirken, groß genug für Briefe, Schlüssel und jene Dinge, von denen Herren später behaupten, sie hätten nichts davon gewusst.)
Das Schreiben darin nennt dich nicht Ermittlerin. Es nennt dich, in einer Prosa trocken genug, um durch beliebig viele Ämter zu gelangen, eine Frau, deren ungewöhnlicher spiritistischer Ruf sie für eine heikle Haushaltsangelegenheit empfehle. Die Formulierung ist erlesen. Sie bejaht nicht und verneint nicht. Sie erlaubt allen Beteiligten, später zu glauben, sie hätten nichts Ungehöriges geglaubt. #gloss[spiritistischer](Den Spiritismus betreffend.)
Das Schreiben darin nennt dich nicht Ermittlerin. Es nennt dich, in einer Prosa trocken genug, um durch beliebig viele Ämter zu gelangen, eine Frau, deren ungewöhnlicher spiritistischer Ruf sie für eine heikle Haushaltsangelegenheit empfehle. Die Formulierung ist erlesen. Sie bejaht nicht und verneint nicht. Sie erlaubt allen Beteiligten, später zu glauben, sie hätten nichts Ungehöriges geglaubt. #gloss[spiritistischer Ruf](Ein Ruf im Umkreis des Spiritismus. Sehr nützlich, wenn eine Frau gehört werden soll, aber nicht zu deutlich als Urheberin ihrer eigenen Einsichten erscheinen darf.)
Die gräfliche Familie im Jagdhaus Hohenreith hat um Diskretion ersucht. Wien hat mit einem versiegelten Schreiben geantwortet, mit einer Frau, der man nachsagt, sie spreche mit dem Verborgenen, und mit einem Mann ihr gegenüber, der eigene Befehle hat. #gloss[Jagdhaus](Ländliches Haus oder kleineres Schloss für Jagdaufenthalte.) #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
Die gräfliche Familie im Jagdhaus Hohenreith hat um Diskretion ersucht. Wien hat mit einem versiegelten Schreiben geantwortet, mit einer Frau, der man nachsagt, sie spreche mit dem Verborgenen, und mit einem Mann ihr gegenüber, der eigene Befehle hat. #gloss[Jagdhaus](Adeliger Landsitz für Jagden und kurze Aufenthalte. Weniger Hauptsitz als Bühne für Gäste, Förster, Gewehre und Geheimnisse.)
Das Schreiben nennt keine Kirche. Gerade das macht die Kirche anwesend.
* [__Glaube__: Der Glaube ist dir wirklich heilig.] #action:thinking
~ religion_stance = "devout_catholic"
Gott ist kein Gesprächsgegenstand für Abteile. Er ist kein Talent, kein Ruf, keine gesellschaftliche Bequemlichkeit. Du glaubst nicht kindlich, aber tief: an Sünde, Gnade, Sakrament, Versuchung und an die gefährliche Nähe der unsichtbaren Welt. #gloss[Abteile](Abgeschlossene Räume eines Eisenbahnwagens.) #gloss[Sünde](Verstoß gegen göttliches Gebot oder sittliche Ordnung.) #gloss[Gnade](Göttliche Barmherzigkeit und Hilfe.) #gloss[Sakrament](Heilige kirchliche Handlung die göttliche Gnade vermittelt.)
Gott ist kein Gesprächsgegenstand für Abteile. Er ist kein Talent, kein Ruf, keine gesellschaftliche Bequemlichkeit. Du glaubst nicht kindlich, aber tief: an Sünde, Gnade, Sakrament, Versuchung und an die gefährliche Nähe der unsichtbaren Welt.
* [__Bedenke__: Die katholische Ordnung, in der du erzogen wurdest.] #action:thinking
~ religion_stance = "social_catholic"
Du kennst die Feste, die Gebete, das Gewicht der Beichte und die Macht eines Pfarrers über Menschen, die behaupten, ihn nicht zu fürchten. Dein Glaube ist nicht leer; aber er ist ebenso Gewohnheit wie Überzeugung, ebenso Ordnung wie Trost. #gloss[Beichte](Katholisches Sakrament des Bekenntnisses und der Vergebung von Sünden.) #gloss[Pfarrer](Priester und Vorsteher einer Pfarre.)
Du kennst die Feste, die Gebete, das Gewicht der Beichte und die Macht eines Pfarrers über Menschen, die behaupten, ihn nicht zu fürchten. Dein Glaube ist nicht leer; aber er ist ebenso Gewohnheit wie Überzeugung, ebenso Ordnung wie Trost.
* [__Misstraue__: Dem Weihrauch, hinter dem Behörden sitzen.] #action:thinking
~ religion_stance = "josephinian_sceptic"
@@ -395,7 +395,7 @@ Das Schreiben nennt keine Kirche. Gerade das macht die Kirche anwesend.
* [__Verbinde__: Heiligenbilder, Totenmessen und Séancen.] #action:thinking
~ religion_stance = "spiritist_syncretic"
Heiligenbilder, Totenmessen, Séancen, Ahnungen, Tischklopfen, Träume: Die sauberen Grenzen dazwischen scheinen dir eher von Männern gezogen als von der Ewigkeit selbst. Was überlebt, spricht vielleicht in Formen, die keine Kanzlei genehmigt hat. #gloss[Heiligenbilder](Andachtsbilder oder Darstellungen heiliger Personen.) #gloss[Totenmessen](Katholische Messen für Verstorbene.) #gloss[Séancen](Spiritistische Sitzungen zum Verkehr mit Verstorbenen oder Geistern.) #gloss[Tischklopfen](Spiritistische Klopfzeichen eines Tisches als vermeintliche Botschaften.) #gloss[Kanzlei](Amtliche Schreib und Verwaltungsstelle für Schreiben Erlässe und Akten.)
Heiligenbilder, Totenmessen, Séancen, Ahnungen, Tischklopfen, Träume: Die sauberen Grenzen dazwischen scheinen dir eher von Männern gezogen als von der Ewigkeit selbst. Was überlebt, spricht vielleicht in Formen, die keine Kanzlei genehmigt hat. #gloss[Heiligenbilder](Bilder heiliger Personen für Andacht, Trost und häusliche Überwachung des Gewissens. Ein guter Blick von der Wand erspart manchen schlechten Gedanken.) #gloss[Totenmessen](Messen für Verstorbene. Die Kirche versteht den Umgang mit Toten besser als die Salons, doch nicht immer mit weniger Interesse.) #gloss[Séancen](Geistersitzungen mit Tisch, gedämpftem Licht und sehr viel Erwartung. Je dunkler der Raum, desto leichter glauben die Anwesenden, sie sähen klar.) #gloss[Tischklopfen](Klopfzeichen, die als Botschaften aus der unsichtbaren Welt gelten. Eine bequeme Methode, wenn Tote sprechen sollen, ohne die Gesprächsordnung der Lebenden zu stören.) #gloss[Kanzlei](Amtliche Schreibstube. Sie verwandelt Furcht in Formulierungen, Schuld in Zuständigkeit und Menschen in Aktenlagen.)
* [__Erinnere dich__: Der Glaube hat dich geformt, bevor du dich wehren konntest.] #action:thinking
~ religion_stance = "wounded_catholic"
@@ -419,7 +419,7 @@ Vor dieser Reise, vor diesem Zug, bevor die Berge beginnen, Stück für Stück d
- religion_stance == "devout_catholic":
Gerade deshalb erschreckt dich der Gedanke. Wer die Toten ruft, lädt vielleicht nicht nur die Toten ein.
- religion_stance == "spiritist_syncretic":
Der Satz fügt sich in dir nicht gegen den Glauben, sondern unter seine Ränder, dort, wo Volksfrömmigkeit, Séance und Totenmesse einander längst heimlich berühren. #gloss[Volksfrömmigkeit](Religiöse Bräuche und Vorstellungen des Volkes neben der offiziellen Lehre.) #gloss[Séance](Spiritistische Sitzung zum Verkehr mit Verstorbenen oder Geistern.) #gloss[Totenmesse](Katholische Messe für Verstorbene.)
Der Satz fügt sich in dir nicht gegen den Glauben, sondern unter seine Ränder, dort, wo Volksfrömmigkeit, Séance und Totenmesse einander längst heimlich berühren. #gloss[Volksfrömmigkeit](Frömmigkeit des Alltags. Sie hält sich an Kirche, Küche, Wetter, Krankheit und Erzählungen und fragt selten, ob eine Grenze amtlich gezogen wurde.) #gloss[Séance](Geistersitzung mit Tisch, gedämpftem Licht und sehr viel Erwartung. Je dunkler der Raum, desto leichter glauben die Anwesenden, sie sähen klar.) #gloss[Totenmesse](Messe für einen Verstorbenen. Ein kirchlicher Versuch, Trauer in Ordnung zu bringen und Schuld wenigstens liturgisch zu beschäftigen.)
- religion_stance == "josephinian_sceptic":
Du nennst es nicht Frömmigkeit. Eher eine vorläufige Hypothese über Dinge, für die die amtliche Sprache zu grob ist.
- else:
@@ -456,7 +456,7 @@ Vor dieser Reise, vor diesem Zug, bevor die Berge beginnen, Stück für Stück d
{
- religion_stance == "devout_catholic":
Irgendwo in dir notiert eine strengere Stimme das Wort Sünde. Eine andere, praktischere, antwortet: Auftrag. #gloss[Sünde](Verstoß gegen göttliches Gebot oder sittliche Ordnung.)
Irgendwo in dir notiert eine strengere Stimme das Wort Sünde. Eine andere, praktischere, antwortet: Auftrag.
- religion_stance == "wounded_catholic":
Wenn der Glaube dich schon als Mädchen verkleidete, ist es nur gerecht, dass du nun lernst, Verkleidungen selbst zu wählen.
- else:
@@ -493,7 +493,7 @@ Unter Ruf und Aufführung hat die Erinnerung ihre eigene Aussage.
~ supernatural_senses = "genuine"
~ supernatural_exposure += 2
Einmal, als Kind, wusstest du es, bevor das Telegramm kam. Einmal, in einem überfüllten Zimmer, trat der Kummer einer Fremden mit solcher Gewalt in dich ein, dass deine eigenen Knie nachgaben. Einmal sahst du in einem Spiegel eine Tür hinter dir, die nicht im Raum war, als du dich umdrehtest. #gloss[Telegramm](Durch Telegraphie übermittelte kurze schriftliche Nachricht.)
Einmal, als Kind, wusstest du es, bevor das Telegramm kam. Einmal, in einem überfüllten Zimmer, trat der Kummer einer Fremden mit solcher Gewalt in dich ein, dass deine eigenen Knie nachgaben. Einmal sahst du in einem Spiegel eine Tür hinter dir, die nicht im Raum war, als du dich umdrehtest.
Danach lerntest du Vorsicht. Es ist unklug für eine Frau, Dinge zu wissen, bevor ein Mann ihre Meinung erbeten hat.
@@ -522,7 +522,7 @@ Unter Ruf und Aufführung hat die Erinnerung ihre eigene Aussage.
~ supernatural_senses = "repressed"
~ eccentric += 1
Es gibt Kindheitserinnerungen, die hinter Höflichkeit versiegelt sind: ein Kinderzimmerspiegel, zur Wand gedreht; eine Amme, ohne Zeugnis entlassen; die Hand deiner Mutter um dein Handgelenk, so fest, dass die Knochen sich beschwerten. #gloss[Amme](Frau die ein fremdes Kind stillt oder betreut.) #gloss[Zeugnis](Schriftliche Bescheinigung über Dienst Betragen oder Befähigung.)
Es gibt Kindheitserinnerungen, die hinter Höflichkeit versiegelt sind: ein Kinderzimmerspiegel, zur Wand gedreht; eine Amme, ohne Zeugnis entlassen; die Hand deiner Mutter um dein Handgelenk, so fest, dass die Knochen sich beschwerten. #gloss[Amme](Frau, die ein fremdes Kind stillt oder früh betreut. Sie kommt dem Körper eines Hauses oft näher als jene, denen das Haus gehört.) #gloss[Zeugnis](Schriftliche Bescheinigung über Dienst, Betragen und Befähigung. Wer keinen Rang besitzt, braucht Papier, das guten Leumund behauptet.)
Danach wurdest du auf Arten sonderbar, die die Gesellschaft leichter bewundern als verstehen konnte.
@@ -533,7 +533,7 @@ Unter Ruf und Aufführung hat die Erinnerung ihre eigene Aussage.
Für einige Sekunden gibt dir das Fenster nichts als Viktor, die Lampe, deine Handschuhe, die Linie deines Hutes und den bleichen Umriss deines Gesichts zurück. Der Tunnel löscht jede andere Welt aus.
Gerade deshalb wird das Glas nützlich. Nachdem Name und Auftrag in dir geordnet sind, zeigt es nicht bloß eine Dame im richtigen Abteil. Es zeigt die Frau, die in Eibenreith aussteigen wird. #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.) #gloss[Eibenreith](Fiktives Dorf im obersteirischen Gebirge unterhalb von Jagdhaus Hohenreith.)
Gerade deshalb wird das Glas nützlich. Nachdem Name und Auftrag in dir geordnet sind, zeigt es nicht bloß eine Dame im richtigen Abteil. Es zeigt die Frau, die in Eibenreith aussteigen wird.
* [__Schaue__: Auf eine kleine, schmale Gestalt.] #action:orientation
~ body_detail = "small_slender"
@@ -549,7 +549,7 @@ Gerade deshalb wird das Glas nützlich. Nachdem Name und Auftrag in dir geordnet
* [__Schaue__: Auf eine kompakte, kräftigere Gestalt.] #action:orientation
~ body_detail = "compact_strong"
Reisekleidung und Korsett ordnen dich, aber sie verleugnen nicht alles. In deinen Unterarmen, im Nacken, in der Art, wie du ein Gleichgewicht hältst, liegt mehr Kraft, als man einer Dame höflich zutraut. #gloss[Korsett](Formendes Mieder das Taille und Oberkörper stützt.)
Reisekleidung und Korsett ordnen dich, aber sie verleugnen nicht alles. In deinen Unterarmen, im Nacken, in der Art, wie du ein Gleichgewicht hältst, liegt mehr Kraft, als man einer Dame höflich zutraut. #gloss[Korsett](Formendes Mieder für Taille und Oberkörper. Es stützt Haltung, beschränkt Atem und beweist, dass weibliche Selbstbeherrschung zuerst am Körper verlangt wird.)
* [__Schaue__: Auf eine zierliche, empfindsam wirkende Gestalt.] #action:orientation
~ body_detail = "delicate"
@@ -573,7 +573,7 @@ Die dunkle Scheibe hält nun Haar und Hut fest.
* [__Schaue__: Auf hellbraunes Haar mit goldenen Strähnen.] #action:orientation
~ hair_colour = "light_brown_gold"
Hellbraunes Haar mit goldenen Strähnen wirkt im Abteil beinahe zu warm für diese Reise, als habe Wien einen letzten Rest Nachmittag darin vergessen. #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.)
Hellbraunes Haar mit goldenen Strähnen wirkt im Abteil beinahe zu warm für diese Reise, als habe Wien einen letzten Rest Nachmittag darin vergessen.
* [__Schaue__: Auf rotbraunes, sorgfältig gebändigtes Haar.] #action:orientation
~ hair_colour = "auburn"
@@ -638,16 +638,16 @@ Der Rest der Spiegelung ist Kostüm, Rüstung und Beweismittel.
- birth_class == "middle":
Die Kleidung muss eine höhere Welt betreten können, ohne zu schreien, dass sie dafür gearbeitet hat.
- else:
Die Kleidung muss beweisen, dass man dich in die erste Klasse setzen konnte, ohne dass der Stoff gegen dich aussagt. #gloss[erste Klasse](Vornehmste Wagenklasse der Eisenbahn mit besserer Polsterung mehr Raum und höherem Fahrpreis.)
Die Kleidung muss beweisen, dass man dich in die erste Klasse setzen konnte, ohne dass der Stoff gegen dich aussagt. #gloss[erste Klasse](Die teuerste Wagenklasse der Eisenbahn. Sie bietet weichere Polster, bessere Luft und vor allem weniger Menschen, was im bürgerlichen Fortschrittsglauben oft dasselbe bedeutet.)
}
* [__Trage__: Ein dunkel anthrazitfarbenes Reisekostüm mit pflaumenfarbenem Samtkragen.] #action:thinking
~ outfit_detail = "charcoal_plum_velvet"
Du trägst ein geschneidertes Reisekostüm aus dunkler anthrazitfarbener Wolle. Am Kragen und an den Manschetten liegt ein pflaumenfarbener Samtton, gedämpft genug für den Tag, teuer genug für Menschen mit Augen. #gloss[Reisekostüm](Damen Reiseanzug aus Rock und Jacke oder Mantel.) #gloss[anthrazitfarbener](Sehr dunkles Grau beinahe schwarz.)
Du trägst ein geschneidertes Reisekostüm aus dunkler anthrazitfarbener Wolle. Am Kragen und an den Manschetten liegt ein pflaumenfarbener Samtton, gedämpft genug für den Tag, teuer genug für Menschen mit Augen. #gloss[Reisekostüm](Damenkleidung für die Reise. Fest genug für Bahnhofsschmutz, korrekt genug für fremde Blicke, und unbequem genug, damit niemand vergisst, dass auch Zweckmäßigkeit weiblich auszusehen hat.) #gloss[anthrazitfarbener](Von Anthrazit hergeleitet, also sehr dunkelgrau. Eine Farbe für Menschen, die nicht trauern, aber auch nicht verdächtig fröhlich wirken möchten.)
* [__Trage__: Ein schwarzbraunes Wollkostüm mit elfenbeinfarbener Bluse und schmaler Spitze.] #action:thinking
~ outfit_detail = "black_brown_ivory_lace"
Der Rock ist dunkel und schwer genug für die Reise, die Jacke streng, die elfenbeinfarbene Bluse am Hals hochgeschlossen. Die Spitze ist schmal, sauber und gefährlich nahe an Frömmigkeit. #gloss[Bluse](Oberteil einer Damenkleidung unter Jacke oder Kostüm.)
Der Rock ist dunkel und schwer genug für die Reise, die Jacke streng, die elfenbeinfarbene Bluse am Hals hochgeschlossen. Die Spitze ist schmal, sauber und gefährlich nahe an Frömmigkeit.
* [__Trage__: Ein graublaues Reisekostüm mit kurzem Mantel und praktischen Knöpfen.] #action:thinking
~ outfit_detail = "blue_grey_practical"
@@ -659,18 +659,18 @@ Der Rest der Spiegelung ist Kostüm, Rüstung und Beweismittel.
* [__Trage__: Ein schwarzes Reisekleid mit Schleier, zu ernst für bloße Mode.] #action:thinking
~ outfit_detail = "black_veil_severe"
Das Schwarz ist nicht Trauer, jedenfalls nicht offiziell. Ein schmaler Schleier, dunkle Handschuhe, glatter Rock, hohe Knopfleiste. Es ist die Art Kleidung, in der skeptische Männer leichter an Ahnungen glauben. #gloss[Schleier](Feines durchsichtiges Gewebe vor Gesicht oder Hut.)
Das Schwarz ist nicht Trauer, jedenfalls nicht offiziell. Ein schmaler Schleier, dunkle Handschuhe, glatter Rock, hohe Knopfleiste. Es ist die Art Kleidung, in der skeptische Männer leichter an Ahnungen glauben.
-
{
- outfit_detail == "blue_grey_practical":
Viktor wird dieses Kostüm später vermutlich für ein Zeichen von Vernunft halten. Das ist nützlich, auch wenn es nicht vollständig wahr ist.
- outfit_detail == "black_veil_severe":
Selbst Viktor wird Mühe haben, diese Kleidung ganz von deinem Ruf als Medium zu trennen. Das ist kein Zufall. #gloss[Medium](Person der man Verkehr mit Geistern oder verborgenen Kräften zuschreibt.)
Selbst Viktor wird Mühe haben, diese Kleidung ganz von deinem Ruf als Medium zu trennen. Das ist kein Zufall. #gloss[Medium](Person, durch die Geister oder verborgene Kräfte sprechen sollen. Gesellschaftlich besonders brauchbar, weil eine Frau so Dinge sagen darf, die man ihr als eigene Meinung übel nähme.)
- outfit_detail == "dark_green_black_trim":
Das Kostüm ist diskret genug für die Reise und bestimmt genug, um in Erinnerung zu bleiben. Es sagt nicht viel. Nur genug.
- else:
Die Kleidung wird in Hohenreith sprechen, bevor du es tust. Das ist der Sinn guter Kleidung und die Gefahr schlechter. #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
Die Kleidung wird in Hohenreith sprechen, bevor du es tust. Das ist der Sinn guter Kleidung und die Gefahr schlechter.
}
Als die Berge zurückkehren, wirken sie näher.
@@ -680,18 +680,18 @@ Als die Berge zurückkehren, wirken sie näher.
=== first_viktor_exchange ===
{not tut_dialog_intro:
#alert[Wenn eine Wahl in Anführungszeichen steht, sprichst du diese Worte aus. Das Verb davor verrät, ob du antwortest, fragst, mitteilst, spottest oder schweigst.]
#alert[Steht eine Zeile in Anführungszeichen, sprichst du sie aus. Das hervorgehobene Verb zeigt, was du tust und meist, an wen du dich richtest: Frage Viktor, Antworte, Spotte oder Widersprich.]
~ tut_dialog_intro = true
}
Er faltet die Zeitung zusammen, obwohl du sehr sicher bist, dass er nicht gelesen hat.
{birth_class == "noble":
„Sie sind sehr still, gnädiges Fräulein. Für eine Dame auf ihrer ersten amtlichen Reise beweisen Sie bemerkenswerte Zurückhaltung.“ #gloss[amtliche Reise](Reise im Auftrag einer offiziellen Stelle oder Behörde.)
„Sie sind sehr still, gnädiges Fräulein. Für eine Dame auf ihrer ersten amtlichen Reise beweisen Sie bemerkenswerte Zurückhaltung.“ #tts[For Viktor, use a restrained, polished officer's voice. Courteous on the surface, with a faint test hidden underneath.] #gloss[amtliche Reise](Eine Reise im Auftrag einer Behörde oder Hofstelle. Der Unterschied zur privaten Reise besteht vor allem darin, dass man weniger frei ist und mehr Papier mitführt.)
Die Anrede wahrt die Form und vermeidet den Titel. Gerade deshalb verrät sie Absicht.
- else:
„Sie sind sehr still, Fräulein {surname}. Für eine Dame auf ihrer ersten amtlichen Reise beweisen Sie bemerkenswerte Zurückhaltung.“
„Sie sind sehr still, Fräulein {surname}. Für eine Dame auf ihrer ersten amtlichen Reise beweisen Sie bemerkenswerte Zurückhaltung.“ #tts[For Viktor, use a restrained, polished officer's voice. Courteous on the surface, with a faint test hidden underneath.]
Die Anrede ist korrekt, doch sie prüft dich mehr, als sie dich schützt. Er weiß noch nicht, welcher Teil von dir brauchbar ist, welcher Verkleidung und welcher Gefahr.
}
@@ -772,9 +772,9 @@ Viktor wartet auf die Antwort, die seine Bemerkung verlangt. Der Zug ruckt einma
~ class_confidence += 1
„Eine Erziehung in Zimmern, in denen selbst jeder Stuhl Rang besitzt.“
„Dann wird Hohenreith Sie vielleicht nicht überraschen.“ #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
„Dann wird Hohenreith Sie vielleicht nicht überraschen.“
Die Möglichkeit, dass Hohenreith bessere Geheimnisse als Stühle besitzt, darf unausgesprochen bleiben. #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
Die Möglichkeit, dass Hohenreith bessere Geheimnisse als Stühle besitzt, darf unausgesprochen bleiben.
--
-> viktor_mission_briefing
@@ -825,7 +825,7 @@ Viktors Bemerkung bleibt zwischen euch liegen wie ein höflicher Vorwurf.
** [__Antworte__: „Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“] #action:conversation
#route:eccentric
~ eccentric += 1
„Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“
„Eine nützliche. Bitterkeit ist nur der Geschmack, den Belehrung zurücklässt.“ #tts[Read the protagonist's line dryly and evenly, with a small sting on "Bitterkeit".]
„Sie sammeln Redewendungen wie Waffen.“
@@ -847,7 +847,7 @@ Viktors Bemerkung bleibt zwischen euch liegen wie ein höflicher Vorwurf.
#route:lover
~ lover += 1
~ medium_reputation += 1
„Wenn ich schweige, Herr Nowak, so deshalb, weil Männer sich schneller erklären, wenn ihnen die Stille missfällt.“
„Wenn ich schweige, Herr Nowak, so deshalb, weil Männer sich schneller erklären, wenn ihnen die Stille missfällt.“ #tts[Let this be calm and deliberate, almost conversationally intimate. Give "Stille" a tiny pause before continuing.]
„Eine Methode?“
@@ -887,7 +887,7 @@ Viktors Bemerkung bleibt zwischen euch liegen wie ein höflicher Vorwurf.
~ eccentric += 1
„Wie bequem. Die beiden anderen dürfen die Verantwortung leugnen.“
„Ich rate Ihnen, den Witz in Hohenreith nicht zu Ihrem ersten Werkzeug zu machen.“ #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
„Ich rate Ihnen, den Witz in Hohenreith nicht zu Ihrem ersten Werkzeug zu machen.“
Die Herabstufung des Witzes zum zweiten Werkzeug bleibt theoretisch genug, um ungefährlich zu sein.
@@ -905,7 +905,7 @@ Viktors Bemerkung bleibt zwischen euch liegen wie ein höflicher Vorwurf.
=== viktor_class_working ===
Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Darunter hörst du die Frage, wie sehr dieses Abteil dich verbessert hat. #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.)
Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Darunter hörst du die Frage, wie sehr dieses Abteil dich verbessert hat.
* [__Antworte__: „Zurückhaltung ist, was die Leute loben, wenn sie die Mühe dahinter nicht sehen wollen.“] #action:conversation
#route:detective
@@ -924,7 +924,7 @@ Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Daru
„Das dürfte schwer zu vermeiden sein.“
Wenn Hohenreith dich billig loben will, wird es die Ökonomie der Enttäuschung kennenlernen müssen. #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
Wenn Hohenreith dich billig loben will, wird es die Ökonomie der Enttäuschung kennenlernen müssen.
** [__Antworte__: „Nur, wenn es die Person verbirgt, die die Arbeit getan hat.“] #action:conversation
#route:sapphic
@@ -944,6 +944,24 @@ Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Daru
„Ich habe Sie nicht um Dankbarkeit gebeten.“
** [__Summe__: Eine Antwort, die keine Worte braucht.] #action:conversation #optional
#route:eccentric
~ eccentric += 1
Du summst so leise, dass es mehr Haltung als Ton ist. Der Rhythmus der Räder nimmt die Melodie auf und gibt sie höflicher zurück, als sie gekommen ist. #tts[Hum this line very softly, almost under the breath. Keep it restrained, intimate, and wordless, with a faint ironic composure.]
Viktor sieht nicht auf.
„Sie haben die ungewöhnliche Gabe, Schweigen beschäftigt wirken zu lassen.“
** [__Singe__: Die Kaiserhymne, beinahe ohne Stimme.] #action:conversation #optional
#route:eccentric
~ eccentric += 1
Du singst kaum lauter als das Rauschen im Polster: „Gott beschütze Franz den Kaiser ...“ Nicht fromm genug für ein Gebet, nicht spöttisch genug für Hochverrat. #tts[Sing the quoted hymn fragment almost silently, like a controlled private murmur. Use a formal, old Austrian ceremonial feel, but keep the delivery restrained and ambiguous.]
Ein Muskel an Viktors Kiefer bewegt sich.
„Eine riskante Art, Ihre Loyalität nachzuweisen.“
** [__Antworte__: „Nein. Sie haben verlangt, dass ich lenkbar sei.“] #action:conversation
#route:eccentric
~ eccentric += 1
@@ -994,9 +1012,9 @@ Viktors Höflichkeit ist glatt genug, um keine Fingerabdrücke zu behalten. Daru
Der Zug tritt aus dem Tunnel in einen blassen Nachmittag aus dunklen Tannen und weißem Fels. Tief unten zeigt sich Wasser nur in Blitzen. Das Tal ist kein Anblick aus einem Salonbild mehr. Es hat Tiefe genug, um Dinge zu verbergen.
Viktor öffnet eine Ledermappe und nimmt ein Memorandum heraus. Er reicht es dir nicht sofort. #gloss[Memorandum](Schriftliche Denkschrift oder amtliche Notiz.)
Viktor öffnet eine Ledermappe und nimmt ein Memorandum heraus. Er reicht es dir nicht sofort. #gloss[Memorandum](Amtliche Denkschrift. Lang genug, um Zuständigkeit zu behaupten, kurz genug, um die gefährlichen Dinge nicht beim Namen nennen zu müssen.)
„Wenn wir die Bahn verlassen“, sagt er, „werden wir von einer Kutsche aus Hohenreith erwartet. Von diesem Augenblick an sind Äußerlichkeiten von Bedeutung. Ihren Gastgebern wurde mitgeteilt, dass ich bei Korrespondenz, Reiseangelegenheiten und praktischen Vorkehrungen behilflich bin. Mit militärischen Definitionen muss man sie nicht behelligen.“ #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.) #gloss[Korrespondenz](Briefwechsel und schriftliche Erledigung von Nachrichten.)
„Wenn wir die Bahn verlassen“, sagt er, „werden wir von einer Kutsche aus Hohenreith erwartet. Von diesem Augenblick an sind Äußerlichkeiten von Bedeutung. Ihren Gastgebern wurde mitgeteilt, dass ich bei Korrespondenz, Reiseangelegenheiten und praktischen Vorkehrungen behilflich bin. Mit militärischen Definitionen muss man sie nicht behelligen.“ #gloss[Korrespondenz](Briefwechsel. Die vornehme Kunst, Absichten so lange zu falten, bis sie in ein Kuvert passen.)
* [__Frage Viktor__: „Und die Dorfbewohner?“] #action:conversation
„Und die Dorfbewohner?“
@@ -1027,7 +1045,7 @@ Viktor öffnet eine Ledermappe und nimmt ein Memorandum heraus. Er reicht es dir
-
„Man wird Sie nach dem Stand anreden, den Sie vorweisen“, fährt er fort. „Der Haushalt des Grafen wird den Rang beachten. Die Dienerschaft wird beachten, was der Haushalt beachtet. Die Dorfbewohner mögen weniger beachten und mehr behalten. Ich rate zur Zurückhaltung.“ #gloss[Haushalt des Grafen](Häusliche Ordnung eines gräflichen Hauses mit Dienerschaft Verwaltung und Rang.) #gloss[Dienerschaft](Gesamtheit der Bediensteten eines Hauses oder Gutes.)
„Man wird Sie nach dem Stand anreden, den Sie vorweisen“, fährt er fort. „Der Haushalt des Grafen wird den Rang beachten. Die Dienerschaft wird beachten, was der Haushalt beachtet. Die Dorfbewohner mögen weniger beachten und mehr behalten. Ich rate zur Zurückhaltung.“ #tts[For Viktor's briefing, sound exact and professional. Keep each sentence cleanly separated, like points in a confidential report.] #gloss[Haushalt des Grafen](Das Haus als Rangordnung. Familie, Dienerschaft, Gäste und Zuständigkeiten bilden darin ein Uhrwerk, das besonders laut tickt, wenn jemand falsch steht.) #gloss[Dienerschaft](Die Bediensteten eines Hauses. Offiziell Teil der Ordnung, praktisch ihr Gedächtnis.)
Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
@@ -1046,14 +1064,14 @@ Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
** [__Antworte__: „Eine praktische.“] #action:conversation
„Eine praktische.“
„Sie gedenken, sie in Hohenreith anzuwenden?“ #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
„Sie gedenken, sie in Hohenreith anzuwenden?“
*** [__Antworte__: „Nur dort, wo die Pflicht gegen die Monarchie Opfer verlangt.“] #action:conversation
#route:lover
~ lover += 1
„Nur dort, wo die Pflicht gegen die Monarchie Opfer verlangt.“
Er blickt auf das Memorandum hinunter, aber nicht schnell genug, um zu verbergen, dass er dich neu einschätzt. #gloss[Memorandum](Schriftliche Denkschrift oder amtliche Notiz.)
Er blickt auf das Memorandum hinunter, aber nicht schnell genug, um zu verbergen, dass er dich neu einschätzt. #gloss[Memorandum](Amtliche Denkschrift. Lang genug, um Zuständigkeit zu behaupten, kurz genug, um die gefährlichen Dinge nicht beim Namen nennen zu müssen.)
*** [__Antworte__: „Nur dort, wo Männer Begehren mit Urteil verwechseln.“] #action:conversation
#route:lover
@@ -1069,9 +1087,9 @@ Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
~ eccentric += 1
„Gefährliche Lehren reisen am besten in guten Handschuhen.“
„Sie gedenken, Hohenreith durch Charme zum Geständnis zu bringen?“ #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
„Sie gedenken, Hohenreith durch Charme zum Geständnis zu bringen?“
Wenn Hohenreith darauf besteht, bezaubert zu werden, wird es kaum deine Schuld sein. #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
Wenn Hohenreith darauf besteht, bezaubert zu werden, wird es kaum deine Schuld sein.
--
@@ -1117,11 +1135,11 @@ Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
„Sie wissen, dass Sie empfohlen kommen. Sie vermuten, dass Sie imstande sein könnten, die Störungen ohne Polizei, Priester oder Presse beizulegen. Ihnen ist gestattet, Betrug, Zwang, Gefährdung der öffentlichen Ordnung oder glaubwürdige, derzeit nicht einzuordnende Erscheinungen zu prüfen.“
** [__Antworte__: „Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“] #action:conversation
„Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“
„Glaubwürdige, derzeit nicht einzuordnende Erscheinungen.“ #tts[Repeat the phrase with quiet skepticism, tasting the bureaucracy of it.]
„So lautet die Wendung.“
Die Formulierung setzt sich in deinem Geist fest wie ein bürokratisches Gespenst.
Die Formulierung setzt sich in deinem Geist fest wie ein bürokratisches Gespenst. #tts[Make this sentence slightly colder and more uncanny, with a faint emphasis on "bürokratisches Gespenst".]
„Die ungefährlichste Art“, sagt er.
@@ -1150,7 +1168,7 @@ Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
„Wie praktisch.“
Die Last der Sachlichkeit wandert zu ihm hinüber, so anmutig wie ein Ohnmachtssofa, das in ein Feldlazarett geschleppt wird. #gloss[Ohnmachtssofa](Scherzhafte Bezeichnung für ein kleines Ruhemöbel bei Schwäche oder Ohnmacht.) #gloss[Feldlazarett](Militärische Kranken und Verwundetenstation im Feld.)
Die Last der Sachlichkeit wandert zu ihm hinüber, so anmutig wie ein Ohnmachtssofa, das in ein Feldlazarett geschleppt wird. #gloss[Ohnmachtssofa](Ruhemöbel für weibliche Schwächeanfälle, echte oder erwartete. Eine Gesellschaft, die Damen eng schnürt, sorgt immerhin für passende Möbel, wenn sie nicht mehr stehen.) #gloss[Feldlazarett](Militärische Krankenstation im Feld. Dort endet die Sprache von Ehre und beginnt die Sprache von Blut, Listen und Verbänden.)
Seine Antwort verzögert sich um einen halben Atemzug.
@@ -1181,7 +1199,7 @@ Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
** [__Antworte__: „Nein. Ich missbillige nur die Bequemlichkeit, Dummköpfe unentschieden bleiben zu lassen.“] #action:conversation
„Nein. Ich missbillige nur die Bequemlichkeit, Dummköpfe unentschieden bleiben zu lassen.“
„In Hohenreith könnte diese Abneigung kostspielig werden.“ #gloss[Hohenreith](Gräfliches Jagdhaus oberhalb von Eibenreith und neuerer Name des Zielortes.)
„In Hohenreith könnte diese Abneigung kostspielig werden.“
Wenn der Graf Fügsamkeit wollte, hätte er jemand Günstigeren einladen können.
@@ -1196,13 +1214,13 @@ Der Rat ist vernünftig. Das macht ihn nicht weniger ärgerlich.
-
Die Räder nehmen eine Kurve. Das Abteil neigt sich. Für einen Augenblick hält euch dieselbe schmale Schräglage. #gloss[Abteil](Abgeschlossener Raum eines Eisenbahnwagens.)
Die Räder nehmen eine Kurve. Das Abteil neigt sich. Für einen Augenblick hält euch dieselbe schmale Schräglage.
Viktor gibt dir endlich das Memorandum. #gloss[Memorandum](Schriftliche Denkschrift oder amtliche Notiz.)
Viktor gibt dir endlich das Memorandum. #gloss[Memorandum](Amtliche Denkschrift. Lang genug, um Zuständigkeit zu behaupten, kurz genug, um die gefährlichen Dinge nicht beim Namen nennen zu müssen.)
Das Schriftstück ist nicht lang. Das ist Teil seiner Bedrohlichkeit. Lange Schriftstücke laden zum Widerspruch ein; kurze tragen Autorität.
Ein gräflicher Haushalt. Ein Jagdhaus in der Obersteiermark, nicht der Hauptsitz der Familie. Berichte über Störungen unter Dienerschaft und Dorfbewohnern. Kein Einschreiten der Polizei erbeten. Keine öffentliche kirchliche Untersuchung erwünscht. Keine Presse. Keine Korrespondenz außerhalb genehmigter Kanäle. Deine Anwesenheit ist als diskrete Konsultation auf Wunsch der Familie zu erklären. Herr Nowak dient zur Unterstützung praktischer Angelegenheiten. #gloss[Jagdhaus](Ländliches Haus oder kleineres Schloss für Jagdaufenthalte.) #gloss[Obersteiermark](Nördlicher alpiner Teil der Steiermark.) #gloss[Dienerschaft](Gesamtheit der Bediensteten eines Hauses oder Gutes.) #gloss[kirchliche Untersuchung](Prüfung durch kirchliche Stellen.) #gloss[Korrespondenz](Briefwechsel und schriftliche Erledigung von Nachrichten.)
Ein gräflicher Haushalt. Ein Jagdhaus in der Obersteiermark, nicht der Hauptsitz der Familie. Berichte über Störungen unter Dienerschaft und Dorfbewohnern. Kein Einschreiten der Polizei erbeten. Keine öffentliche kirchliche Untersuchung erwünscht. Keine Presse. Keine Korrespondenz außerhalb genehmigter Kanäle. Deine Anwesenheit ist als diskrete Konsultation auf Wunsch der Familie zu erklären. Herr Nowak dient zur Unterstützung praktischer Angelegenheiten. #gloss[Jagdhaus](Adeliger Landsitz für Jagden und kurze Aufenthalte. Weniger Hauptsitz als Bühne für Gäste, Förster, Gewehre und Geheimnisse.) #gloss[Obersteiermark](Der gebirgige Norden der Steiermark. Wälder, Eisen, enge Täler und ein Wetter, das sich nicht für Wiener Empfindlichkeiten interessiert.) #gloss[Dienerschaft](Die Bediensteten eines Hauses. Offiziell Teil der Ordnung, praktisch ihr Gedächtnis.) #gloss[kirchliche Untersuchung](Prüfung durch geistliche Stellen. Sie ist besonders beruhigend, solange man sicher ist, dass der Schrecken der richtigen Art angehört.) #gloss[Korrespondenz](Briefwechsel. Die vornehme Kunst, Absichten so lange zu falten, bis sie in ein Kuvert passen.)
Niemand hat das Wort Geist geschrieben.
@@ -1213,11 +1231,11 @@ Niemand hat das Wort Tochter geschrieben.
Doch die Auslassungen ordnen sich auf der Seite an wie Möbel um eine Leiche.
* [__Antworte__: „Es gibt noch eine weitere Weisung.“] #action:conversation
„Es gibt noch eine weitere Weisung.“ #gloss[Weisung](Amtlicher oder dienstlicher Auftrag dessen Befolgung erwartet wird.)
„Es gibt noch eine weitere Weisung.“ #gloss[Weisung](Ein Befehl, dem man die Uniform ausgezogen hat. Sehr nützlich, wenn Gehorsam erwartet wird, aber niemand persönlich Verantwortung tragen möchte.)
Viktor fragt nicht, woher du es weißt.
„Es gibt immer noch eine weitere Weisung“, sagt er. #gloss[Weisung](Amtlicher oder dienstlicher Auftrag dessen Befolgung erwartet wird.)
„Es gibt immer noch eine weitere Weisung“, sagt er. #gloss[Weisung](Ein Befehl, dem man die Uniform ausgezogen hat. Sehr nützlich, wenn Gehorsam erwartet wird, aber niemand persönlich Verantwortung tragen möchte.)
** [__Antworte__: „Für Sie.“] #action:conversation
„Für Sie.“
@@ -1254,7 +1272,7 @@ Doch die Auslassungen ordnen sich auf der Seite an wie Möbel um eine Leiche.
#route:detective
~ detective += 1
~ viktor_trust += 1
„Ihre Fassung ist kürzer als Ihr Schweigen. Das bedeutet, es gibt noch eine weitere Weisung.“ #gloss[Weisung](Amtlicher oder dienstlicher Auftrag dessen Befolgung erwartet wird.)
„Ihre Fassung ist kürzer als Ihr Schweigen. Das bedeutet, es gibt noch eine weitere Weisung.“ #gloss[Weisung](Ein Befehl, dem man die Uniform ausgezogen hat. Sehr nützlich, wenn Gehorsam erwartet wird, aber niemand persönlich Verantwortung tragen möchte.)
Viktor fragt nicht, woher du es weißt.
@@ -1344,14 +1362,14 @@ Der Zug beginnt langsamer zu werden. Der Rhythmus verändert sich zuerst im Bode
* [__Antworte__: „Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“] #action:conversation
#route:eccentric
~ eccentric += 1
„Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“ #gloss[Weisung](Amtlicher oder dienstlicher Auftrag dessen Befolgung erwartet wird.)
„Dann werde ich die Weisung so kunstvoll enttäuschen, wie es die Umstände erlauben.“ #gloss[Weisung](Ein Befehl, dem man die Uniform ausgezogen hat. Sehr nützlich, wenn Gehorsam erwartet wird, aber niemand persönlich Verantwortung tragen möchte.)
„Ich hoffe aufrichtig, dass Sie es nicht tun.“
* [__Antworte__: „Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“] #action:conversation
#route:detective
~ detective += 1
„Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“ #gloss[Weisung](Amtlicher oder dienstlicher Auftrag dessen Befolgung erwartet wird.)
„Dann behalten Sie Ihre zweite Weisung, Herr Nowak. Ich bevorzuge Quellen erster Hand.“ #gloss[Weisung](Ein Befehl, dem man die Uniform ausgezogen hat. Sehr nützlich, wenn Gehorsam erwartet wird, aber niemand persönlich Verantwortung tragen möchte.)
„Eine Vorliebe, die im kaiserlichen Dienst nicht immer gewährt wird.“
+4 -3
View File
@@ -20,15 +20,16 @@ Niemand muss das. Die Nachricht ist bereits ins Dorf eingetreten, auf Wegen schn
Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.
- (dorfbeobachtung)
* [__Schaue__: In die Gesichter am Straßenrand.] #action:orientation #optional #key:l
Die Gesichter verschwinden nicht, wenn du hinsiehst. Sie verändern nur ihre Begründung: Eine Frau prüft plötzlich ihren Eimer. Ein Bub entdeckt die Gänse neu. Ein Mann tut, als habe er schon immer zum Kirchtor gesehen. Das Dorf besitzt keine Bühne, aber jeder hier kennt seinen Auftritt.
-> village_arrival_options
-> dorfbeobachtung
* [__Höre__: Auf das Wasser unter der Straße.] #action:orientation #optional
Unter den Rädern, unter Brettern und Steinen, unter der höflichen Behauptung einer Dorfstraße läuft Wasser. Es klingt nicht tief, aber schnell. Als hätte der Ort einen zweiten Atem, einen kalten, verborgenen, der nicht durch menschliche Münder geht.
-> village_arrival_options
-> dorfbeobachtung
* [__Untersuche__: Die Kirche.] #action:orientation #optional
Der Turm ist nicht schlank genug, um in den Himmel zu zeigen. Er steht da wie eine Faust. Die kleinen Fenster geben wenig preis, und die Mauer des Kirchhofs wirkt weniger wie Einfriedung als wie eine alte Gewohnheit, sich gegen etwas zu stemmen.
@@ -42,7 +43,7 @@ Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.
Die Kirche sieht nicht aus, als habe sie den älteren Dingen im Tal widersprochen. Eher, als habe sie gelernt, über ihnen zu stehen.
}
-> village_arrival_options
-> dorfbeobachtung
* [__Warte__: Bis die Kutsche hält.] #action:social #key:z
-> village_exit_puzzle
File diff suppressed because one or more lines are too long
+3
View File
@@ -21,6 +21,9 @@ export declare class InkEngine {
private restoreState;
private loadStory;
private continueStory;
private isParagraphScopedTag;
private reassignTrailingGlossTags;
private normalizeGlossMatchText;
private getChoiceTags;
private extractChoicePreviewTags;
private resolveInkPath;
+69 -2
View File
@@ -129,6 +129,7 @@ class InkEngine {
const paragraphs = [];
const globalTags = [];
const turnTags = [];
let pendingParagraphTags = [];
while (this.story.canContinue) {
const rawText = this.story.Continue();
const text = String(rawText || '').trim();
@@ -138,12 +139,26 @@ class InkEngine {
.filter((tag) => tag.key === 'title' || tag.key === 'author')
.forEach((tag) => globalTags.push(tag));
if (text) {
paragraphs.push({ text, tags });
const paragraphTags = this.reassignTrailingGlossTags(text, [...pendingParagraphTags, ...tags], paragraphs);
pendingParagraphTags = [];
paragraphs.push({ text, tags: paragraphTags });
}
else {
tags.forEach((tag) => globalTags.push(tag));
const paragraphTags = this.reassignTrailingGlossTags('', tags, paragraphs);
paragraphTags.forEach((tag) => {
if (this.isParagraphScopedTag(tag)) {
pendingParagraphTags.push(tag);
}
else {
globalTags.push(tag);
}
});
}
}
if (pendingParagraphTags.length > 0) {
globalTags.push(...pendingParagraphTags);
pendingParagraphTags = [];
}
const choices = this.story.currentChoices.map((choice) => {
const tags = this.getChoiceTags(choice);
const category = (0, tag_parser_1.getTagValue)(tags, 'action');
@@ -195,6 +210,58 @@ class InkEngine {
gameState: Object.keys(gameState).length > 0 ? gameState : undefined,
};
}
isParagraphScopedTag(tag) {
const key = String(tag?.key || '').toLowerCase();
return ['chapter', 'heading', 'section', 'textblock', 'image', 'music', 'sfx', 'sound', 'audio', 'gloss', 'tts']
.includes(key) || key.startsWith('tts-');
}
reassignTrailingGlossTags(text, tags, paragraphs) {
if (!Array.isArray(tags) || tags.length === 0)
return [];
const previous = paragraphs.length > 0 ? paragraphs[paragraphs.length - 1] : null;
if (!previous)
return tags;
const currentText = this.normalizeGlossMatchText(text);
const previousText = this.normalizeGlossMatchText(previous.text);
const remainingTags = [];
tags.forEach((tag) => {
if (tag.key === 'tts' || tag.key.startsWith('tts-')) {
if (!currentText) {
previous.tags.push(tag);
}
else {
remainingTags.push(tag);
}
return;
}
if (tag.key !== 'gloss') {
remainingTags.push(tag);
return;
}
const term = this.normalizeGlossMatchText(tag.value || '');
if (!term) {
remainingTags.push(tag);
return;
}
const matchesCurrent = currentText.includes(term);
const matchesPrevious = previousText.includes(term);
if (!matchesCurrent && matchesPrevious) {
previous.tags.push(tag);
}
else {
remainingTags.push(tag);
}
});
return remainingTags;
}
normalizeGlossMatchText(value) {
return String(value || '')
.normalize('NFC')
.toLocaleLowerCase('de-DE')
.replace(/[.,;:!?()[\]{}"'„“”‚‘’»«]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
getChoiceTags(choice) {
const directTags = (0, tag_parser_1.parseTags)(choice?.tags || []);
const previewTags = this.extractChoicePreviewTags(choice);
+1 -1
View File
File diff suppressed because one or more lines are too long
+21 -7
View File
@@ -112,15 +112,27 @@ function getOrCreateEngine(socketId) {
}
return engine;
}
async function handleGameApi(socket, method, args) {
function withClientRequestId(turn, requestId) {
const id = Number(requestId || 0);
return Number.isInteger(id) && id > 0
? { ...turn, clientRequestId: id }
: turn;
}
async function handleGameApi(socket, method, args, requestId) {
const slots = getSlots(socket.id);
switch (method) {
case 'newGame':
case 'newGame()': {
const engine = new ink_engine_1.InkEngine(getStoryPath());
sessions.set(socket.id, engine);
socket.emit('narrativeResponse', engine.newGame());
return { success: true, result: true, running: true, canLoad: slots.size > 0 };
socket.emit('narrativeResponse', withClientRequestId(engine.newGame(), requestId));
return {
success: true,
result: true,
running: true,
canLoad: slots.size > 0,
savedState: engine.saveGame(),
};
}
case 'chooseChoice':
case 'chooseChoice()': {
@@ -132,7 +144,7 @@ async function handleGameApi(socket, method, args) {
if (!Number.isInteger(choiceIndex)) {
return { success: false, error: 'invalid_choice', result: false };
}
socket.emit('narrativeResponse', engine.chooseChoice(choiceIndex));
socket.emit('narrativeResponse', withClientRequestId(engine.chooseChoice(choiceIndex), requestId));
return { success: true, result: true };
}
case 'loadGame':
@@ -143,8 +155,8 @@ async function handleGameApi(socket, method, args) {
return { success: false, error: 'missing_save', result: false };
}
const engine = getOrCreateEngine(socket.id);
socket.emit('narrativeResponse', engine.loadGame(browserSave || slots.get(slot)));
socket.emit('gameLoaded', { slot });
socket.emit('narrativeResponse', withClientRequestId(engine.loadGame(browserSave || slots.get(slot)), requestId));
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
return { success: true, result: true, running: true, slot };
}
case 'resumeGame':
@@ -198,7 +210,9 @@ io.on('connection', (socket) => {
socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig));
socket.on('gameApi', async (request, respond) => {
try {
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []);
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : [], Number.isInteger(Number(request?.requestId)) && Number(request?.requestId) > 0
? Number(request?.requestId)
: undefined);
if (typeof respond === 'function')
respond(result);
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -8
View File
@@ -103,7 +103,13 @@ function normalizeSaveSlot(slot) {
const value = Number(slot);
return Number.isInteger(value) && value > 0 ? value : 1;
}
async function startDemoGameForSocket(socket) {
function withClientRequestId(turn, requestId) {
const id = Number(requestId || 0);
return Number.isInteger(id) && id > 0
? { ...turn, clientRequestId: id }
: turn;
}
async function startDemoGameForSocket(socket, requestId) {
nextTurnIds.set(socket.id, 1);
const gameRunner = new game_runner_1.GameRunner();
const worldFile = (0, game_config_1.projectPath)(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile);
@@ -114,7 +120,7 @@ async function startDemoGameForSocket(socket) {
...(0, turn_result_1.textToParagraphs)(gameState.world.introduction),
...(0, turn_result_1.textToParagraphs)(gameRunner.getCurrentRoomDescription()),
];
socket.emit('narrativeResponse', {
socket.emit('narrativeResponse', withClientRequestId({
turnId: nextTurnId(socket.id),
paragraphs,
choices: [],
@@ -122,16 +128,16 @@ async function startDemoGameForSocket(socket) {
gameState: {
currentRoomId: gameState.currentRoomId,
},
});
}, requestId));
return gameRunner;
}
async function handleGameApi(socket, method, args = []) {
async function handleGameApi(socket, method, args = [], requestId) {
const saveGames = socket.data.saveGames || new Map();
socket.data.saveGames = saveGames;
switch (method) {
case 'newGame':
case 'newGame()':
await startDemoGameForSocket(socket);
await startDemoGameForSocket(socket, requestId);
return { success: true, result: true, running: true, canLoad: saveGames.size > 0 };
case 'loadGame':
case 'loadGame()': {
@@ -139,8 +145,8 @@ async function handleGameApi(socket, method, args = []) {
if (!saveGames.has(slot)) {
return { success: false, error: 'missing_save', result: false };
}
await startDemoGameForSocket(socket);
socket.emit('gameLoaded', { slot });
await startDemoGameForSocket(socket, requestId);
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
return { success: true, result: true, running: true, slot };
}
case 'saveGame':
@@ -176,7 +182,8 @@ io.on('connection', (socket) => {
socket.data.saveGames = new Map();
socket.on('gameApi', async (request, respond) => {
try {
const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : []);
const requestId = Number(request?.requestId || 0);
const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : [], Number.isInteger(requestId) && requestId > 0 ? requestId : undefined);
if (typeof respond === 'function') {
respond(response);
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+13 -5
View File
@@ -127,7 +127,13 @@ function getSlots(socketId) {
}
return slots;
}
async function handleGameApi(socket, method, args) {
function withClientRequestId(turn, requestId) {
const id = Number(requestId || 0);
return Number.isInteger(id) && id > 0
? { ...turn, clientRequestId: id }
: turn;
}
async function handleGameApi(socket, method, args, requestId) {
const slots = getSlots(socket.id);
debugLog(`gameApi request from ${socket.id}: ${method}`, { args });
switch (method) {
@@ -135,7 +141,7 @@ async function handleGameApi(socket, method, args) {
case 'newGame()': {
const engine = getOrCreateEngine(socket.id);
const turn = await engine.newGame();
socket.emit('narrativeResponse', toClientTurn(turn));
socket.emit('narrativeResponse', withClientRequestId(toClientTurn(turn), requestId));
return {
success: true,
result: true,
@@ -151,8 +157,8 @@ async function handleGameApi(socket, method, args) {
}
const engine = getOrCreateEngine(socket.id);
const turn = await engine.loadGame(slots.get(slot));
socket.emit('narrativeResponse', toClientTurn(turn));
socket.emit('gameLoaded', { slot });
socket.emit('narrativeResponse', withClientRequestId(toClientTurn(turn), requestId));
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
return { success: true, result: true, running: true, slot };
}
case 'saveGame':
@@ -229,7 +235,9 @@ io.on('connection', (socket) => {
socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig));
socket.on('gameApi', async (request, respond) => {
try {
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []);
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : [], Number.isInteger(Number(request?.requestId)) && Number(request?.requestId) > 0
? Number(request?.requestId)
: undefined);
debugLog(`gameApi response to ${socket.id}`, result);
if (typeof respond === 'function')
respond(result);
+1 -1
View File
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
node_modules
speech_cache
-2
View File
@@ -1,2 +0,0 @@
GET https://api.elevenlabs.io/v1/voices
xi-api-key: d191e27c2e5b07573b39fe70f0783f48
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-931
View File
@@ -1,931 +0,0 @@
/**
* @license Hyphenopoly 5.2.0-beta.1 - client side hyphenation for webbrowsers
* ©2023 Mathias Nater, Güttingen (mathiasnater at gmail dot com)
* https://github.com/mnater/Hyphenopoly
*
* Released under the MIT license
* http://mnater.github.io/Hyphenopoly/LICENSE
*/
/* globals Hyphenopoly:readonly */
((w, o) => {
"use strict";
const SOFTHYPHEN = "\u00AD";
/**
* Event
*/
const event = ((H) => {
const knownEvents = new Map([
["afterElementHyphenation", []],
["beforeElementHyphenation", []],
["engineReady", []],
[
"error", [
(e) => {
if (e.runDefault) {
w.console.warn(e);
}
}
]
],
["hyphenopolyEnd", []],
["hyphenopolyStart", []]
]);
if (H.hev) {
const userEvents = new Map(o.entries(H.hev));
knownEvents.forEach((eventFuncs, eventName) => {
if (userEvents.has(eventName)) {
eventFuncs.unshift(userEvents.get(eventName));
}
});
}
return {
"fire": ((eventName, eventData) => {
eventData.runDefault = true;
eventData.preventDefault = () => {
eventData.runDefault = false;
};
knownEvents.get(eventName).forEach((eventFn) => {
eventFn(eventData);
});
})
};
})(Hyphenopoly);
/**
* Register copy event on element
* @param {Object} el The element
* @returns {undefined}
*/
function registerOnCopy(el) {
el.addEventListener(
"copy",
(e) => {
e.preventDefault();
const sel = w.getSelection();
const div = document.createElement("div");
div.appendChild(sel.getRangeAt(0).cloneContents());
e.clipboardData.setData("text/plain", sel.toString().replace(RegExp(SOFTHYPHEN, "g"), ""));
e.clipboardData.setData("text/html", div.innerHTML.replace(RegExp(SOFTHYPHEN, "g"), ""));
},
true
);
}
/**
* Convert settings from H.setup-Object to Map
* This is a IIFE to keep complexity low.
*/
((H) => {
/**
* Create a Map with a default Map behind the scenes. This mimics
* kind of a prototype chain of an object, but without the object-
* injection security risk.
*
* @param {Map} defaultsMap - A Map with default values
* @returns {Proxy} - A Proxy for the Map (dot-notation or get/set)
*/
function createMapWithDefaults(defaultsMap) {
const userMap = new Map();
/**
* The get-trap: get the value from userMap or else from defaults
* @param {Sring} key - The key to retrieve the value for
* @returns {*}
*/
function get(key) {
return (userMap.has(key))
? userMap.get(key)
: defaultsMap.get(key);
}
/**
* The set-trap: set the value to userMap and don't touch defaults
* @param {Sring} key - The key for the value
* @param {*} value - The value
* @returns {*}
*/
function set(key, value) {
userMap.set(key, value);
}
return new Proxy(defaultsMap, {
"get": (_target, prop) => {
if (prop === "set") {
return set;
}
if (prop === "get") {
return get;
}
return get(prop);
},
"ownKeys": () => {
return [
...new Set(
[...defaultsMap.keys(), ...userMap.keys()]
)
];
}
});
}
const settings = createMapWithDefaults(new Map([
["defaultLanguage", "en-us"],
[
"dontHyphenate", (() => {
const list = "abbr,acronym,audio,br,button,code,img,input,kbd,label,math,option,pre,samp,script,style,sub,sup,svg,textarea,var,video";
return createMapWithDefaults(
new Map(list.split(",").map((val) => {
return [val, true];
}))
);
})()
],
["dontHyphenateClass", "donthyphenate"],
["exceptions", new Map()],
["keepAlive", true],
["normalize", false],
["processShadows", false],
["safeCopy", true],
["substitute", new Map()],
["timeout", 1000]
]));
o.entries(H.s).forEach(([key, value]) => {
switch (key) {
case "selectors":
// Set settings.selectors to array of selectors
settings.set("selectors", o.keys(value));
/*
* For each selector add a property to settings with
* selector specific settings
*/
o.entries(value).forEach(([sel, selSettings]) => {
const selectorSettings = createMapWithDefaults(new Map([
["compound", "hyphen"],
["hyphen", SOFTHYPHEN],
["leftmin", 0],
["leftminPerLang", 0],
["minWordLength", 6],
["mixedCase", true],
["orphanControl", 1],
["rightmin", 0],
["rightminPerLang", 0]
]));
o.entries(selSettings).forEach(
([selSetting, setVal]) => {
if (typeof setVal === "object") {
selectorSettings.set(
selSetting,
new Map(o.entries(setVal))
);
} else {
selectorSettings.set(selSetting, setVal);
}
}
);
settings.set(sel, selectorSettings);
});
break;
case "dontHyphenate":
case "exceptions":
o.entries(value).forEach(([k, v]) => {
settings.get(key).set(k, v);
});
break;
case "substitute":
o.entries(value).forEach(([lang, subst]) => {
settings.substitute.set(
lang,
new Map(o.entries(subst))
);
});
break;
default:
settings.set(key, value);
}
});
H.c = settings;
})(Hyphenopoly);
((H) => {
const C = H.c;
let mainLanguage = null;
event.fire(
"hyphenopolyStart",
{
"msg": "hyphenopolyStart"
}
);
/**
* Factory for elements
* @returns {Object} elements-object
*/
function makeElementCollection() {
const list = new Map();
/*
* Counter counts the elements to be hyphenated.
* Needs to be an object (Pass by reference)
*/
const counter = [0];
/**
* Add element to elements
* @param {object} el The element
* @param {string} lang The language of the element
* @param {string} sel The selector of the element
* @returns {Object} An element-object
*/
function add(el, lang, sel) {
const elo = {
"element": el,
"selector": sel
};
if (!list.has(lang)) {
list.set(lang, []);
}
list.get(lang).push(elo);
counter[0] += 1;
return elo;
}
/**
* Removes elements from the list and updates the counter
* @param {string} lang - The lang of the elements to remove
*/
function rem(lang) {
let langCount = 0;
if (list.has(lang)) {
langCount = list.get(lang).length;
list.delete(lang);
counter[0] -= langCount;
if (counter[0] === 0) {
event.fire(
"hyphenopolyEnd",
{
"msg": "hyphenopolyEnd"
}
);
if (!C.keepAlive) {
window.Hyphenopoly = null;
}
}
}
}
return {
add,
counter,
list,
rem
};
}
/**
* Get language of element by searching its parents or fallback
* @param {Object} el The element
* @param {string} parentLang Lang of parent if available
* @param {boolean} fallback Will falback to mainlanguage
* @returns {string|null} The language or null
*/
function getLang(el, parentLang = "", fallback = true) {
// Find closest el with lang attr not empty
el = el.closest("[lang]:not([lang=''])");
if (el && el.lang) {
return el.lang.toLowerCase();
}
if (parentLang) {
return parentLang;
}
return (fallback)
? mainLanguage
: null;
}
/**
* Collect elements that have a selector defined in C.selectors
* and add them to elements.
* @param {Object} [parent = null] The start point element
* @param {string} [selector = null] The selector matching the parent
* @returns {Object} elements-object
*/
function collectElements(parent = null, selector = null) {
const elements = makeElementCollection();
const dontHyphenateSelector = (() => {
let s = "." + C.dontHyphenateClass;
o.getOwnPropertyNames(C.dontHyphenate).forEach((tag) => {
if (C.dontHyphenate.get(tag)) {
s += "," + tag;
}
});
return s;
})();
const matchingSelectors = C.selectors.join(",") + "," + dontHyphenateSelector;
/**
* Recursively walk all elements in el, lending lang and selName
* add them to elements if necessary.
* @param {Object} el The element to scan
* @param {string} pLang The language of the parent element
* @param {string} sel The selector of the parent element
* @param {boolean} isChild If el is a child element
* @returns {undefined}
*/
function processElements(el, pLang, sel, isChild = false) {
const eLang = getLang(el, pLang);
const langDef = H.cf.langs.get(eLang);
if (langDef === "H9Y") {
elements.add(el, eLang, sel);
if (!isChild && C.safeCopy) {
registerOnCopy(el);
}
} else if (!langDef && eLang !== "zxx") {
event.fire(
"error",
Error(`Element with '${eLang}' found, but '${eLang}.wasm' not loaded. Check language tags!`)
);
}
el.childNodes.forEach((n) => {
if (n.nodeType === 1 && !n.matches(matchingSelectors)) {
processElements(n, eLang, sel, true);
}
});
}
/**
* Searches the DOM for each sel
* @param {object} root The DOM root
* @returns {undefined}
*/
function getElems(root) {
C.selectors.forEach((sel) => {
root.querySelectorAll(sel).forEach((n) => {
processElements(n, getLang(n), sel, false);
});
});
}
if (parent === null) {
if (C.processShadows) {
w.document.querySelectorAll("*").forEach((m) => {
if (m.shadowRoot) {
getElems(m.shadowRoot);
}
});
}
getElems(w.document);
} else {
processElements(parent, getLang(parent), selector);
}
return elements;
}
const wordHyphenatorPool = new Map();
/**
* Factory for hyphenatorFunctions for a specific language and selector
* @param {Object} lo Language-Object
* @param {string} lang The language
* @param {string} sel The selector
* @returns {function} The hyphenate function
*/
function createWordHyphenator(lo, lang, sel) {
const poolKey = lang + "-" + sel;
if (wordHyphenatorPool.has(poolKey)) {
return wordHyphenatorPool.get(poolKey);
}
const selSettings = C.get(sel);
lo.cache.set(sel, new Map());
/**
* HyphenateFunction for non-compound words
* @param {string} word The word
* @returns {string} The hyphenated word
*/
function hyphenateNormal(word) {
if (word.length > 61) {
event.fire(
"error",
Error("Found word longer than 61 characters")
);
} else if (!lo.reNotAlphabet.test(word)) {
return lo.hyphenate(
word,
selSettings.hyphen.charCodeAt(0),
selSettings.leftminPerLang.get(lang),
selSettings.rightminPerLang.get(lang)
);
}
return word;
}
/**
* HyphenateFunction for compound words
* @param {string} word The word
* @returns {string} The hyphenated compound word
*/
function hyphenateCompound(word) {
const zeroWidthSpace = "\u200B";
let parts = null;
let wordHyphenator = null;
if (selSettings.compound === "auto" ||
selSettings.compound === "all") {
wordHyphenator = createWordHyphenator(lo, lang, sel);
parts = word.split("-").map((p) => {
if (p.length >= selSettings.minWordLength) {
return wordHyphenator(p);
}
return p;
});
if (selSettings.compound === "auto") {
word = parts.join("-");
} else {
word = parts.join("-" + zeroWidthSpace);
}
} else {
word = word.replace("-", "-" + zeroWidthSpace);
}
return word;
}
/**
* Checks if a string is mixed case
* @param {string} s The string
* @returns {boolean} true if s is mixed case
*/
function isMixedCase(s) {
return [...s].map((c) => {
return (c === c.toLowerCase());
}).some((v, i, a) => {
return (v !== a[0]);
});
}
/**
* HyphenateFunction for words (compound or not)
* @param {string} word The word
* @returns {string} The hyphenated word
*/
function hyphenator(word) {
let hw = lo.cache.get(sel).get(word);
if (!hw) {
if (lo.exc.has(word)) {
hw = lo.exc.get(word).replace(
/-/g,
selSettings.hyphen
);
} else if (!selSettings.mixedCase && isMixedCase(word)) {
hw = word;
} else if (word.indexOf("-") === -1) {
hw = hyphenateNormal(word);
} else {
hw = hyphenateCompound(word);
}
lo.cache.get(sel).set(word, hw);
}
return hw;
}
wordHyphenatorPool.set(poolKey, hyphenator);
return hyphenator;
}
const orphanControllerPool = new Map();
/**
* Factory for function that handles orphans
* @param {string} sel The selector
* @returns {function} The function created
*/
function createOrphanController(sel) {
if (orphanControllerPool.has(sel)) {
return orphanControllerPool.get(sel);
}
const selSettings = C.get(sel);
/**
* Function template
* @param {string} ignore unused result of replace
* @param {string} leadingWhiteSpace The leading whiteSpace
* @param {string} lastWord The last word
* @param {string} trailingWhiteSpace The trailing whiteSpace
* @returns {string} Treated end of text
*/
function controlOrphans(
ignore,
leadingWhiteSpace,
lastWord,
trailingWhiteSpace
) {
if (selSettings.orphanControl === 3 && leadingWhiteSpace === " ") {
// \u00A0 = no-break space (nbsp)
leadingWhiteSpace = "\u00A0";
}
return leadingWhiteSpace + lastWord.replace(RegExp(selSettings.hyphen, "g"), "") + trailingWhiteSpace;
}
orphanControllerPool.set(sel, controlOrphans);
return controlOrphans;
}
const wordRegExpPool = new Map();
/**
* Hyphenate an entitiy (text string or Element-Object)
* @param {string} lang - the language of the string
* @param {string} sel - the selectorName of settings
* @param {string} entity - the entity to be hyphenated
* @returns {string | null} hyphenated str according to setting of sel
*/
function hyphenate(lang, sel, entity) {
const lo = H.languages.get(lang);
const selSettings = C.get(sel);
const minWordLength = selSettings.minWordLength;
const regExpWord = (() => {
const key = lang + minWordLength;
if (wordRegExpPool.has(key)) {
return wordRegExpPool.get(key);
}
/*
* Transpiled RegExp of
* /[${alphabet}\p{Mn}Subset\p{Letter}\00AD-]
* {${minwordlength},}/gui
*/
const reWord = RegExp(
`[${lo.alphabet}a-z\u0300-\u036F\u0483-\u0487\u00DF-\u00F6\u00F8-\u00FE\u0101\u0103\u0105\u0107\u0109\u010D\u010F\u0111\u0113\u0117\u0119\u011B\u011D\u011F\u0123\u0125\u012B\u012F\u0131\u0135\u0137\u013C\u013E\u0142\u0144\u0146\u0148\u014D\u0151\u0153\u0155\u0159\u015B\u015D\u015F\u0161\u0165\u016B\u016D\u016F\u0171\u0173\u017A\u017C\u017E\u017F\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC\u0219\u021B\u02BC\u0390\u03AC-\u03CE\u03D0\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF\u03F2\u0430-\u044F\u0451-\u045C\u045E\u045F\u0491\u04AF\u04E9\u0561-\u0585\u0587\u0905-\u090C\u090F\u0910\u0913-\u0928\u092A-\u0930\u0932\u0933\u0935-\u0939\u093D\u0960\u0961\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A85-\u0A8B\u0A8F\u0A90\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AE0\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B60\u0B61\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D60\u0D61\u0D7A-\u0D7F\u0E01-\u0E2E\u0E30\u0E32\u0E33\u0E40-\u0E45\u10D0-\u10F0\u1200-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u1E0D\u1E37\u1E41\u1E43\u1E45\u1E47\u1E6D\u1F00-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB2-\u1FB4\u1FB6\u1FB7\u1FC2-\u1FC4\u1FC6\u1FC7\u1FD2\u1FD3\u1FD6\u1FD7\u1FE2-\u1FE7\u1FF2-\u1FF4\u1FF6\u1FF7\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CC9\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\u00AD\u200B-\u200D-]{${minWordLength},}`, "gui"
);
wordRegExpPool.set(key, reWord);
return reWord;
})();
/**
* Hyphenate text according to setting in sel
* @param {string} text - the strint to be hyphenated
* @returns {string} hyphenated string according to setting of sel
*/
function hyphenateText(text) {
if (C.normalize) {
text = text.normalize("NFC");
}
let tn = text.replace(
regExpWord,
createWordHyphenator(lo, lang, sel)
);
if (selSettings.orphanControl !== 1) {
tn = tn.replace(
/(\u0020*)(\S+)(\s*)$/,
createOrphanController(sel)
);
}
return tn;
}
/**
* Hyphenate element according to setting in sel
* @param {object} el - the HTMLElement to be hyphenated
* @returns {undefined}
*/
function hyphenateElement(el) {
event.fire(
"beforeElementHyphenation",
{
el,
lang
}
);
el.childNodes.forEach((n) => {
if (
n.nodeType === 3 &&
(/\S/).test(n.data) &&
n.data.length >= minWordLength
) {
n.data = hyphenateText(n.data);
}
});
H.res.els.counter[0] -= 1;
event.fire(
"afterElementHyphenation",
{
el,
lang
}
);
}
let r = null;
if (typeof entity === "string") {
r = hyphenateText(entity);
} else if (entity instanceof HTMLElement) {
hyphenateElement(entity);
}
return r;
}
/**
* Creates a language-specific string hyphenator
* @param {String} lang - The language this hyphenator hyphenates
*/
function createStringHyphenator(lang) {
return ((entity, sel = ".hyphenate") => {
if (typeof entity !== "string") {
event.fire(
"error",
Error("This use of hyphenators is deprecated. See https://mnater.github.io/Hyphenopoly/Hyphenators.html")
);
}
return hyphenate(lang, sel, entity);
});
}
/**
* Creates a polyglot HTML hyphenator
*/
function createDOMHyphenator() {
return ((entity, sel = ".hyphenate") => {
collectElements(entity, sel).list.forEach((els, l) => {
els.forEach((elo) => {
hyphenate(l, elo.selector, elo.element);
});
});
return null;
});
}
H.unhyphenate = () => {
H.res.els.list.forEach((els) => {
els.forEach((elo) => {
const n = elo.element.firstChild;
n.data = n.data.replace(RegExp(C[elo.selector].hyphen, "g"), "");
});
});
return Promise.resolve(H.res.els);
};
/**
* Hyphenate all elements with a given language
* @param {string} lang The language
* @param {Array} elArr Array of elements
* @returns {undefined}
*/
function hyphenateLangElements(lang, elements) {
const elArr = elements.list.get(lang);
if (elArr) {
elArr.forEach((elo) => {
hyphenate(lang, elo.selector, elo.element);
});
} else {
event.fire(
"error",
Error(`Engine for language '${lang}' loaded, but no elements found.`)
);
}
if (elements.counter[0] === 0) {
w.clearTimeout(H.timeOutHandler);
H.hide(0, null);
event.fire(
"hyphenopolyEnd",
{
"msg": "hyphenopolyEnd"
}
);
if (!C.keepAlive) {
window.Hyphenopoly = null;
}
}
}
/**
* Convert the exceptions from user input to Map
* @param {string} lang - The language for which the Map is created
* @return {Map}
*/
function createExceptionMap(lang) {
let exc = "";
if (C.exceptions.has(lang)) {
exc = C.exceptions.get(lang);
}
if (C.exceptions.has("global")) {
if (exc === "") {
exc = C.exceptions.get("global");
} else {
exc += ", " + C.exceptions.get("global");
}
}
if (exc === "") {
return new Map();
}
return new Map(exc.split(", ").map((e) => {
return [e.replace(/-/g, ""), e];
}));
}
/**
* Setup lo
* @param {string} lang The language
* @param {function} hyphenateFunction The hyphenateFunction
* @param {string} alphabet List of used characters
* @param {number} leftmin leftmin
* @param {number} rightmin rightmin
* @returns {undefined}
*/
function prepareLanguagesObj(
lang,
hyphenateFunction,
alphabet,
patternLeftmin,
patternRightmin
) {
C.selectors.forEach((sel) => {
const selSettings = C.get(sel);
if (selSettings.leftminPerLang === 0) {
selSettings.set("leftminPerLang", new Map());
}
if (selSettings.rightminPerLang === 0) {
selSettings.set("rightminPerLang", new Map());
}
selSettings.leftminPerLang.set(lang, Math.max(
patternLeftmin,
selSettings.leftmin,
Number(selSettings.leftminPerLang.get(lang)) || 0
));
selSettings.rightminPerLang.set(lang, Math.max(
patternRightmin,
selSettings.rightmin,
Number(selSettings.rightminPerLang.get(lang)) || 0
));
});
if (!H.languages) {
H.languages = new Map();
}
alphabet = alphabet.replace(/\\*-/g, "\\-");
H.languages.set(lang, {
alphabet,
"cache": new Map(),
"exc": createExceptionMap(lang),
"hyphenate": hyphenateFunction,
"ready": true,
"reNotAlphabet": RegExp(`[^${alphabet}]`, "i")
});
H.hy6ors.get(lang).resolve(createStringHyphenator(lang));
event.fire(
"engineReady",
{
lang
}
);
if (H.res.els) {
hyphenateLangElements(lang, H.res.els);
}
}
const decode = (() => {
const utf16ledecoder = new TextDecoder("utf-16le");
return ((ui16) => {
return utf16ledecoder.decode(ui16);
});
})();
/**
* Setup env for hyphenateFunction
* @param {ArrayBuffer} buf Memory buffer
* @param {function} hyphenateFunc hyphenateFunction
* @returns {function} hyphenateFunction with closured environment
*/
function encloseHyphenateFunction(buf, hyphenateFunc) {
const wordStore = new Uint16Array(buf, 0, 64);
return ((word, hyphencc, leftmin, rightmin) => {
wordStore.set([
...[...word].map((c) => {
return c.charCodeAt(0);
}),
0
]);
const len = hyphenateFunc(leftmin, rightmin, hyphencc);
if (len > 0) {
word = decode(
new Uint16Array(buf, 0, len)
);
}
return word;
});
}
/**
* Instantiate Wasm Engine
* @param {string} lang The language
* @returns {undefined}
*/
function instantiateWasmEngine(heProm, lang) {
const wa = window.WebAssembly;
/**
* Register character substitutions in the .wasm-hyphenEngine
* @param {number} alphalen - The length of the alphabet
* @param {object} exp - Export-object of the hyphenEngine
*/
function registerSubstitutions(alphalen, exp) {
if (C.substitute.has(lang)) {
const subst = C.substitute.get(lang);
subst.forEach((substituer, substituted) => {
const substitutedU = substituted.toUpperCase();
const substitutedUcc = (substitutedU === substituted)
? 0
: substitutedU.charCodeAt(0);
alphalen = exp.subst(
substituted.charCodeAt(0),
substitutedUcc,
substituer.charCodeAt(0)
);
});
}
return alphalen;
}
/**
* Instantiate the hyphenEngine
* @param {object} res - The fetched ressource
*/
function handleWasm(res) {
const exp = res.instance.exports;
// eslint-disable-next-line multiline-ternary
let alphalen = (wa.Global) ? exp.lct.value : exp.lct;
alphalen = registerSubstitutions(alphalen, exp);
heProm.l.forEach((l) => {
prepareLanguagesObj(
l,
encloseHyphenateFunction(
exp.mem.buffer,
exp.hyphenate
),
decode(new Uint16Array(exp.mem.buffer, 1408, alphalen)),
/* eslint-disable multiline-ternary */
(wa.Global) ? exp.lmi.value : exp.lmi,
(wa.Global) ? exp.rmi.value : exp.rmi
/* eslint-enable multiline-ternary */
);
});
}
heProm.w.then((response) => {
if (response.ok) {
if (
wa.instantiateStreaming &&
(response.headers.get("Content-Type") === "application/wasm")
) {
return wa.instantiateStreaming(response);
}
return response.arrayBuffer().then((ab) => {
return wa.instantiate(ab);
});
}
return Promise.reject(Error(`File ${lang}.wasm can't be loaded from ${H.paths.patterndir}`));
}).then(handleWasm, (e) => {
event.fire("error", e);
H.res.els.rem(lang);
});
}
H.main = () => {
H.res.DOM.then(() => {
mainLanguage = getLang(w.document.documentElement, "", false);
if (!mainLanguage && C.defaultLanguage !== "") {
mainLanguage = C.defaultLanguage;
}
const elements = collectElements();
H.res.els = elements;
elements.list.forEach((ignore, lang) => {
if (H.languages &&
H.languages.has(lang) &&
H.languages.get(lang).ready
) {
hyphenateLangElements(lang, elements);
}
});
});
H.res.he.forEach(instantiateWasmEngine);
Promise.all(
// Make sure all lang specific hyphenators and DOM are ready
[...H.hy6ors.entries()].
reduce((accumulator, value) => {
if (value[0] !== "HTML") {
return accumulator.concat(value[1]);
}
return accumulator;
}, []).
concat(H.res.DOM)
).then(() => {
H.hy6ors.get("HTML").resolve(createDOMHyphenator());
}, (e) => {
event.fire("error", e);
});
};
H.main();
})(Hyphenopoly);
})(window, Object);
-347
View File
@@ -1,347 +0,0 @@
/**
* @license Hyphenopoly_Loader 5.2.0-beta.1 - client side hyphenation
* ©2023 Mathias Nater, Güttingen (mathiasnater at gmail dot com)
* https://github.com/mnater/Hyphenopoly
*
* Released under the MIT license
* http://mnater.github.io/Hyphenopoly/LICENSE
*/
/* globals Hyphenopoly:readonly */
window.Hyphenopoly = {};
((w, d, H, o) => {
"use strict";
/**
* Shortcut for new Map
* @param {any} init - initialiser for new Map
* @returns {Map}
*/
const mp = (init) => {
return new Map(init);
};
const scriptName = "Hyphenopoly_Loader.js";
const thisScript = d.currentScript.src;
const store = sessionStorage;
let mainScriptLoaded = false;
/**
* The main function runs the feature test and loads Hyphenopoly if
* necessary.
*/
const main = (() => {
const shortcuts = {
"ac": "appendChild",
"ce": "createElement",
"ct": "createTextNode"
};
/**
* Create deferred Promise
*
* From http://lea.verou.me/2016/12/resolve-promises-externally-with-
* this-one-weird-trick/
* @return {promise}
*/
const defProm = () => {
let res = null;
let rej = null;
const promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
promise.resolve = res;
promise.reject = rej;
return promise;
};
H.ac = new AbortController();
const fetchOptions = {
"credentials": H.s.CORScredentials,
"signal": H.ac.signal
};
let stylesNode = null;
/**
* Define function H.hide.
* This function hides (state = 1) or unhides (state = 0)
* the whole document (mode == 0) or
* each selected element (mode == 1) or
* text of each selected element (mode == 2) or
* nothing (mode == -1)
* @param {integer} state - State
* @param {integer} mode - Mode
*/
H.hide = (state, mode) => {
if (state) {
let vis = "{visibility:hidden!important}";
stylesNode = d[shortcuts.ce]("style");
let myStyle = "";
if (mode === 0) {
myStyle = "html" + vis;
} else if (mode !== -1) {
if (mode === 2) {
vis = "{color:transparent!important}";
}
o.keys(H.s.selectors).forEach((sel) => {
myStyle += sel + vis;
});
}
stylesNode[shortcuts.ac](d[shortcuts.ct](myStyle));
d.head[shortcuts.ac](stylesNode);
} else if (stylesNode) {
stylesNode.remove();
}
};
const tester = (() => {
let fakeBody = null;
return {
/**
* Append fakeBody with tests to document
* @returns {Object|null} The body element or null, if no tests
*/
"ap": () => {
if (fakeBody) {
d.documentElement[shortcuts.ac](fakeBody);
return fakeBody;
}
return null;
},
/**
* Remove fakeBody
* @returns {undefined}
*/
"cl": () => {
if (fakeBody) {
fakeBody.remove();
}
},
/**
* Create and append div with CSS-hyphenated word
* @param {string} lang Language
* @returns {undefined}
*/
"cr": (lang) => {
if (H.cf.langs.has(lang)) {
return;
}
fakeBody = fakeBody || d[shortcuts.ce]("body");
const testDiv = d[shortcuts.ce]("div");
const ha = "hyphens:auto";
testDiv.lang = lang;
testDiv.style.cssText = `visibility:hidden;-webkit-${ha};-ms-${ha};${ha};width:48px;font-size:12px;line-height:12px;border:none;padding:0;word-wrap:normal`;
testDiv[shortcuts.ac](
d[shortcuts.ct](H.lrq.get(lang).wo.toLowerCase())
);
fakeBody[shortcuts.ac](testDiv);
}
};
})();
/**
* Checks if hyphens (ev.prefixed) is set to auto for the element.
* @param {Object} elm - the element
* @returns {Boolean} result of the check
*/
const checkCSSHyphensSupport = (elmStyle) => {
const h = elmStyle.hyphens ||
elmStyle.webkitHyphens ||
elmStyle.msHyphens;
return (h === "auto");
};
H.res = {
"he": mp()
};
/**
* Load hyphenEngines to H.res.he
*
* Make sure each .wasm is loaded exactly once, even for fallbacks
* Store a list of languages to by hyphenated with each .wasm
* @param {string} lang The language
* @returns {undefined}
*/
const loadhyphenEngine = (lang) => {
const fn = H.lrq.get(lang).fn;
H.cf.pf = true;
H.cf.langs.set(lang, "H9Y");
if (H.res.he.has(fn)) {
H.res.he.get(fn).l.push(lang);
} else {
H.res.he.set(
fn,
{
"l": [lang],
"w": w.fetch(H.paths.patterndir + fn + ".wasm", fetchOptions)
}
);
}
};
H.lrq.forEach((value, lang) => {
if (value.wo === "FORCEHYPHENOPOLY" || H.cf.langs.get(lang) === "H9Y") {
loadhyphenEngine(lang);
} else {
tester.cr(lang);
}
});
const testContainer = tester.ap();
if (testContainer) {
testContainer.querySelectorAll("div").forEach((n) => {
if (checkCSSHyphensSupport(n.style) && n.offsetHeight > 12) {
H.cf.langs.set(n.lang, "CSS");
} else {
loadhyphenEngine(n.lang);
}
});
tester.cl();
}
const hev = H.hev;
if (H.cf.pf) {
H.res.DOM = new Promise((res) => {
if (d.readyState === "loading") {
d.addEventListener(
"DOMContentLoaded",
res,
{
"once": true,
"passive": true
}
);
} else {
res();
}
});
H.hide(1, H.s.hide);
H.timeOutHandler = w.setTimeout(() => {
H.hide(0, null);
// eslint-disable-next-line no-bitwise
if (H.s.timeout & 1) {
H.ac.abort();
}
// eslint-disable-next-line no-console
console.info(scriptName + " timed out.");
}, H.s.timeout);
if (mainScriptLoaded) {
H.main();
} else {
// Load main script
fetch(H.paths.maindir + "Hyphenopoly.js", fetchOptions).
then((response) => {
if (response.ok) {
response.blob().then((blb) => {
const script = d[shortcuts.ce]("script");
script.src = URL.createObjectURL(blb);
d.head[shortcuts.ac](script);
mainScriptLoaded = true;
URL.revokeObjectURL(script.src);
});
}
});
}
H.hy6ors = mp();
H.cf.langs.forEach((langDef, lang) => {
if (langDef === "H9Y") {
H.hy6ors.set(lang, defProm());
}
});
H.hy6ors.set("HTML", defProm());
H.hyphenators = new Proxy(H.hy6ors, {
"get": (target, key) => {
return target.get(key);
},
"set": () => {
// Inhibit setting of hyphenators
return true;
}
});
(() => {
if (hev && hev.polyfill) {
hev.polyfill();
}
})();
} else {
(() => {
if (hev && hev.tearDown) {
hev.tearDown();
}
w.Hyphenopoly = null;
})();
}
(() => {
if (H.cft) {
store.setItem(scriptName, JSON.stringify(
{
"langs": [...H.cf.langs.entries()],
"pf": H.cf.pf
}
));
}
})();
});
H.config = (c) => {
/**
* Sets default properties for an Object
* @param {object} obj - The object to set defaults to
* @param {object} defaults - The defaults to set
* @returns {object}
*/
const setDefaults = (obj, defaults) => {
if (obj) {
o.entries(defaults).forEach(([k, v]) => {
// eslint-disable-next-line security/detect-object-injection
obj[k] = obj[k] || v;
});
return obj;
}
return defaults;
};
H.cft = Boolean(c.cacheFeatureTests);
if (H.cft && store.getItem(scriptName)) {
H.cf = JSON.parse(store.getItem(scriptName));
H.cf.langs = mp(H.cf.langs);
} else {
H.cf = {
"langs": mp(),
"pf": false
};
}
const maindir = thisScript.slice(0, (thisScript.lastIndexOf("/") + 1));
const patterndir = maindir + "patterns/";
H.paths = setDefaults(c.paths, {
maindir,
patterndir
});
H.s = setDefaults(c.setup, {
"CORScredentials": "include",
"hide": "all",
"selectors": {".hyphenate": {}},
"timeout": 1000
});
// Change mode string to mode int
H.s.hide = ["all", "element", "text"].indexOf(H.s.hide);
if (c.handleEvent) {
H.hev = c.handleEvent;
}
const fallbacks = mp(o.entries(c.fallbacks || {}));
H.lrq = mp();
o.entries(c.require).forEach(([lang, wo]) => {
H.lrq.set(lang.toLowerCase(), {
"fn": fallbacks.get(lang) || lang,
wo
});
});
main();
};
})(window, document, Hyphenopoly, Object);
-8
View File
@@ -1,8 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
-39
View File
@@ -1,39 +0,0 @@
CC0 1.0 Universal
==================
Statement of Purpose
---------------------
The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights.
--------------------------------
A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
2. Waiver.
-----------
To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback.
----------------------------
Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
--------------------------------
a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
-93
View File
@@ -1,93 +0,0 @@
Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
# electron-quick-start
**Clone and run for a quick way to see Electron in action.**
This is a minimal Electron application based on the [Quick Start Guide](https://electronjs.org/docs/latest/tutorial/quick-start) within the Electron documentation.
A basic Electron application needs just these files:
- `package.json` - Points to the app's main file and lists its details and dependencies.
- `main.js` - Starts the app and creates a browser window to render HTML. This is the app's **main process**.
- `index.html` - A web page to render. This is the app's **renderer process**.
- `preload.js` - A content script that runs before the renderer process loads.
You can learn more about each of these components in depth within the [Tutorial](https://electronjs.org/docs/latest/tutorial/tutorial-prerequisites).
## To Use
To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:
```bash
# Clone this repository
git clone https://github.com/electron/electron-quick-start
# Go into the repository
cd electron-quick-start
# Install dependencies
npm install
# Run the app
npm start
```
Note: If you're using Linux Bash for Windows, [see this guide](https://www.howtogeek.com/261575/how-to-run-graphical-linux-desktop-applications-from-windows-10s-bash-shell/) or use `node` from the command prompt.
## Resources for Learning Electron
- [electronjs.org/docs](https://electronjs.org/docs) - all of Electron's documentation
- [Electron Fiddle](https://electronjs.org/fiddle) - Electron Fiddle, an app to test small Electron experiments
## License
[CC0 1.0 (Public Domain)](LICENSE.md)
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

-16
View File
@@ -1,16 +0,0 @@
# title: Das Herrenhaus
# author: Georg Tomitsch
VAR location = 0
INCLUDE Stats.ink
-> Herrenhaus
=== Herrenhaus ===
~ location = Herrenhaus
Du stehst vor einem Herrenhaus.
* Aktion 1 # 1
* Aktion 2 # 2
* Aktion 3 # 3
- -> END
-79
View File
@@ -1,79 +0,0 @@
// Quest tracking functions as described by Jon Ingold in his GDC 2017 talk.
// Every state implies the states before.
=== function state_reached(state) ===
~ return
// The state is never rolled back. If a lower or the same state is moved to nothing happens.
=== function move_to_state(state) ===
~ return
// Often the state is checked as a range (excluding first and last state)
=== function state_between(first_state, last_state) ===
~ return
// Implementation of ChoiceScript's Fairmath system
// Adjust the variable by adding amount percent of the current value
=== function set(ref variable, amount) ===
~ variable = MIN(100, variable + variable * amount / 100)
~ return variable
// Implementation of relative opposed pair stats
=== function opposed(positive, negative) ===
~ return positive / negative * 100 // TODO Check if this calculation is correct
// Inkle's default number writing function
=== function print_num(x)
{
- x >= 1000:
{print_num(x / 1000)} thousand { x mod 1000 > 0:{print_num(x mod 1000)}}
- x >= 100:
{print_num(x / 100)} hundred { x mod 100 > 0:and {print_num(x mod 100)}}
- x == 0:
zero
- else:
{ x >= 20:
{ x / 10:
- 2: twenty
- 3: thirty
- 4: forty
- 5: fifty
- 6: sixty
- 7: seventy
- 8: eighty
- 9: ninety
}
{ x mod 10 > 0:
<>-<>
}
}
{ x < 10 || x > 20:
{ x mod 10:
- 1: one
- 2: two
- 3: three
- 4: four
- 5: five
- 6: six
- 7: seven
- 8: eight
- 9: nine
}
- else:
{ x:
- 10: ten
- 11: eleven
- 12: twelve
- 13: thirteen
- 14: fourteen
- 15: fifteen
- 16: sixteen
- 17: seventeen
- 18: eighteen
- 19: nineteen
}
}
}
-566
View File
@@ -1,566 +0,0 @@
(function(storyContent) {
// Create ink story from the content using inkjs
var story = new inkjs.Story(storyContent);
var savePoint = "";
let fade_in = true;
// Global tags - those at the top of the ink file
// We support:
// # theme: dark
// # author: Your Name
var globalTags = story.globalTags;
if( globalTags ) {
for(var i=0; i<story.globalTags.length; i++) {
var globalTag = story.globalTags[i];
var splitTag = splitPropertyTag(globalTag);
// THEME: dark
if( splitTag && splitTag.property == "title" ) {
var title = document.querySelector('.title');
title.innerHTML = splitTag.val;
}
// author: Your Name
else if( splitTag && splitTag.property == "author" ) {
var byline = document.querySelector('.byline');
byline.innerHTML = "by "+splitTag.val;
}
}
}
var storyContainer = document.querySelector('#story');
var choiceContainer = document.querySelector('#choices');
var outerScrollContainer = document.querySelector('#book');
function updateParagraphPreview(paragraph_data, indent_width, preview_width) {
var old_preview = document.getElementById("preview");
var preview = document.createElement("div");
preview.id = "preview";
preview.style.width = preview_width + 'px';
if(old_preview) {
old_preview.replaceWith(preview);
// preview = old_preview;
} else {
document.body.appendChild(preview);
}
// p = typesetParagraph(paragraph_data, indent_width);
// preview.appendChild(p);
}
let timeoutQueue = [];
function scheduleTimeout(func, delay, ...args) {
const timeoutObject = {
execute: () => func(...args),
timeoutId: null
};
timeoutObject.timeoutId = setTimeout(() => {
timeoutObject.execute();
timeoutQueue = timeoutQueue.filter(t => t !== timeoutObject);
}, delay);
timeoutQueue.push(timeoutObject);
return timeoutObject.timeoutId;
}
function fastForward() {
// Sort the queue based on timeoutId (assuming that smaller ids are scheduled earlier)
timeoutQueue.sort((a, b) => a.timeoutId - b.timeoutId);
// Clear and execute all timeouts
timeoutQueue.forEach(timeoutObject => {
clearTimeout(timeoutObject.timeoutId);
timeoutObject.execute();
});
timeoutQueue = [];
document.getElementById("page_right").scrollTo({top: document.getElementById("page_right").scrollHeight, behavior: 'smooth'});
}
// var numberOfPreviewLines = 0;
function typesetParagraph(paragraph_data, indent_width, delay = 0) {
console.log("Typesetting Paragraph with: ", paragraph_data, indent_width);
var left = indent_width;
var p = document.createElement("p");
p.style.position = 'relative';
var line_height = parseFloat(window.getComputedStyle(document.querySelector("#ruler")).lineHeight);
// numberOfPreviewLines += paragraph_data.breaks.length - 1;
// console.log("Calculated line height:", line_height);
p.style.height = line_height * (paragraph_data.breaks.length - 1) + 'px';
p.style.marginBlockEnd = 0;
for(let i = 1; i < paragraph_data.breaks.length; i++) {
if(i > 1)
left = 0;
for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) {
// console.log("i =",i,"j =",j,"from =",paragraph_data.breaks[i-1].position,"to =",paragraph_data.breaks[i].position,"node_width =", paragraph_data.nodes[j].width, "left =", left, "type =", paragraph_data.nodes[j].type, "value =", paragraph_data.nodes[j].value);
if(paragraph_data.nodes[j].type === 'box' && paragraph_data.nodes[j].value !== '' && j < paragraph_data.breaks[i].position) {
if(j > paragraph_data.breaks[i-1].position + 1 && paragraph_data.nodes[j-1].type === 'penalty' && p.lastChild) {
p.lastChild.textContent += paragraph_data.nodes[j].value;
left += paragraph_data.nodes[j].width;
} else {
let word = document.createElement("span");
word.style.position = 'absolute';
word.classList.add("fade-in");
word.style.top = line_height * (i - 1) + 'px';
word.style.left = left + 'px';
word.innerHTML = paragraph_data.nodes[j].value;
insertAfter(delay, p, word);
delay += 100.0;
// p.appendChild(word);
if(j > 0)
left += paragraph_data.nodes[j].width;
else
left += paragraph_data.nodes[j].width - indent_width;
}
} else if(j > paragraph_data.breaks[i-1].position && paragraph_data.nodes[j].type === 'glue' && paragraph_data.nodes[j].width !== 0 && j <= paragraph_data.breaks[i].position) {
// Insert space character
if(paragraph_data.breaks[i].ratio > 0) {
left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].stretch;
} else {
left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].shrink;
}
} else if(paragraph_data.nodes[j].type === 'penalty' && paragraph_data.nodes[j].penalty === 100 && j === paragraph_data.breaks[i].position) {
let word = document.createElement("span");
word.style.position = 'absolute';
word.style.top = line_height * (i - 1) + 'px';
word.style.left = left + 'px';
word.innerHTML = "-";
insertAfter(delay, p, word);
delay += 100;
// p.appendChild(word);
// left += paragraph_data.nodes[j].width;
}
}
};
return [p, delay];
}
function measureText(str) {
if (str === ' ') {
str = '\u00A0';
}
ruler.textContent = str;
return ruler.getClientRects()[0].width;
}
function updateBookDimensions() {
const vw = window.innerWidth;
const vh = window.innerHeight;
const viewportAspectRatio = vw / vh;
const imageAspectRatio = 2727 / 1691;
let bookWidth, bookHeight;
if (viewportAspectRatio > imageAspectRatio) {
bookWidth = vh * imageAspectRatio;
bookHeight = vh;
} else {
bookWidth = vw;
bookHeight = vw / imageAspectRatio;
}
document.documentElement.style.setProperty('--book-width', `${bookWidth}px`);
document.documentElement.style.setProperty('--book-height', `${bookHeight}px`);
// Setting a CSS variable that will be either vw or vh depending on the viewport aspect ratio
document.documentElement.style.setProperty(
"--viewport-dimension",
viewportAspectRatio > imageAspectRatio ? 'vw' : 'vh'
);
document.documentElement.style.setProperty('--viewport-aspect-ratio', viewportAspectRatio);
let story = document.getElementById("story");
let paddingTop = window.getComputedStyle(story).paddingTop;
let paddingBottom = window.getComputedStyle(story).paddingBottom;
document.documentElement.style.setProperty('--story-line-height', (story.clientHeight - paddingTop - paddingBottom) / 28)
}
// Update the aspect ratio when the page loads
updateBookDimensions();
// Update the aspect ratio whenever the window is resized
window.addEventListener('resize', updateBookDimensions);
window.addEventListener('keydown', (event) => {
if (event.code === 'Space') {
fade_in = false;
fastForward();
}
});
// page features setup
var hasSave = loadSavePoint();
setupButtons(hasSave);
// Set initial save point
savePoint = story.state.toJson();
// Kick off the start of the story!
continueStory(true);
// Main story processing function. Each time this is called it generates
// all the next content up as far as the next set of choices.
function continueStory(firstTime) {
var paragraphIndex = 0;
var delay = 0.0;
// Don't over-scroll past new content
var previousBottomEdge = firstTime ? 0 : contentBottomEdgeY();
var fade_in = true
// Generate story text - loop through available content
while(story.canContinue) {
// Get ink to generate the next paragraph
var paragraphText = story.Continue();
var tags = story.currentTags;
// Any special tags included with this line
var customClasses = [];
for(var i=0; i<tags.length; i++) {
var tag = tags[i];
// Detect tags of the form "X: Y". Currently used for IMAGE and CLASS but could be
// customised to be used for other things too.
var splitTag = splitPropertyTag(tag);
// AUDIO: src
if( splitTag && splitTag.property == "AUDIO" ) {
if('audio' in this) {
this.audio.pause();
this.audio.removeAttribute('src');
this.audio.load();
}
this.audio = new Audio(splitTag.val);
this.audio.play();
}
// AUDIOLOOP: src
else if( splitTag && splitTag.property == "AUDIOLOOP" ) {
if('audioLoop' in this) {
this.audioLoop.pause();
this.audioLoop.removeAttribute('src');
this.audioLoop.load();
}
this.audioLoop = new Audio(splitTag.val);
this.audioLoop.play();
this.audioLoop.loop = true;
}
// IMAGE: src
if( splitTag && splitTag.property == "IMAGE" ) {
var imageElement = document.createElement('img');
imageElement.src = splitTag.val;
storyContainer.appendChild(imageElement);
showAfter(delay, imageElement);
delay += 200.0;
}
// LINK: url
else if( splitTag && splitTag.property == "LINK" ) {
window.location.href = splitTag.val;
}
// LINKOPEN: url
else if( splitTag && splitTag.property == "LINKOPEN" ) {
window.open(splitTag.val);
}
// BACKGROUND: src
else if( splitTag && splitTag.property == "BACKGROUND" ) {
outerScrollContainer.style.backgroundImage = 'url('+splitTag.val+')';
}
// CLASS: className
else if( splitTag && splitTag.property == "CLASS" ) {
customClasses.push(splitTag.val);
}
// CLEAR - removes all existing content.
// RESTART - clears everything and restarts the story from the beginning
else if( tag == "CLEAR" || tag == "RESTART" ) {
removeAll("p");
removeAll("img");
// Comment out this line if you want to leave the header visible when clearing
// setVisible(".header", false);
if( tag == "RESTART" ) {
restart();
return;
}
}
}
// Create paragraph element (initially hidden)
(function(text) {
if(text.trim().length === 0)
return;
console.log("Hyphenating:", text);
let hyphenator_promise = Hyphenopoly.hyphenators["en-us"].then((hyphenator_en) => {
var measure = parseFloat(window.getComputedStyle(document.getElementById("story")).width);
var indentWidth = parseFloat(window.getComputedStyle(document.querySelector("#indent")).textIndent);
var previewWidth = measure;
var preview_data = kap(hyphenator_en(text, '.hyphenatePipe'), measureText, 'align-justify', measure, true, indentWidth);
return { preview_data, indentWidth, previewWidth};
});
hyphenator_promise.then(({ preview_data, indentWidth, previewWidth }) => {
// updateParagraphPreview(preview_data, indentWidth, previewWidth);
var p, d;
[p, d] = typesetParagraph(preview_data, indentWidth, delay);
delay = d;
// Add any custom classes derived from ink tags
for(var i=0; i<customClasses.length; i++)
p.classList.add(customClasses[i]);
storyContainer.appendChild(p);
p.scrollIntoView({ behavior: 'smooth'});
});
})(paragraphText);
// var paragraphElement = document.createElement('p');
// var words = paragraphText.split(" ");
// words.forEach(word => {
// var wordElement = document.createElement('span');
// Hyphenopoly.hyphenators["en-us"].then((hyphenator_en) => {
// wordElement.innerHTML = hyphenator_en(word);
// });
// // showAfter(delay, wordElement);
// insertAfter(delay, paragraphElement, wordElement, fade_in);
// insertAfter(delay, paragraphElement, document.createTextNode(" "), false);
// delay +=100.0;
// // paragraphElement.appendChild(wordElement);
// // paragraphElement.appendChild(document.createTextNode(" "));
// });
// // paragraphElement.innerHTML = paragraphText;
// storyContainer.appendChild(paragraphElement);
// Fade in paragraph after a short delay
// showAfter(delay, paragraphElement);
// delay += 200.0;
}
// Create HTML choices from ink choices
story.currentChoices.forEach(function(choice) {
// Create paragraph with anchor element
var choiceParagraphElement = document.createElement('p');
choiceParagraphElement.classList.add("choice");
choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
// choiceContainer.appendChild(choiceParagraphElement);
insertAfter(delay, choiceContainer, choiceParagraphElement, fade_in);
// Fade choice in after a short delay
// showAfter(delay, choiceParagraphElement);
delay += 200.0;
// Click on choice
var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
choiceAnchorEl.addEventListener("click", function(event) {
// Don't follow <a> link
event.preventDefault();
// Remove all existing choices
removeAll(".choice", true);
// Tell the story where to go next
story.ChooseChoiceIndex(choice.index);
// This is where the save button will save from
savePoint = story.state.toJson();
// Aaand loop
continueStory();
});
});
// Extend height to fit
// We do this manually so that removing elements and creating new ones doesn't
// cause the height (and therefore scroll) to jump backwards temporarily.
// storyContainer.style.height = contentBottomEdgeY()+"px";
if( !firstTime )
scrollDown(previousBottomEdge);
}
function restart() {
story.ResetState();
setVisible(".header", true);
removeAll(".choice", true);
// set save point to here
savePoint = story.state.toJson();
continueStory(true);
outerScrollContainer.scrollTo({ top: 0, left: 0, behavior: 'smooth'});
}
// -----------------------------------
// Various Helper functions
// -----------------------------------
// Fades in an element after a specified delay
function showAfter(delay, el) {
el.classList.add("hide");
setTimeout(function() {
setTimeout(function() { el.classList.remove("hide") }, delay);
});
}
function insertAfter(delay, target, el, fade_in = true) {
if(fade_in) {
el.classList.add("fade-in");
scheduleTimeout(function() {
target.appendChild(el);
el.scrollIntoView({ behavior: 'smooth'});
}, delay);
} else {
scheduleTimeout(function() {
target.appendChild(el);
}, delay);
}
}
// Scrolls the page down, but no further than the bottom edge of what you could
// see previously, so it doesn't go too far.
function scrollDown(previousBottomEdge) {
return; // TODO: Fix or remove function
// Line up top of screen with the bottom of where the previous content ended
var target = previousBottomEdge;
// Can't go further than the very bottom of the page
var limit = outerScrollContainer.scrollHeight - outerScrollContainer.clientHeight;
if( target > limit ) target = limit;
var start = outerScrollContainer.scrollTop;
var dist = target - start;
var duration = 300 + 300*dist/100;
var startTime = null;
function step(time) {
if( startTime == null ) startTime = time;
var t = (time-startTime) / duration;
var lerp = 3*t*t - 2*t*t*t; // ease in/out
outerScrollContainer.scrollTo({ left: 0, top: (1.0-lerp)*start + lerp*target, behavior: 'smooth'});
if( t < 1 ) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
// The Y coordinate of the bottom end of all the story content, used
// for growing the container, and deciding how far to scroll.
function contentBottomEdgeY() {
var bottomElement = storyContainer.lastElementChild;
return bottomElement ? bottomElement.offsetTop + bottomElement.offsetHeight : 0;
}
// Remove all elements that match the given selector. Used for removing choices after
// you've picked one, as well as for the CLEAR and RESTART tags.
function removeAll(selector, choices = false)
{
if(choices)
var allElements = choiceContainer.querySelectorAll(selector);
else
var allElements = storyContainer.querySelectorAll(selector);
for(var i=0; i<allElements.length; i++) {
var el = allElements[i];
el.parentNode.removeChild(el);
}
}
// Used for hiding and showing the header when you CLEAR or RESTART the story respectively.
function setVisible(selector, visible)
{
var allElements = storyContainer.querySelectorAll(selector);
for(var i=0; i<allElements.length; i++) {
var el = allElements[i];
if( !visible )
el.classList.add("invisible");
else
el.classList.remove("invisible");
}
}
// Helper for parsing out tags of the form:
// # PROPERTY: value
// e.g. IMAGE: source path
function splitPropertyTag(tag) {
var propertySplitIdx = tag.indexOf(":");
if( propertySplitIdx != null ) {
var property = tag.substr(0, propertySplitIdx).trim();
var val = tag.substr(propertySplitIdx+1).trim();
return {
property: property,
val: val
};
}
return null;
}
// Loads save state if exists in the browser memory
function loadSavePoint() {
try {
let savedState = window.localStorage.getItem('save-state');
if (savedState) {
story.state.LoadJson(savedState);
return true;
}
} catch (e) {
console.debug("Couldn't load save state");
}
return false;
}
// Used to hook up the functionality for global functionality buttons
function setupButtons(hasSave) {
let rewindEl = document.getElementById("rewind");
if (rewindEl) rewindEl.addEventListener("click", function(event) {
removeAll("p");
removeAll("img");
setVisible(".header", false);
restart();
});
let saveEl = document.getElementById("save");
if (saveEl) saveEl.addEventListener("click", function(event) {
try {
window.localStorage.setItem('save-state', savePoint);
document.getElementById("reload").removeAttribute("disabled");
} catch (e) {
console.warn("Couldn't save state");
}
});
let reloadEl = document.getElementById("reload");
if (!hasSave) {
reloadEl.setAttribute("disabled", "disabled");
}
reloadEl.addEventListener("click", function(event) {
if (reloadEl.getAttribute("disabled"))
return;
removeAll("p");
removeAll("img");
removeAll(".choice", true);
try {
let savedState = window.localStorage.getItem('save-state');
if (savedState) story.state.LoadJson(savedState);
} catch (e) {
console.debug("Couldn't load save state");
}
continueStory(true);
});
}
})(storyContent);
-1034
View File
File diff suppressed because it is too large Load Diff
-57
View File
@@ -1,57 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'blob'; style-src 'self' 'unsafe-inline'" -->
<title>Ink.js Book Runtime</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p id="versions">We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.</p>
<div id="book">
<div id="page_left">
<div class="header">
<h2 class="byline l10n-by">by </h2>
<h1 class="title"></h1>
<h3 class="subtitle"></h3>
<div class="separator"><double></double></div>
</div>
<div id="controls" class="buttons">
<a class="l10n-speech" id="speech" title="Toggle text to speech" disabled="disabled">speech</a>
<span><a id="speed_reset"><span class="l10n-speed">speed<sup>*</sup></span></a><input type="range" min="0" max="100" value="50" id="speed" name="speed" /></span>
<a class="l10n-restart" id="rewind" title="Restart story from beginning" disabled="disabled">restart</a>
<a class="l10n-save" id="save" title="Save progress">save</a>
<a class="l10n-load" id="reload" title="Reload from save point" disabled="disabled">load</a>
</div>
<div id="choices" class="container">
</div>
<div class="l10n-remark" id="remark"><i><sup>*</sup>click on page or press spacebar to fast forward text animation</i></div>
</div>
<div id="page_right">
<div id="story" class="container">
</div>
</div>
</div>
<div id="ruler"></div>
<div class="l10n-prompt" id="indent">What do you want to do next?</div>
<div id="lighting" />
<!-- You can also require other files to run in this process -->
<script src="smartypants.js"></script>
<script src="linked-list.js"></script>
<script src="linebreak.js"></script>
<script src="knuth-and-plass.js"></script>
<script src="ink-full.js"></script>
<!-- <script src="TheIntercept.js"></script> -->
<script src="Hyphenopoly_Loader.js"></script>
<script>
var locale = "de";
</script>
<script src="game.js"></script>
<script src="./renderer.js"></script>
</body>
</html>
-2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-2
View File
File diff suppressed because one or more lines are too long
-10
View File
@@ -1,10 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"CodeGPT.apiKey": "Ollama"
}
}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("path"),t=require("fs");function r(e){if(e&&e.__esModule)return e;var t=Object.create(null);return e&&Object.keys(e).forEach((function(r){if("default"!==r){var n=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,n.get?n:{enumerable:!0,get:function(){return e[r]}})}})),t.default=e,Object.freeze(t)}var n=r(e),o=r(t);function i(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function a(e,t,r){return t&&i(e.prototype,t),r&&i(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}var u=a((function e(t){var r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.rootPath=t,this.ResolveInkFilename=function(e){if(void 0!==r.rootPath&&""!==r.rootPath)return n.join(r.rootPath,e);var t=process.cwd();return n.join(t,e)},this.LoadInkFileContents=function(e){return o.readFileSync(e,"utf-8")}}));exports.PosixFileHandler=u;
-56
View File
@@ -1,56 +0,0 @@
function kap(text, measureText, measure, hyphenation) {
console.log("Typesetting hyphenated text:", text, measure);
if (!hyphenation) {
text = text.replace(/\|/g, '');
}
let hyphenWidth = measureText('-');
let spaceWidth = measureText('\u00A0');
let nodes = [];
text.split(/([.,:;!?] |\s|\||<.*?>)/u).forEach(function (fragment) {
let fragmentWidth = measureText(fragment);
if (fragment === ' ') {
let stretch = (spaceWidth * 3) / 6;
let shrink = (spaceWidth * 3) / 9;
nodes.push(linebreak.glue(spaceWidth, stretch, shrink));
} else if (fragment === '|') {
// nodes.push(linebreak.penalty(hyphenWidth, 100, 1));
nodes.push(linebreak.penalty(hyphenWidth * 0.25, 100, 1));
} else if (fragment.match(/(<.*?>)/u)) {
nodes.push(linebreak.tag(fragmentWidth, fragment));
} else if (fragment.match(/[.,:;!?] /u)) {
let punctuation = fragment.match(/([.,:;!?])( )/u);
let punctuationSymbolWidth = measureText(punctuation[1]) * 0.25;
let punctuationWidth = measureText(punctuation[1]) * 0.75 + spaceWidth;
nodes.push(linebreak.box(punctuationSymbolWidth, punctuation[1]));
let stretch = (punctuationWidth * 3) / 6;
let shrink = (punctuationWidth * 3) / 9;
nodes.push(linebreak.glue(punctuationWidth, stretch, shrink));
} else if (fragment.match(/(\s+)/u)) {
} else {
nodes.push(linebreak.box(fragmentWidth, fragment));
}
});
nodes.push(linebreak.glue(0, linebreak.infinity, 0));
nodes.push(linebreak.penalty(0, -linebreak.infinity, 1));
let demerits = {
line: 10,
flagged: 100,
fitness: 3000
};
let breaks = linebreak(nodes, measure, { tolerance: 3, demerits });
if (!breaks.length) {
breaks = linebreak(nodes, measure, { tolerance: 10, demerits });
}
return { nodes, breaks };
}
-334
View File
@@ -1,334 +0,0 @@
var linebreak = function (nodes, lines, settings = {
demerits: {
line: 10,
flagged: 100,
fitness: 3000
},
tolerance: 2
}) {
const options = settings;
activeNodes = new LinkedList(),
sum = {
width: 0,
stretch: 0,
shrink: 0
},
lineLengths = lines,
breaks = [],
tmp = {
data: {
demerits: Infinity
}
};
function breakpoint(position, demerits, ratio, line, fitnessClass, totals, previous) {
return {
position: position,
demerits: demerits,
ratio: ratio,
line: line,
fitnessClass: fitnessClass,
totals: totals || {
width: 0,
stretch: 0,
shrink: 0
},
previous: previous
};
}
function computeCost(start, end, active, currentLine) {
var width = sum.width - active.totals.width,
stretch = 0,
shrink = 0,
// If the current line index is within the list of linelengths, use it, otherwise use
// the last line length of the list.
lineLength = currentLine < lineLengths.length ? lineLengths[currentLine - 1] : lineLengths[lineLengths.length - 1];
if (nodes[end].type === 'penalty') {
width += nodes[end].width;
}
if (width < lineLength) {
// Calculate the stretch ratio
stretch = sum.stretch - active.totals.stretch;
if (stretch > 0) {
return (lineLength - width) / stretch;
} else {
return linebreak.infinity;
}
} else if (width > lineLength) {
// Calculate the shrink ratio
shrink = sum.shrink - active.totals.shrink;
if (shrink > 0) {
return (lineLength - width) / shrink;
} else {
return linebreak.infinity;
}
} else {
// perfect match
return 0;
}
}
// Add width, stretch and shrink values from the current
// break point up to the next box or forced penalty.
function computeSum(breakPointIndex) {
var result = {
width: sum.width,
stretch: sum.stretch,
shrink: sum.shrink
},
i = 0;
for (i = breakPointIndex; i < nodes.length; i += 1) {
if (nodes[i].type === 'glue') {
result.width += nodes[i].width;
result.stretch += nodes[i].stretch;
result.shrink += nodes[i].shrink;
} else if (nodes[i].type === 'box' || (nodes[i].type === 'penalty' && nodes[i].penalty === -linebreak.infinity && i > breakPointIndex)) {
break;
}
}
return result;
}
let graphNodes = [];
let graphEdges = [];
// The main loop of the algorithm
function mainLoop(node, index, nodes) {
var active = activeNodes.first,
next = null,
ratio = 0,
demerits = 0,
candidates = [],
badness,
currentLine = 0,
tmpSum,
currentClass = 0,
fitnessClass,
candidate,
newNode;
// The inner loop iterates through all the active nodes with line < currentLine and then
// breaks out to insert the new active node candidates before looking at the next active
// nodes for the next lines. The result of this is that the active node list is always
// sorted by line number.
while (active !== null) {
candidates = [{
demerits: Infinity
}, {
demerits: Infinity
}, {
demerits: Infinity
}, {
demerits: Infinity
}];
// Iterate through the linked list of active nodes to find new potential active nodes
// and deactivate current active nodes.
while (active !== null) {
next = active.next;
currentLine = active.data.line + 1;
ratio = computeCost(active.data.position, index, active.data, currentLine);
// Deactive nodes when the distance between the current active node and the
// current node becomes too large (i.e. it exceeds the stretch limit and the stretch
// ratio becomes negative) or when the current node is a forced break (i.e. the end
// of the paragraph when we want to remove all active nodes, but possibly have a final
// candidate active node---if the paragraph can be set using the given tolerance value.)
if (ratio < -1 || (node.type === 'penalty' && node.penalty === -linebreak.infinity)) {
activeNodes.remove(active);
}
// If the ratio is within the valid range of -1 <= ratio <= tolerance calculate the
// total demerits and record a candidate active node.
if (-1 <= ratio && ratio <= options.tolerance) {
badness = 100 * Math.pow(Math.abs(ratio), 3);
// Positive penalty
if (node.type === 'penalty' && node.penalty >= 0) {
demerits = Math.pow(options.demerits.line + badness, 2) + Math.pow(node.penalty, 2);
// Negative penalty but not a forced break
} else if (node.type === 'penalty' && node.penalty !== -linebreak.infinity) {
demerits = Math.pow(options.demerits.line + badness, 2) - Math.pow(node.penalty, 2);
// All other cases
} else {
demerits = Math.pow(options.demerits.line + badness, 2);
}
if (node.type === 'penalty' && nodes[active.data.position].type === 'penalty') {
demerits += options.demerits.flagged * node.flagged * nodes[active.data.position].flagged;
}
// Calculate the fitness class for this candidate active node.
if (ratio < -0.5) {
currentClass = 0;
} else if (ratio <= 0.5) {
currentClass = 1;
} else if (ratio <= 1) {
currentClass = 2;
} else {
currentClass = 3;
}
// Add a fitness penalty to the demerits if the fitness classes of two adjacent lines
// differ too much.
if (Math.abs(currentClass - active.data.fitnessClass) > 1) {
demerits += options.demerits.fitness;
}
// Add the total demerits of the active node to get the total demerits of this candidate node.
demerits += active.data.demerits;
// Only store the best candidate for each fitness class
if (demerits < candidates[currentClass].demerits) {
candidates[currentClass] = {
active: active,
demerits: demerits,
ratio: ratio
};
}
}
active = next;
// Stop iterating through active nodes to insert new candidate active nodes in the active list
// before moving on to the active nodes for the next line.
// TODO: The Knuth and Plass paper suggests a conditional for currentLine < j0. This means paragraphs
// with identical line lengths will not be sorted by line number. Find out if that is a desirable outcome.
// For now I left this out, as it only adds minimal overhead to the algorithm and keeping the active node
// list sorted has a higher priority.
if (active !== null && active.data.line >= currentLine) {
break;
}
}
tmpSum = computeSum(index);
for (fitnessClass = 0; fitnessClass < candidates.length; fitnessClass += 1) {
candidate = candidates[fitnessClass];
if (candidate.demerits < Infinity) {
newNode = new Node(breakpoint(index, candidate.demerits, candidate.ratio,
candidate.active.data.line + 1, fitnessClass, tmpSum, candidate.active));
graphNodes.push({
id: index
});
graphEdges.push({
from: index,
to: candidate.active.data.position,
label: candidate.ratio.toFixed(2)
});
if (active !== null) {
activeNodes.insertBefore(active, newNode);
} else {
activeNodes.push(newNode);
}
}
}
}
}
// Add an active node for the start of the paragraph.
activeNodes.push(new Node(breakpoint(0, 0, 0, 0, 0, undefined, null)));
graphNodes.push({
id: 0
});
nodes.forEach(function (node, index, nodes) {
if (node.type === 'box') {
sum.width += node.width;
} else if (node.type === 'glue') {
if (index > 0 && nodes[index - 1].type === 'box') {
mainLoop(node, index, nodes);
}
sum.width += node.width;
sum.stretch += node.stretch;
sum.shrink += node.shrink;
} else if (node.type === 'penalty' && node.penalty !== linebreak.infinity) {
mainLoop(node, index, nodes);
}
});
if (activeNodes.size !== 0) {
// Find the best active node (the one with the least total demerits.)
activeNodes.forEach(function (node) {
if (node.data.demerits < tmp.data.demerits) {
tmp = node;
}
});
graphNodes.forEach(function (n) {
let label = nodes[n.id].value;
if (nodes[n.id].type === 'glue') {
label = nodes[n.id - 1].value;
} else if (nodes[n.id].type === 'penalty') {
label = nodes[n.id - 1].value;
} else {
label = nodes[n.id].value;
}
n.label = label;
});
while (tmp !== null) {
breaks.push({
position: tmp.data.position,
ratio: tmp.data.ratio
});
tmp = tmp.data.previous;
}
return breaks.reverse();
} else {
console.warn('Overfull paragraph.');
}
return [];
};
linebreak.infinity = 10000;
linebreak.glue = function (width, stretch, shrink) {
return {
type: 'glue',
width: width,
stretch: stretch,
shrink: shrink
};
};
linebreak.box = function (width, value) {
return {
type: 'box',
width: width,
value: value
};
};
linebreak.tag = function (width, value) {
return {
type: 'tag',
width: width,
value: value
}
}
linebreak.penalty = function (width, penalty, flagged) {
return {
type: 'penalty',
width: width,
penalty: penalty,
flagged: flagged
};
};
-187
View File
@@ -1,187 +0,0 @@
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
this.listSize = 0;
}
get size() {
return this.listSize;
}
isLinked(node) {
return !((node && node.prev === null && node.next === null && this.tail !== node && this.head !== node) || this.isEmpty());
}
isEmpty() {
return this.listSize === 0;
}
get first() {
return this.head;
}
get last() {
return this.last;
}
toString() {
return this.toArray().toString();
}
toArray() {
var node = this.head,
result = [];
while (node !== null) {
result.push(node);
node = node.next;
}
return result;
}
// Note that modifying the list during
// iteration is not safe.
forEach(fun) {
var node = this.head;
while (node !== null) {
fun(node);
node = node.next;
}
}
contains(n) {
var node = this.head;
if (!this.isLinked(n)) {
return false;
}
while (node !== null) {
if (node === n) {
return true;
}
node = node.next;
}
return false;
}
at(i) {
var node = this.head, index = 0;
if (i >= this.listLength || i < 0) {
return null;
}
while (node !== null) {
if (i === index) {
return node;
}
node = node.next;
index += 1;
}
return null;
}
insertAfter(node, newNode) {
if (!this.isLinked(node)) {
return this;
}
newNode.prev = node;
newNode.next = node.next;
if (node.next === null) {
this.tail = newNode;
} else {
node.next.prev = newNode;
}
node.next = newNode;
this.listSize += 1;
return this;
}
insertBefore(node, newNode) {
if (!this.isLinked(node)) {
return this;
}
newNode.prev = node.prev;
newNode.next = node;
if (node.prev === null) {
this.head = newNode;
} else {
node.prev.next = newNode;
}
node.prev = newNode;
this.listSize += 1;
return this;
}
push(node) {
if (this.head === null) {
this.unshift(node);
} else {
this.insertAfter(this.tail, node);
}
return this;
}
unshift(node) {
if (this.head === null) {
this.head = node;
this.tail = node;
node.prev = null;
node.next = null;
this.listSize += 1;
} else {
this.insertBefore(this.head, node);
}
return this;
}
remove(node) {
if (!this.isLinked(node)) {
return this;
}
if (node.prev === null) {
this.head = node.next;
} else {
node.prev.next = node.next;
}
if (node.next === null) {
this.tail = node.prev;
} else {
node.next.prev = node.prev;
}
this.listSize -= 1;
return this;
}
pop() {
var node = this.tail;
this.tail.prev.next = null;
this.tail = this.tail.prev;
this.listSize -= 1;
node.prev = null;
node.next = null;
return node;
}
shift() {
var node = this.head;
this.head.next.prev = null;
this.head = this.head.next;
this.listSize -= 1;
node.prev = null;
node.next = null;
return node;
}
}
class Node {
constructor(data) {
this.prev = null;
this.next = null;
this.data = data;
}
toString() {
return this.data.toString();
}
}
-108
View File
@@ -1,108 +0,0 @@
// Modules to control application life and create native browser window
const { contextBridge, ipcMain, session, app, BrowserWindow } = require('electron')
const path = require('path')
const fs = require('fs');
const vm = require('vm');
require('./speech');
// const fetch = require('node-fetch');
// // Use a polyfill for fetch
// if (!globalThis.fetch) {
// globalThis.fetch = fetch;
// }
// const hyphenopolyScript = fs.readFileSync(require.resolve('./Hyphenopoly_Loader.js'), 'utf-8');
// vm.runInThisContext(hyphenopolyScript, { filename: 'Hyphenopoly_Loader.js' });
// Hyphenopoly.config({
// require: {
// 'en-us': 'FORCEHYPHENOPOLY',
// 'de': 'Silbentrennungsalgorithmus',
// },
// paths: {
// maindir: './',
// patterndir: './patterns/',
// },
// setup: {
// selectors: {
// '.hyphenate': {
// hyphen: '&shy;',
// },
// '.hyphenatePipe': {
// hyphen: '|',
// },
// },
// },
// });
// contextBridge.exposeInMainWorld('api', {
// hyphenateWord: async (word, selector = '.hyphenate') => {
// const hyphenator = await Hyphenopoly.hyphenators['en-us'];
// return hyphenator(word, selector);
// },
// });
const debug = true;
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
// fullscreen: true,
// frame: false,
// titleBarStyle: 'hidden',
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
// contentSecurityPolicy: "script-src 'self' 'unsafe-inline';",
preload: path.join(__dirname, 'preload.js')
}
})
if(!debug)
mainWindow.removeMenu();
// and load the index.html of the app.
mainWindow.loadFile('index.html')
mainWindow.maximize()
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'self\'; script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' blob:; style-src \'self\' \'unsafe-inline\'']
}
})
})
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
-1754
View File
File diff suppressed because it is too large Load Diff
-29
View File
@@ -1,29 +0,0 @@
{
"name": "electron-quick-start",
"version": "1.0.0",
"description": "A minimal Electron application",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"repository": "https://github.com/electron/electron-quick-start",
"keywords": [
"Electron",
"quick",
"start",
"tutorial",
"demo"
],
"author": "GitHub",
"license": "CC0-1.0",
"devDependencies": {
"electron": "^25.1.0"
},
"dependencies": {
"axios": "^1.4.0",
"crypto": "^1.0.1",
"fs": "^0.0.1-security",
"node-fetch": "^3.3.1",
"play-sound": "^1.1.5"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More