Add glossary hover presentation
This commit is contained in:
+11
-1
@@ -215,6 +215,15 @@ Markdown emphasis:
|
||||
***bold italic*** or ___bold italic___
|
||||
```
|
||||
|
||||
Right-page glossary notes:
|
||||
|
||||
```text
|
||||
The train stops at Eibenreith.
|
||||
#gloss[Eibenreith](A fictional alpine town in the Kaiserpunk setting.)
|
||||
```
|
||||
|
||||
The tag is scoped to the right-page paragraph/block it belongs to. The bracket value is the visible term; the parenthesized value is the note. The renderer marks every matching instance of that term in the same block. The tag is not displayed, not sent to TTS, and ignored by choices and command history. Escape literal Ink control characters in explanations as needed (`\|`, `\{`, `\}`).
|
||||
|
||||
Chapter:
|
||||
|
||||
```text
|
||||
@@ -288,7 +297,7 @@ Choice tags:
|
||||
# action[examine]
|
||||
```
|
||||
|
||||
`#letter[x]`, `# letter[x]`, or `#key:x` reserves keyboard letter `X` for that choice. Explicit letters are assigned first; remaining visible choices receive `A` through `Z` in screen order, skipping reserved letters. The current UI supports up to 26 visible choices and renders them in one list. `#action[name]` or `#action:name` is stored as the choice category and reserved for later template routing.
|
||||
`#letter[x]`, `# letter[x]`, or `#key:x` reserves keyboard letter `X` for that choice. Explicit letters are assigned first; remaining visible choices receive `1` through `0`, then `A` through `Z` in screen order, skipping reserved letters. The current UI supports up to 36 visible choices and renders them in one list. `#action[name]` or `#action:name` is stored as the choice category and reserved for later template routing.
|
||||
|
||||
Future choice-template metadata should keep the same bracket tag syntax if implemented:
|
||||
|
||||
@@ -459,6 +468,7 @@ Longer-term goal:
|
||||
- [x] Added chapter heading and dropcap markup.
|
||||
- [x] Added section/textblock markup.
|
||||
- [x] Added Markdown emphasis parsing.
|
||||
- [x] Added right-page `#gloss[term](note)` annotations with clean TTS/plain-text handling.
|
||||
- [x] Added image markup parsing, line-snapped rendering, and history/save restoration.
|
||||
- [x] Added sound effect markup and playback.
|
||||
- [x] Added music markup, playback modes, loop/once, and lead-in.
|
||||
|
||||
@@ -31,6 +31,17 @@ Bare flags are accepted as tags with no value:
|
||||
#optional
|
||||
```
|
||||
|
||||
## Right-Page Glossary Notes
|
||||
|
||||
Glossary notes are story tags scoped to the paragraph/block they belong to. They affect only the right-page story rendering, never choice text or command history.
|
||||
|
||||
```ink
|
||||
The conductor points toward Eibenreith.
|
||||
#gloss[Eibenreith](A fictional alpine town in the Kaiserpunk setting.)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Choice Metadata
|
||||
|
||||
Choice tags are placed on the Ink choice they belong to:
|
||||
|
||||
@@ -67,7 +67,7 @@ Major modules:
|
||||
|
||||
- `module-registry.js`, `base-module.js`, `loader.js`: module lifecycle, dependency graph, progress overlay, state reporting.
|
||||
- `text-processor-module.js`, `paragraph-layout-module.js`, `layout-renderer-module.js`: SmartyPants, language-aware hyphenation, Knuth-Plass line breaking, DOM rendering.
|
||||
- `markup-parser-module.js`: story markup fallback for chapters, sections, Markdown emphasis, images, SFX, and music.
|
||||
- `markup-parser-module.js`: story markup fallback for chapters, sections, Markdown emphasis, right-page glossary notes, images, SFX, and music.
|
||||
- `sentence-queue-module.js`, `playback-coordinator-module.js`, `animation-queue-module.js`: sentence preparation, synchronized playback, timing, fast-forward.
|
||||
- `tts-factory-module.js` plus provider modules: TTS provider selection, voice settings, speed mapping, caching, and playback.
|
||||
- `audio-manager-module.js`: master, speech, music, and sound effect volume, music playback, sound effects, and music ducking.
|
||||
@@ -88,6 +88,15 @@ Inline Markdown emphasis:
|
||||
***bold italic*** or ___bold italic___
|
||||
```
|
||||
|
||||
Right-page glossary notes:
|
||||
|
||||
```text
|
||||
The train stops at Eibenreith.
|
||||
#gloss[Eibenreith](A fictional alpine town in the Kaiserpunk setting.)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Canonical block/media/control tags use Ink-style `#` syntax. In Ink these are real Ink tags. In YAML and Zork 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:
|
||||
@@ -180,7 +189,7 @@ The renderer is designed to behave like a scaled static book page. The page keep
|
||||
Text processing order:
|
||||
|
||||
1. Parse story markup and remove non-display media markers.
|
||||
2. Apply Markdown emphasis spans.
|
||||
2. Apply Markdown emphasis spans and right-page glossary annotations.
|
||||
3. Run SmartyPants for typographic punctuation.
|
||||
4. Apply Hyphenopoly for the selected language.
|
||||
5. Calculate line breaks with the Knuth-Plass algorithm.
|
||||
|
||||
+68
-1897
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
// eibenreith_02_bahnhof.ink
|
||||
// Kapitel: Der Bahnhof.
|
||||
// Enthält Stations-Erkundung, Gepäckwahl und erstes Manierenpuzzle.
|
||||
|
||||
=== railway_station ===
|
||||
|
||||
Die Station ist klein genug, dass der Zug kurz verlegen wirkt, als er dort hält. #chapter[Der Bahnhof] #image[muerzzuschlag.png](portrait)
|
||||
|
||||
Ein Gepäckträger mit einer zu großen Kappe eilt über den Bahnsteig. Eine Frau mit Korb tritt vor dem Dampf zurück wie vor einem Tier. Irgendwo jenseits des Stationsgebäudes stampft ein Kutschpferd im gefrorenen Schlamm. Das Schild gibt dem Ort einen Namen, den du im Fahrplan gesehen hast und an den du dich nicht mit Zuneigung erinnern wirst.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
=== station_platform_options ===
|
||||
|
||||
{not tut_optional_intro:
|
||||
#alert[Manche Wahlen sind Erkundungen. Sie öffnen Beobachtungen, Stimmungen oder Hinweise, ohne dich meist sofort auf einen unwiderruflichen Schritt festzulegen.]
|
||||
~ tut_optional_intro = true
|
||||
}
|
||||
|
||||
* [__Schaue__: Auf das Stationsschild.] #action:orientation #optional #key:l
|
||||
Der Ortsname auf dem Schild ist mit schwarzer Farbe auf hellem Grund gemalt, zweckmäßig, kaiserlich, ohne jede Rücksicht auf den Eindruck, den er auf Ankommende macht. Die Buchstaben sehen aus, als hätten sie nie vorgehabt, in einem Salon ausgesprochen zu werden.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
* [__Höre__: Auf den Bahnsteig.] #action:orientation #optional
|
||||
Unter dem Zischen der Lokomotive liegen kleinere Geräusche: ein Koffer, der auf Holz abgesetzt wird; das kurze Räuspern eines Beamten; Pferdehufe im gefrorenen Schlamm; eine Frau, die ein Gebet beginnt und beim zweiten Wort wieder verschluckt.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
* [__Untersuche__: Die Wartenden.] #action:orientation #optional
|
||||
Niemand starrt offen. Das wäre grob. Stattdessen entstehen kleine Leerstellen in den Bewegungen der Leute: ein Blick, der zu spät weiterwandert; ein Schritt, der seine Richtung ändert; ein Gespräch, das plötzlich nur noch aus Endungen besteht.
|
||||
|
||||
-> station_platform_options
|
||||
|
||||
* [__Überblicke__: Dein Gepäck.] #action:object
|
||||
-> station_baggage
|
||||
|
||||
=== station_baggage ===
|
||||
|
||||
Dein Gepäck wird in Etappen ausgeladen.
|
||||
|
||||
* [__Überblicke__: Eine disziplinierte amtliche Zusammenstellung.] #action:object
|
||||
~ baggage_style = "official"
|
||||
~ detective += 1
|
||||
Zuerst kommt ein nüchterner Reisekoffer mit vom Gebrauch stumpfen Messingecken, dann eine Aktenmappe, dann eine Hutschachtel, dann der schmale schwarze Kasten, dessen Inhalt sowohl einen Priester als auch einen Taschenspieler in Verlegenheit bringen würde, falls einer von beiden ihn ohne Phantasie durchsuchte.
|
||||
|
||||
* [__Überblicke__: Das Gepäck einer eleganten Dame.] #action:object
|
||||
~ baggage_style = "elegant"
|
||||
~ class_confidence += 1
|
||||
Zuerst kommt ein großer Koffer aus dunklem Leder, dann ein kleinerer für Wäsche, dann eine runde Hutschachtel, ein Reise-Necessaire und ein Ridikül, das du zu nahe bei der Hand behältst, als dass ein Gepäckträger seine Bedeutung missverstehen dürfte.
|
||||
|
||||
* [__Überblicke__: Das Gepäck einer Darstellerin.] #action:object
|
||||
~ baggage_style = "performer"
|
||||
~ medium_reputation += 1
|
||||
Zuerst kommt ein respektabler Koffer, dann eine Hutschachtel, dann ein Reisekasten mit Handschuhen, Schleiern, Bändern, Visitenkarten und kleinen Gegenständen, mit denen man ein Zimmer überreden kann, an Kräfte zu glauben, die längst anwesend sind.
|
||||
|
||||
* [__Überblicke__: Eine praktische Auswahl, die zu viel Vorbereitung verrät.] #action:object
|
||||
~ baggage_style = "practical"
|
||||
~ detective += 1
|
||||
Zuerst kommt ein abgenützter, an den Ecken verstärkter Koffer, dann eine Ledertasche mit Notizheften, Bleistiften, gefalteten Karten, Ersatzhandschuhen, einer Handlampe und genug kleinen Notwendigkeiten, um jeden zu beleidigen, der Frauen lieber dekorativ hat.
|
||||
|
||||
* [__Überblicke__: Ein übertriebener Haufen, der jede Tarnung erschwert.] #action:object
|
||||
~ baggage_style = "excessive"
|
||||
~ careless += 1
|
||||
Zuerst kommt ein Koffer, dann ein zweiter, dann eine Hutschachtel, dann eine Reisedecke, dann ein Toilettenkasten, dann der schmale schwarze Kasten, dann ein kleineres Paket, von dem du vergessen hattest, dass es das Packen überlebt hat. Am Ende sieht selbst Viktor einen Augenblick lang zahlenmäßig unterlegen aus.
|
||||
|
||||
-
|
||||
|
||||
Viktor überwacht die Umladung mit knapper Höflichkeit. Er trägt nicht wie ein Diener. Er weist an wie ein Mann, der vorgibt, nicht zu befehlen.
|
||||
|
||||
Die kleine Szene vor dem Waggon ist harmlos genug, um gefährlich zu sein. Ein Gepäckträger wartet mit geneigtem Kopf. Der Kutscher steht einige Schritte entfernt. Viktor ist nah genug, um dir beim Aussteigen die Hand zu reichen, aber nicht so nah, dass er es ohne dein stilles Einverständnis täte. Drei Männer, drei Stände, drei verschiedene Arten von Nützlichkeit.
|
||||
|
||||
Was hier geschieht, wird niemand in einem Bericht erwähnen. Gerade deshalb wird es behalten.
|
||||
|
||||
{
|
||||
- baggage_style == "practical":
|
||||
Weil dein Gepäck nach Vorbereitung aussieht, wirkt die nüchterne Anweisung an einen Gepäckträger weniger wie Anmaßung und mehr wie Gewohnheit.
|
||||
- baggage_style == "excessive":
|
||||
Weil dein Gepäck zu zahlreich ist, wird schon vor dem ersten Wort sichtbar, dass jemand in deiner Nähe arbeiten muss.
|
||||
- baggage_style == "performer":
|
||||
Der schmale schwarze Kasten zieht Viktors Blick eine Spur länger auf sich als die übrigen Stücke.
|
||||
- else:
|
||||
Das Gepäck gibt den Männern genug zu tun, um ihnen ihre Rollen zu erklären.
|
||||
}
|
||||
|
||||
{not tut_manners_intro:
|
||||
#alert[In Gesellschaft entscheidet oft nicht nur, was du tust, sondern wann und vor wem. Höflichkeit, Rang und Timing können ebenso viel verraten wie ein Geständnis.]
|
||||
~ tut_manners_intro = true
|
||||
}
|
||||
|
||||
{not tut_gated_intro:
|
||||
{
|
||||
- birth_class == "noble":
|
||||
#alert[Manche Möglichkeiten erkennst du nur, weil deine Herkunft, dein Glaube oder deine bisherigen Entscheidungen sie dir öffnen. Der hervorgehobene Hinweis nach dem Mittelpunkt zeigt, wodurch sie möglich wurde.]
|
||||
~ tut_gated_intro = true
|
||||
- birth_class == "working":
|
||||
#alert[Manche Möglichkeiten erkennst du nur, weil deine Herkunft, dein Glaube oder deine bisherigen Entscheidungen sie dir öffnen. Der hervorgehobene Hinweis nach dem Mittelpunkt zeigt, wodurch sie möglich wurde.]
|
||||
~ tut_gated_intro = true
|
||||
}
|
||||
}
|
||||
|
||||
* {birth_class == "noble"} [__Warte__ · **Adel**: Bis Viktor seine Hand anbietet.] #action:social #gated:noble #key:z
|
||||
#manners:excellent
|
||||
~ class_confidence += 2
|
||||
~ court_loyalty += 1
|
||||
Du wartest einen Atemzug, bis Viktor seine Hand anbietet, und nimmst sie dann, als wäre dies keine Hilfe, sondern die Ordnung der Welt.
|
||||
|
||||
Du gibst ihm nicht dein Gewicht. Nur deine Hand. Genau genug, dass er dienen darf, ohne Diener zu werden. Der Gepäckträger senkt den Blick ein wenig tiefer. Der Kutscher sieht, was er sehen muss: eine Dame, die ihren Rang nicht beweist, weil Beweise für Leute ohne Rang sind.
|
||||
|
||||
* [__Nicke__: Viktor zu und überlasse dem Gepäckträger das Gepäck.] #action:social
|
||||
#manners:good
|
||||
~ viktor_trust += 1
|
||||
Du nimmst Viktors angebotene Hand knapp und sicher, dankst ihm mit einem Nicken und lässt den Gepäckträger das Gepäck nehmen.
|
||||
|
||||
Es ist gutes Benehmen ohne Prunk: nicht zu vertraut gegenüber Viktor, nicht zu freundlich gegenüber dem Gepäckträger, nicht so kalt, dass es nach Unsicherheit riecht. Mittelstand könnte dies lernen. Adel könnte es billigen. Dienstboten würden erkennen, dass du ihre Arbeit nicht mit Herablassung verwechselst.
|
||||
|
||||
* [__Bitte den Gepäckträger__: „Zuerst den kleineren Kasten, wenn ich bitten darf.“] #action:social
|
||||
#route:detective
|
||||
#manners:practical
|
||||
~ detective += 1
|
||||
Du steigst selbst aus, bevor Viktor sich entscheiden kann, und bittest den Gepäckträger sachlich, zuerst den kleineren Kasten zu nehmen.
|
||||
|
||||
Das ist nicht ganz falsch, aber auch nicht ganz richtig. Viktor bemerkt die kleine Missachtung der erwarteten Form. Der Gepäckträger gehorcht erleichtert, weil klare Anweisungen leichter zu tragen sind als feine Ungewissheit. Der Kutscher ordnet dich eher der Nützlichkeit als dem Rang zu.
|
||||
|
||||
* [__Warte__: Einen Augenblick zu lange, bevor du Viktors Hand nimmst.] #action:social
|
||||
#route:lover
|
||||
#manners:provocative
|
||||
~ lover += 1
|
||||
~ viktor_suspicion += 1
|
||||
Du lässt Viktor zu lange mit ausgestreckter Hand warten und lächelst erst dann, als hättest du ihn absichtlich geprüft.
|
||||
|
||||
Es ist fast ein Fauxpas, gerettet durch Anmut und die Tatsache, dass Männer Demütigungen leichter verzeihen, wenn sie sich wie Aufmerksamkeit anfühlen. Viktor hilft dir hinunter. Seine Hand bleibt vollkommen korrekt. Sein Blick nicht ganz.
|
||||
|
||||
* [__Greife__: Selbst nach einem Koffer.] #action:object
|
||||
#route:careless
|
||||
#manners:awkward
|
||||
~ careless += 1
|
||||
Du entschuldigst dich beim Gepäckträger dafür, dass deine Sachen Mühe machen, und greifst selbst nach einem Koffer.
|
||||
|
||||
Der Gepäckträger erstarrt, als hättest du ihm eine philosophische Frage gestellt. Viktor tritt sofort dazwischen, höflich genug, um die Rettung wie Zufall aussehen zu lassen. Du hast gegen keine Moral verstoßen, nur gegen die unsichtbare Arbeitsteilung, auf der diese kleine Welt ruht.
|
||||
|
||||
* {birth_class == "working"} [__Nimm__ · **Unterschicht**: Dem Gepäckträger beinahe den Koffer ab.] #action:object #gated:working #key:t
|
||||
#manners:fauxpas
|
||||
~ class_confidence -= 1
|
||||
~ careless += 1
|
||||
Du springst hinunter, bevor jemand dir helfen kann, und nimmst dem Gepäckträger beinahe den Koffer aus der Hand.
|
||||
|
||||
Für eine Sekunde bist du schneller als deine Verkleidung. Der Gepäckträger hält fest, Viktor greift nach deinem Ellbogen, der Kutscher sieht weg, weil Wegsehen manchmal die höflichste Form von Zeugenschaft ist. Es ist kein Unglück. Nur ein Riss, klein genug, um ihn mit Haltung zu schließen.
|
||||
|
||||
-
|
||||
|
||||
Die Kutsche aus Hohenreith wartet jenseits des Stationshofes: dunkelgrüner Lack, schwarze Räder, das gräfliche Wappen dezent auf der Tür, zwei Pferde bereits unruhig im Geschirr. Der Kutscher nimmt den Hut ab, als er dich sieht. Nicht zu tief. Tief genug für Rang, nicht tief genug für Ehrfurcht. #sfx[horse-neigh.ogg]
|
||||
|
||||
„Gnädiges Fräulein? Herr Sekretär?“
|
||||
|
||||
{birth_class == "noble":
|
||||
Man hat ihm genug gesagt, um dich einzuordnen. Das ist eine Höflichkeit. Es ist auch eine Warnung.
|
||||
- else:
|
||||
Er zögert bei dir um das kleinste Maß. Das Zögern ist keine Unhöflichkeit. Es ist Berechnung. Erste Klasse, Hofschreiben, kein Titel außer Fräulein, und ein Mann neben dir, der aussieht, als hätte er Menschen für weniger verhaften lassen als Starren.
|
||||
}
|
||||
|
||||
Viktor antwortet, bevor du es kannst.
|
||||
|
||||
„Vom Jagdhaus Hohenreith?“
|
||||
|
||||
„Jawohl, Herr Sekretär. Der Weg ist befahrbar. Wenn der Nebel nicht dichter wird, sollten wir Eibenreith vor Einbruch der Dunkelheit erreichen.“
|
||||
|
||||
Das Wort tritt ohne Zeremonie in die Luft.
|
||||
|
||||
Eibenreith.
|
||||
|
||||
Nicht Hohenreith, der Name, der in sauberer Hand auf der Einladung steht. Eibenreith: das Dorf darunter. Ein kleinerer Name. Älter im Mund. Ein Name mit Wurzeln statt Briefpapier.
|
||||
|
||||
-> coach_journey
|
||||
@@ -0,0 +1,209 @@
|
||||
// eibenreith_03_graben.ink
|
||||
// Kapitel: Der Graben.
|
||||
// Enthält Kutschfahrt, optionale Grabenbeobachtungen und Statue/Viktor-Reaktion.
|
||||
|
||||
=== coach_journey ===
|
||||
|
||||
Die Kutsche lässt die Station hinter sich und damit das letzte leicht erkennbare Zeichen der Monarchie. #chapter[Der Graben] #music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)
|
||||
|
||||
Zuerst folgt der Weg einem Tal, in dem Telegraphendraht ihm noch Gesellschaft leistet und der Fluss in einem hellen, steinigen Bett läuft. Sägewerke, umzäunte Wiesen und Bauernhäuser erscheinen und verschwinden hinter Fichtenbeständen. Die Berge steigen nicht auf einmal. Sie rücken zuständigkeitsweise vor. Ein bewaldeter Hang beansprucht den linken Himmel, dann schließt eine graue Wand aus Kalk den Norden, dann sammelt sich im Osten ein weiterer Rücken, bis selbst die Wolken in Dienst getreten scheinen.
|
||||
|
||||
Der Kutscher nennt Orte, wenn Viktor fragt, doch die Namen sind örtlich und praktisch, gedacht für Männer, die wissen, welche Brücke bei Hochwasser nachgibt und welcher Hof störrische Pferde hält. Irgendwo hinter den sichtbaren Rücken, sagt er, liegt der große weiße Rücken des Hochschwab. Nach Osten, jenseits von Wald und Pass, hält die Hohe Veitsch ihr eigenes Wetter. Er sagt das nicht wie ein Führer, sondern wie ein Mann, der Nachbarn erklärt, die vielleicht guter Laune sind und vielleicht nicht.
|
||||
|
||||
Das Haupttal verengt sich.
|
||||
|
||||
Der Weg biegt davon in einen Seitengraben, und die Veränderung ist augenblicklich. Der Klang ändert sich. Die Räder klingen nicht mehr gegen offene Entfernung, sondern mahlen zwischen Böschungen, Wurzeln und nassem Stein. Die Luft riecht nach Lauberde, Harz und kaltem Wasser. Eiben erscheinen zwischen den Fichten in dunkler, unwahrscheinlicher Geduld, ihre Nadeln zu schwarz für den Nachmittag.
|
||||
|
||||
„Eibenreither Graben“, sagt der Kutscher und bekreuzigt sich so rasch, dass die Geste auch einem Schlagloch gegolten haben könnte.
|
||||
|
||||
Viktor bemerkt es. Natürlich bemerkt er es.
|
||||
|
||||
„Schlechter Weg?“, fragt er.
|
||||
|
||||
„Alter Weg“, sagt der Kutscher.
|
||||
|
||||
Eine Weile spricht niemand.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
=== coach_road_options ===
|
||||
|
||||
* [__Berühre__: Das kalte Kutschenfenster.] #action:object #optional
|
||||
Das Glas ist kälter, als es im Inneren der Kutsche sein dürfte. Feuchtigkeit sammelt sich an deinem Handschuh und verschwindet sofort wieder, als hätte sie es sich anders überlegt. Draußen streifen Zweige so nah vorbei, dass sie die Scheibe beinahe mit Nägeln prüfen.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
* [__Höre__: Auf die Räder im Graben.] #action:orientation #optional
|
||||
Das Geräusch der Räder hat sich verändert. Auf der offenen Straße war es ein Rhythmus; hier ist es ein Mahlen, ein Zählen, ein wiederholtes Bestehen gegen Stein und Wurzel. Der Weg klingt nicht befahren. Er klingt benutzt.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
* [__Untersuche__: Viktors Reaktion.] #action:orientation #optional
|
||||
Viktor betrachtet nicht die Landschaft. Er betrachtet ihre Möglichkeiten: Engstellen, Böschungen, tote Winkel, die Entfernung bis zum Kutscher, die Frage, wie rasch man aus einer Kutsche steigt, wenn die Straße selbst dagegen ist.
|
||||
|
||||
-> coach_road_options
|
||||
|
||||
* [__Warte__: In der schaukelnden Kutsche.] #action:social #key:z
|
||||
-> coach_after_road_options
|
||||
|
||||
=== coach_after_road_options ===
|
||||
|
||||
Du beobachtest die Bäume.
|
||||
|
||||
Es gibt Wälder, die zu Geschichten einladen, weil sie hübsch sind, und Wälder, die Geschichten zurückweisen, weil das, was dort geschah, keine Zeugen brauchte. Dieser gehört zur zweiten Art. Seine Stämme stehen eng, nicht wild, sondern mit der Haltung einer Menge, die Platz macht für etwas, das vor langer Zeit durch sie getragen wurde. Der Schnee in den Mulden ist nicht rein. Er hat Nadeln gesammelt, Rinde und einen gelblichen Fleck dort, wo Wasser von unten aufgestiegen ist.
|
||||
|
||||
An einem Hang oberhalb des Weges, halb vom Unterholz verschluckt, erblickst du Stein.
|
||||
|
||||
Ein Wegheiligtum vielleicht. Ein Grenzzeichen. Eine Figur. Die Kutsche rollt schon vorbei, bevor deine Augen sich auf ihre Form einigen können. Für einen Augenblick bleibt der Eindruck eines Frauenkopfes zurück, geneigt nicht im Gebet, sondern im Lauschen. #image[statue.png](square)
|
||||
|
||||
{
|
||||
- supernatural_senses == "genuine":
|
||||
Dein Nacken zieht sich zusammen.
|
||||
|
||||
Nicht Furcht. Wiedererkennen wäre schlimmer.
|
||||
- supernatural_senses == "ambiguous":
|
||||
Dein Nacken zieht sich zusammen.
|
||||
|
||||
Nicht Furcht. Wiedererkennen wäre schlimmer.
|
||||
- supernatural_senses == "repressed":
|
||||
Dein Nacken zieht sich zusammen.
|
||||
|
||||
Nicht Furcht. Wiedererkennen wäre schlimmer.
|
||||
- else:
|
||||
Du sagst dir, dass alter Stein, durch bewegte Zweige gesehen, zu allem wird, wozu der Geist feig genug ist.
|
||||
}
|
||||
|
||||
Viktor wendet sich leicht demselben Hang zu.
|
||||
|
||||
„Haben Sie etwas gesehen?“
|
||||
|
||||
* [__Antworte__: „Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“] #action:conversation
|
||||
#route:eccentric
|
||||
#hint:statue
|
||||
~ eccentric += 1
|
||||
~ viktor_suspicion += 1
|
||||
„Vielleicht eine Frau im Wald. Oder ein Stein, der eine sein wollte.“
|
||||
|
||||
Er betrachtet die vorbeiziehenden Bäume.
|
||||
|
||||
„Ein Wegheiligtum?“
|
||||
|
||||
** [__Antworte__: „Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“] #action:conversation
|
||||
„Wenn es ein Heiligtum ist, so ist es seit langem nicht mehr geliebt worden.“
|
||||
|
||||
„Sie sprechen, als bemerkten Steine Vernachlässigung.“
|
||||
|
||||
Soldaten bemerken Vernachlässigung ebenfalls. Sein Schweigen gesteht genug zu.
|
||||
|
||||
Er antwortet nicht.
|
||||
|
||||
** [__Antworte__: „Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“] #action:conversation
|
||||
~ supernatural_exposure += 1
|
||||
„Nein. Heiligtümer wenden sich den Gläubigen zu. Dieses Ding lauschte seitwärts.“
|
||||
|
||||
Viktors Hand ruht am Halteriemen der Kutsche, still und bereit.
|
||||
|
||||
--
|
||||
|
||||
* [__Antworte__: „Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“] #action:conversation
|
||||
#route:detective
|
||||
#hint:statue
|
||||
~ detective += 1
|
||||
~ viktor_trust += 1
|
||||
„Ein Grenz- oder Wegzeichen. Ich wüsste gern, wohin dieser Pfad führt.“
|
||||
|
||||
„Sie haben einen Pfad gesehen?“
|
||||
|
||||
** [__Antworte__: „Nicht deutlich. Genug, um später danach zu fragen.“] #action:conversation
|
||||
„Nicht deutlich. Genug, um später danach zu fragen.“
|
||||
|
||||
Viktor blickt durch das kleine rückwärtige Fenster. Die Biegung hat den Hang bereits ausgelöscht.
|
||||
|
||||
„Fragen Sie vorsichtig. Orte, die man nicht erwähnt, sind oft aufschlussreicher als jene, die man empfiehlt.“
|
||||
|
||||
** [__Antworte__: „Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“] #action:conversation
|
||||
#route:detective
|
||||
~ detective += 1
|
||||
„Nur die Andeutung eines Pfades. Wenn er existiert, erhält jemand seine Abwesenheit aufrecht.“
|
||||
|
||||
„Sie lassen Abwesenheiten kostspielig klingen.“
|
||||
|
||||
Das sind sie meistens; Abwesenheit ist teuer, wenn jemand sie pflegt.
|
||||
|
||||
--
|
||||
|
||||
* [__Antworte__: „Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“] #action:conversation
|
||||
#route:careless
|
||||
~ careless += 1
|
||||
~ viktor_relation = "dependence"
|
||||
„Nur Bäume. Die Art, bei der man für Herren mit Revolvern dankbar wird.“
|
||||
|
||||
Sein Ausdruck verdunkelt sich um einen amtlichen Grad.
|
||||
|
||||
„Ein Revolver ist ein schlechtes Werkzeug gegen Bäume.“
|
||||
|
||||
** [__Antworte__: „Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“] #action:conversation
|
||||
„Dann werde ich mich darauf verlassen, dass Ihre Unterhaltung sie einschüchtert.“
|
||||
|
||||
Der Kutscher tut, als höre er nichts. Seine Schultern jedoch hören alles.
|
||||
|
||||
** [__Antworte__: „Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“] #action:conversation
|
||||
#route:lover
|
||||
~ lover += 1
|
||||
„Wie bedauerlich. Sie wirkten so berufsmäßig beruhigend.“
|
||||
|
||||
„Ich bevorzuge Feinde, die sich zu erkennen geben.“
|
||||
|
||||
--
|
||||
|
||||
* [__Frage Viktor__: „Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“] #action:conversation
|
||||
#route:lover
|
||||
~ lover += 1
|
||||
~ viktor_suspicion += 1
|
||||
„Würden Sie mir glauben, wenn ich sagte, ich hätte etwas gesehen?“
|
||||
|
||||
„Das hinge davon ab, welchen Vorteil Sie sich von der Antwort versprechen.“
|
||||
|
||||
** [__Antworte__: „Herr Nowak. Sie verletzen mich.“] #action:conversation
|
||||
„Herr Nowak. Sie verletzen mich.“
|
||||
|
||||
„Noch nicht.“
|
||||
|
||||
Es ist das Erste, was er an diesem Tag gesagt hat, das beinahe wie ein Flirt klingt, wenn auch vielleicht nur deshalb, weil Gefahr ein Talent dafür hat, wärmere Kleider zu borgen.
|
||||
|
||||
** [__Weise Viktor an__: „Beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“] #action:social
|
||||
~ viktor_trust += 1
|
||||
„Dann beobachten Sie den Hang, nicht meine Absichten. Eines von beidem könnte nützlich sein.“
|
||||
|
||||
Er gehorcht, ohne zuzugeben, dass er es getan hat.
|
||||
|
||||
--
|
||||
|
||||
* [__Antworte__: „Nein.“] #action:conversation
|
||||
#route:sapphic
|
||||
~ sapphic += 1
|
||||
„Nein.“
|
||||
|
||||
Die Verneinung kommt zu rasch, und ihr hört es beide.
|
||||
|
||||
Du denkst nicht mehr an den Stein. Du denkst an die junge Frau, die irgendwo vor euch wartet: die Tochter des Grafen, der Grund, der sorgsam nicht im Memorandum steht, die Fremde, deren Haushalt dich unter einem Titel herbeigerufen hat, der zugleich lächerlich und nützlich ist.
|
||||
|
||||
** [__Antworte__: „Es war nur Schatten.“] #action:conversation
|
||||
„Es war nur Schatten.“
|
||||
|
||||
Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
|
||||
|
||||
** [__Antworte__: „Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“] #action:conversation
|
||||
#route:detective
|
||||
~ detective += 1
|
||||
„Oder, falls ich etwas sah, ziehe ich es vor, es mir nicht erklären zu lassen, bevor ich verstehe, warum es von Bedeutung ist.“
|
||||
|
||||
Wenn dieser Ort Frauen in Stein hält, denkst du, was tut er dann mit ihnen in den Häusern?
|
||||
|
||||
--
|
||||
|
||||
-
|
||||
|
||||
Der Graben öffnet sich widerwillig.
|
||||
|
||||
-> village_arrival_options
|
||||
@@ -0,0 +1,161 @@
|
||||
// eibenreith_04_dorf.ink
|
||||
// Kapitel: Eibenreith.
|
||||
// Enthält Dorfankunft, optionale Dorfbeobachtungen, Ausstiegs-Manierenpuzzle und Schluss des Intros.
|
||||
|
||||
=== village_arrival_options ===
|
||||
|
||||
Zuerst kommt der Geruch von Rauch. Dann ein Dach, niedrig und dunkel vom Wetter. Dann ein zweites. Dann ein Kirchturm, nicht hoch, nicht anmutig, sondern breitschultrig und blass vor dem Hang dahinter. Seine Mauern wirken älter als das Dorf um sie her und weniger sicher ihres Sieges. Die Fenster sind klein. Die Kirchhofmauer hält die Straße auf Abstand, als bräuchten die Toten Schutz vor den Lebenden oder die Lebenden vor etwas anderem. #chapter[Eibenreith] #sfx[church-bells.ogg](max=8, fade) #image[eibenreith.png](landscape)
|
||||
|
||||
Eibenreith erscheint nicht, wie ein Dorf auf einem Bild erscheint, auf einmal und zur Bewunderung geordnet, sondern in Bruchstücken.
|
||||
|
||||
Eine Frau mit einem dunklen Kopftuch hält mit einem Eimer in der Hand inne. Ein Bub hört auf, Gänse zu treiben, und lässt sie um seine Stiefel klagen. Zwei Männer vor einem Schuppen beenden im selben Augenblick ihr Gespräch, ohne einander anzusehen. Vorhänge rühren sich an Fenstern, hinter denen niemand zugibt zu stehen. Ein Schmiedeschild bewegt sich leicht in Luft, die du nicht fühlen kannst. Wasser läuft irgendwo unter Brettern, unter Stein, unter der Straße selbst, schnell, kalt und verborgen.
|
||||
|
||||
Die Häuser sind nicht arm, nicht eigentlich. Viele sind fest, weißgekalkt, geschindelt, erhalten mit der störrischen Anständigkeit von Menschen, die reparieren, was sie nicht ersetzen können. Und doch stört etwas in ihrer Anordnung das Auge. Sie wenden sich der Kirche zu, aber nicht ganz. Sie halten die Straße, aber lehnen sich von ihr weg. Sie lassen zwischen Hof, Zaun und Holzstoß schmale Durchgänge, in denen sich Schatten zu früh sammelt.
|
||||
|
||||
Die Kutsche wird langsamer.
|
||||
|
||||
Niemand läuft herbei, um sie zu begrüßen.
|
||||
|
||||
Niemand muss das. Die Nachricht ist bereits ins Dorf eingetreten, auf Wegen schneller als Bahn, Telegraph oder kaiserliches Siegel.
|
||||
|
||||
Du sitzt sehr gerade, während Eibenreith dich zum ersten Mal betrachtet.
|
||||
|
||||
* [__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
|
||||
|
||||
* [__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
|
||||
|
||||
* [__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.
|
||||
|
||||
{
|
||||
- religion_stance == "devout_catholic":
|
||||
Gerade das stört dich: nicht der Mangel an Schönheit, sondern der Mangel an Frieden.
|
||||
- religion_stance == "josephinian_sceptic":
|
||||
Du siehst weniger Andacht als Institution: Stein, Besitz, Grenze, Verwaltung der Furcht.
|
||||
- else:
|
||||
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
|
||||
|
||||
* [__Warte__: Bis die Kutsche hält.] #action:social #key:z
|
||||
-> village_exit_puzzle
|
||||
|
||||
=== village_exit_puzzle ===
|
||||
|
||||
Der Kutscher hält vor dem Wirtshaus oder vielleicht nur vor dem Gebäude, das in einem besseren Dorf eines gewesen wäre. Ein Knecht aus dem Dorf tritt aus dem Schatten des Tors. Viktor öffnet die Kutschentür von innen nicht sofort; der Kutscher steigt ab, um den Schlag zu öffnen. Der Knecht sieht auf dein Gepäck, dann auf deine Handschuhe, dann auf Viktor.
|
||||
|
||||
Wieder stellt die Welt eine Frage, ohne sie auszusprechen: Wer darf dir helfen, wer muss dir helfen, und wem erlaubst du, dabei wichtig zu wirken?
|
||||
|
||||
* {birth_class == "noble"} [__Warte__ · **Adel**: Bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt.] #action:social #gated:noble #key:z
|
||||
#manners:excellent
|
||||
~ class_confidence += 2
|
||||
Du wartest, bis der Kutscher den Schlag öffnet und Viktor zuerst aussteigt; erst dann reichst du Viktor die behandschuhte Hand.
|
||||
|
||||
Es geschieht langsam genug, dass alle Beteiligten ihre Rolle finden. Der Kutscher ist Dienst, Viktor ist Begleitung, der Knecht ist noch nicht wichtig genug, um dich zu berühren. Dein Fuß erreicht den Boden, als hätte die Straße sich dafür bereitgehalten.
|
||||
|
||||
* [__Nicke__: Dem Kutscher knapp zu, nachdem Viktor dir geholfen hat.] #action:social
|
||||
#manners:good
|
||||
~ viktor_trust += 1
|
||||
Du lässt Viktor aussteigen, nimmst seine Hand beim Abtreten und dankst dem Kutscher erst danach mit einem knappen Blick.
|
||||
|
||||
Der Ablauf ist korrekt genug, um keine Geschichte zu erzeugen. In einem Dorf, das von Geschichten lebt, ist das ein kleiner Sieg.
|
||||
|
||||
* [__Weise den Knecht an__: „Zuerst den kleineren Kasten.“] #action:social
|
||||
#route:detective
|
||||
#manners:practical
|
||||
~ detective += 1
|
||||
Du gibst dem Knecht eine klare Anweisung, welches Gepäck zuerst abgeladen werden soll, bevor er danach fragen kann.
|
||||
|
||||
Er gehorcht sofort. Viktor registriert die Zweckmäßigkeit. Der Kutscher registriert die Ungewöhnlichkeit. Eine Dame, die Gepäckreihenfolgen kennt, ist entweder sehr erfahren, sehr nervös oder beides.
|
||||
|
||||
* [__Lächle__: Dem Kutscher zu freundlich zu.] #action:social
|
||||
#route:lover
|
||||
#manners:too_warm
|
||||
~ lover += 1
|
||||
Du bietest dem Kutscher ein sichtbares Lächeln und ein zu freundliches „Danke“ an.
|
||||
|
||||
Der Mann senkt den Blick, verwirrt und geschmeichelt. Viktor wird stiller. Freundlichkeit über Standesgrenzen hinweg kann Güte sein, Taktik oder Unachtsamkeit. Auf dem Dorf wird niemand lange brauchen, eine vierte Möglichkeit zu erfinden.
|
||||
|
||||
* [__Steige aus__: Zu früh, ehe alle Rollen verteilt sind.] #action:movement
|
||||
#route:careless
|
||||
#manners:awkward
|
||||
~ careless += 1
|
||||
~ viktor_relation = "dependence"
|
||||
Du steigst zu früh aus, trittst beinahe in den Straßenschlamm und fängst dich an Viktors Arm.
|
||||
|
||||
Er hält dich ohne sichtbare Anstrengung fest. Für einen Augenblick sieht das Dorf genau das, was es am liebsten sieht: eine Dame, gerettet durch einen Mann. Es ist lächerlich nützlich und nützlich lächerlich.
|
||||
|
||||
* {birth_class == "working"} [__Steige aus__ · **Unterschicht**: Allein, bevor jemand dir helfen kann.] #action:movement #gated:working
|
||||
#manners:fauxpas
|
||||
~ class_confidence -= 1
|
||||
Du steigst allein aus, nimmst deinen Rock hoch genug, um den Schlamm zu sehen, und sagst dem Knecht, er solle mit dem schweren Koffer vorsichtig sein.
|
||||
|
||||
Es ist praktisch, schnell und völlig falsch. Nicht, weil du unrecht hast, sondern weil du recht hast wie jemand, der selbst schon getragen hat. Der Knecht erkennt es. Viktor auch.
|
||||
|
||||
-
|
||||
|
||||
Neben dir senkt Viktor die Stimme.
|
||||
|
||||
„Vergessen Sie nicht: In Hohenreith wird jede Höflichkeit etwas bedeuten. Hier wird es jedes Schweigen tun.“
|
||||
|
||||
* [__Antworte__: „Dann werden wir bereits empfangen.“] #action:conversation
|
||||
#route:detective
|
||||
~ detective += 1
|
||||
„Dann werden wir bereits empfangen.“
|
||||
|
||||
„Ja“, sagt er. „Und geprüft.“
|
||||
|
||||
* [__Antworte__: „Sie lassen es klingen, als stünde das Dorf über dem Grafen.“] #action:conversation
|
||||
#route:eccentric
|
||||
~ eccentric += 1
|
||||
„Sie lassen es klingen, als stünde das Dorf über dem Grafen.“
|
||||
|
||||
„Nein“, sagt Viktor. „Nur, als hätte es vielleicht mehr als einen überlebt.“
|
||||
|
||||
* [__Antworte__: „Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“] #action:conversation
|
||||
#route:lover
|
||||
~ lover += 1
|
||||
„Wie glücklich, dass ich mehrere Arten des Schweigens eingepackt habe.“
|
||||
|
||||
Sein Mund bewegt sich beinahe. „Verwenden Sie zuerst das schlichteste.“
|
||||
|
||||
* [__Antworte__: „Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“] #action:conversation
|
||||
#route:careless
|
||||
~ careless += 1
|
||||
„Ich mag es nicht, von Leuten beobachtet zu werden, die sich nicht vorstellen.“
|
||||
|
||||
„Das“, sagt er, „wird sich heute kaum bessern.“
|
||||
|
||||
* [__Antworte__: „Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“] #action:conversation
|
||||
#route:sapphic
|
||||
~ sapphic += 1
|
||||
„Wenn Amalia ihr ganzes Leben unter diesem Blick gelebt hat, beginne ich zu verstehen, weshalb man nach Geistern sandte.“
|
||||
|
||||
Viktor sieht dich an, doch welche Antwort er auch erwägt, er behält sie hinter den Zähnen.
|
||||
|
||||
-
|
||||
|
||||
Die Pferde ziehen die Kutsche an der Kirchhofmauer vorbei. Darüber, auf dem alten Putz neben dem Tor, blickt eine verblasste gemalte Frau unter einem abblätternden blauen Mantel herab. Ihre Hände sind zum Gebet gefaltet. Ihre Augen, vom Wetter beschädigt, zeigen nicht mehr in dieselbe Richtung.
|
||||
|
||||
{
|
||||
- religion_stance == "devout_catholic":
|
||||
Für einen Atemzug stört dich nicht, dass das Bild alt ist. Es stört dich, dass es nicht mehr ganz heilig wirkt.
|
||||
- religion_stance == "josephinian_sceptic":
|
||||
Für einen Atemzug wirkt das Bild weniger wie Andacht als wie Verwaltung: ein aufgemaltes Siegel über etwas, das man nicht fortschaffen konnte.
|
||||
- religion_stance == "wounded_catholic":
|
||||
Für einen Atemzug trifft dich das gemalte Gesicht an einer Stelle, die du lieber Schuld als Erinnerung nennen würdest.
|
||||
- else:
|
||||
Für einen Atemzug, als die Räder über ein verborgenes Wasserrinnsal fahren, wirkt das gemalte Gesicht weniger wie die Heilige Mutter als wie eine Maske, die etwas aufgesetzt wurde, das länger gewartet hatte.
|
||||
}
|
||||
|
||||
Dann fährt die Kutsche in das eigentliche Dorf hinein, und die Straße biegt zu der unsichtbaren Höhe, auf der Jagdhaus Hohenreith über Eibenreith unter seinem neueren Namen steht.
|
||||
|
||||
#score[Du hast Eibenreith erreicht.]
|
||||
-> END
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -22,5 +22,7 @@ export declare class InkEngine {
|
||||
private getChoiceTags;
|
||||
private extractChoicePreviewTags;
|
||||
private resolveInkPath;
|
||||
private findNamedInkChild;
|
||||
private getInkContainerMap;
|
||||
private isNamedContainerMap;
|
||||
}
|
||||
|
||||
Vendored
+31
-4
@@ -236,12 +236,12 @@ class InkEngine {
|
||||
for (const part of parts) {
|
||||
if (!node)
|
||||
return null;
|
||||
if (Array.isArray(node) && node.length > 0 && this.isNamedContainerMap(node[node.length - 1]) && part in node[node.length - 1]) {
|
||||
node = node[node.length - 1][part];
|
||||
}
|
||||
else if (Array.isArray(node) && /^\d+$/.test(part)) {
|
||||
if (Array.isArray(node) && /^\d+$/.test(part)) {
|
||||
node = node[Number(part)];
|
||||
}
|
||||
else if (Array.isArray(node)) {
|
||||
node = this.findNamedInkChild(node, part);
|
||||
}
|
||||
else if (this.isNamedContainerMap(node) && part in node) {
|
||||
node = node[part];
|
||||
}
|
||||
@@ -251,6 +251,33 @@ class InkEngine {
|
||||
}
|
||||
return node;
|
||||
}
|
||||
findNamedInkChild(container, part) {
|
||||
for (let index = container.length - 1; index >= 0; index -= 1) {
|
||||
const item = container[index];
|
||||
if (this.isNamedContainerMap(item) && part in item) {
|
||||
return item[part];
|
||||
}
|
||||
if (!Array.isArray(item))
|
||||
continue;
|
||||
const namedMap = this.getInkContainerMap(item);
|
||||
if (namedMap?.['#n'] === part) {
|
||||
return item;
|
||||
}
|
||||
if (namedMap && part in namedMap) {
|
||||
return namedMap[part];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getInkContainerMap(container) {
|
||||
for (let index = container.length - 1; index >= 0; index -= 1) {
|
||||
const item = container[index];
|
||||
if (this.isNamedContainerMap(item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
isNamedContainerMap(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1467,6 +1467,23 @@ html[data-process-state="playing-ready"] [role="button"] {
|
||||
font-feature-settings: "kern" on, "liga" on, "onum" on, "pnum" on, "dlig" on, "clig" on, "calt" on;
|
||||
}
|
||||
|
||||
.story-glossary-word {
|
||||
border-bottom: none;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-color: rgba(74, 54, 33, 0.62);
|
||||
text-decoration-thickness: 0.035em;
|
||||
text-underline-offset: 0.12em;
|
||||
cursor: var(--pointer-cursor, help);
|
||||
}
|
||||
|
||||
.story-glossary-word:hover,
|
||||
.story-glossary-word:focus-visible {
|
||||
color: var(--ink-strong);
|
||||
clip-path: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@keyframes wordReveal {
|
||||
0% {
|
||||
opacity: 1;
|
||||
@@ -1478,6 +1495,48 @@ html[data-process-state="playing-ready"] [role="button"] {
|
||||
}
|
||||
}
|
||||
|
||||
.story-glossary-tooltip {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 240;
|
||||
width: min(calc(var(--book-width) * 0.22), 28rem);
|
||||
max-width: calc(100vw - 2rem);
|
||||
background: var(--panel-paper);
|
||||
color: var(--ink-text);
|
||||
border: 1px solid var(--panel-border);
|
||||
box-shadow: 0 1.2rem 3rem rgba(0, 0, 0, 0.34);
|
||||
font-family: 'EB Garamond', var(--book-font), serif;
|
||||
font-size: var(--ui-modal-font-size);
|
||||
line-height: 1.25;
|
||||
opacity: 0;
|
||||
transform: translateY(0.22rem);
|
||||
pointer-events: none;
|
||||
transition: opacity 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.story-glossary-tooltip.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.story-glossary-tooltip-header {
|
||||
border-bottom: 1px solid var(--rule-brown);
|
||||
padding: calc(var(--story-line-height) * 0.35) calc(var(--story-line-height) * 0.55) calc(var(--story-line-height) * 0.24);
|
||||
}
|
||||
|
||||
.story-glossary-tooltip-title {
|
||||
margin: 0;
|
||||
color: var(--ink-strong);
|
||||
font: inherit;
|
||||
font-style: italic;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.story-glossary-tooltip-body {
|
||||
padding: calc(var(--story-line-height) * 0.38) calc(var(--story-line-height) * 0.55) calc(var(--story-line-height) * 0.48);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
font-synthesis: none;
|
||||
|
||||
@@ -23,7 +23,16 @@ class LayoutRendererModule extends BaseModule {
|
||||
this.bindMethods([
|
||||
'renderParagraph',
|
||||
'initializeContainers',
|
||||
'adjustJustification'
|
||||
'adjustJustification',
|
||||
'decorateInlineWord',
|
||||
'applyGlossaryEntries',
|
||||
'normalizeGlossaryText',
|
||||
'decorateGlossaryWord',
|
||||
'ensureGlossaryTooltip',
|
||||
'showGlossaryTooltip',
|
||||
'hideGlossaryTooltip',
|
||||
'positionGlossaryTooltip',
|
||||
'escapeRegExp'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -142,6 +151,7 @@ class LayoutRendererModule extends BaseModule {
|
||||
maxLineWidth
|
||||
});
|
||||
});
|
||||
this.applyGlossaryEntries(paragraph, layoutData.glossaryEntries);
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
@@ -184,6 +194,7 @@ class LayoutRendererModule extends BaseModule {
|
||||
if (lastChild && isTrailingPunctuation) {
|
||||
syllable += node.value;
|
||||
lastChild.innerHTML = syllable;
|
||||
this.decorateInlineWord(lastChild, stack);
|
||||
currentLeft += node.width;
|
||||
} else if (j > breaks[i-1].position + 1 &&
|
||||
nodes[j-1].type === 'penalty' &&
|
||||
@@ -191,6 +202,7 @@ class LayoutRendererModule extends BaseModule {
|
||||
// Combine with previous syllable using zero-width non-joiner
|
||||
syllable += '\u200c' + node.value;
|
||||
lastChild.innerHTML = syllable;
|
||||
this.decorateInlineWord(lastChild, stack);
|
||||
currentLeft += node.width;
|
||||
} else {
|
||||
// Create new word span
|
||||
@@ -214,6 +226,7 @@ class LayoutRendererModule extends BaseModule {
|
||||
word.style.clipPath = 'inset(0 100% 0 0)';
|
||||
syllable = node.value;
|
||||
word.innerHTML = syllable;
|
||||
this.decorateInlineWord(word, stack);
|
||||
lastChild = word;
|
||||
|
||||
if (wordCount < 5 || (wordCount % 20 === 0)) {
|
||||
@@ -283,14 +296,178 @@ class LayoutRendererModule extends BaseModule {
|
||||
word.style.visibility = 'hidden';
|
||||
word.style.clipPath = 'inset(0 100% 0 0)';
|
||||
word.innerHTML = "-";
|
||||
this.decorateInlineWord(word, stack);
|
||||
stack[stack.length - 1].appendChild(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.applyGlossaryEntries(paragraph, layoutData.glossaryEntries);
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
decorateInlineWord(word, stack = []) {
|
||||
if (!word || !Array.isArray(stack)) return;
|
||||
|
||||
const glossaryElement = stack
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(element => element?.classList?.contains('story-glossary-term'));
|
||||
if (!glossaryElement) return;
|
||||
|
||||
const definition = glossaryElement.dataset.glossary || glossaryElement.getAttribute('title') || '';
|
||||
word.classList.add('story-glossary-word');
|
||||
word.dataset.glossary = definition;
|
||||
word.removeAttribute('title');
|
||||
word.tabIndex = 0;
|
||||
word.setAttribute('aria-label', `${word.textContent}: ${definition}`);
|
||||
}
|
||||
|
||||
applyGlossaryEntries(paragraph, entries = []) {
|
||||
if (!paragraph || !Array.isArray(entries) || entries.length === 0) return;
|
||||
|
||||
const words = Array.from(paragraph.querySelectorAll('.word'))
|
||||
.map(element => ({
|
||||
element,
|
||||
text: this.normalizeGlossaryText(element.textContent || '')
|
||||
}))
|
||||
.filter(word => word.text.length > 0 && word.text !== '-');
|
||||
|
||||
if (words.length === 0) return;
|
||||
|
||||
let cursor = 0;
|
||||
const segments = [];
|
||||
const fullText = words.map((word, index) => {
|
||||
if (index > 0) cursor += 1;
|
||||
const start = cursor;
|
||||
cursor += word.text.length;
|
||||
segments.push({ ...word, start, end: cursor });
|
||||
return word.text;
|
||||
}).join(' ');
|
||||
|
||||
entries
|
||||
.filter(entry => entry && entry.term && entry.definition)
|
||||
.forEach(entry => {
|
||||
const normalizedTerm = this.normalizeGlossaryText(entry.term);
|
||||
if (!normalizedTerm) return;
|
||||
|
||||
const matcher = new RegExp(`(^|\\s)(${this.escapeRegExp(normalizedTerm)})(?=\\s|$|[.,;:!?])`, 'giu');
|
||||
let match;
|
||||
while ((match = matcher.exec(fullText)) !== null) {
|
||||
const matchStart = match.index + match[1].length;
|
||||
const matchEnd = matchStart + match[2].length;
|
||||
segments
|
||||
.filter(segment => segment.end > matchStart && segment.start < matchEnd)
|
||||
.forEach(segment => this.decorateGlossaryWord(segment.element, entry));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
normalizeGlossaryText(text) {
|
||||
return String(text || '')
|
||||
.replace(/\u200c/g, '')
|
||||
.replace(/\u00ad/g, '')
|
||||
.replace(/-\s*$/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
decorateGlossaryWord(word, entry) {
|
||||
if (!word || !entry?.definition) return;
|
||||
word.classList.add('story-glossary-word');
|
||||
word.dataset.glossaryTerm = entry.term || this.normalizeGlossaryText(word.textContent || '');
|
||||
word.dataset.glossary = entry.definition;
|
||||
word.removeAttribute('title');
|
||||
word.tabIndex = 0;
|
||||
word.setAttribute('aria-label', `${this.normalizeGlossaryText(word.textContent || '')}: ${entry.definition}`);
|
||||
if (word.dataset.glossaryBound === 'true') return;
|
||||
word.dataset.glossaryBound = 'true';
|
||||
word.addEventListener('mouseenter', this.showGlossaryTooltip);
|
||||
word.addEventListener('focus', this.showGlossaryTooltip);
|
||||
word.addEventListener('mousemove', this.positionGlossaryTooltip);
|
||||
word.addEventListener('mouseleave', this.hideGlossaryTooltip);
|
||||
word.addEventListener('blur', this.hideGlossaryTooltip);
|
||||
}
|
||||
|
||||
ensureGlossaryTooltip() {
|
||||
let tooltip = document.getElementById('story_glossary_tooltip');
|
||||
if (tooltip) return tooltip;
|
||||
|
||||
tooltip = document.createElement('div');
|
||||
tooltip.id = 'story_glossary_tooltip';
|
||||
tooltip.className = 'story-glossary-tooltip';
|
||||
tooltip.setAttribute('role', 'tooltip');
|
||||
tooltip.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'story-glossary-tooltip-header';
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'story-glossary-tooltip-title';
|
||||
header.appendChild(title);
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'story-glossary-tooltip-body';
|
||||
|
||||
tooltip.append(header, body);
|
||||
document.body.appendChild(tooltip);
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
showGlossaryTooltip(event) {
|
||||
const word = event.currentTarget;
|
||||
if (!word) return;
|
||||
const tooltip = this.ensureGlossaryTooltip();
|
||||
const title = tooltip.querySelector('.story-glossary-tooltip-title');
|
||||
const body = tooltip.querySelector('.story-glossary-tooltip-body');
|
||||
if (title) title.textContent = word.dataset.glossaryTerm || this.normalizeGlossaryText(word.textContent || '');
|
||||
if (body) body.textContent = word.dataset.glossary || '';
|
||||
tooltip.dataset.anchorId = word.id || '';
|
||||
tooltip.__anchorElement = word;
|
||||
tooltip.classList.add('visible');
|
||||
tooltip.setAttribute('aria-hidden', 'false');
|
||||
this.positionGlossaryTooltip(event);
|
||||
}
|
||||
|
||||
hideGlossaryTooltip() {
|
||||
const tooltip = document.getElementById('story_glossary_tooltip');
|
||||
if (!tooltip) return;
|
||||
tooltip.classList.remove('visible');
|
||||
tooltip.setAttribute('aria-hidden', 'true');
|
||||
tooltip.__anchorElement = null;
|
||||
}
|
||||
|
||||
positionGlossaryTooltip(event) {
|
||||
const tooltip = document.getElementById('story_glossary_tooltip');
|
||||
if (!tooltip || !tooltip.classList.contains('visible')) return;
|
||||
|
||||
const anchor = event?.currentTarget || tooltip.__anchorElement;
|
||||
if (!anchor || typeof anchor.getBoundingClientRect !== 'function') return;
|
||||
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const margin = Math.max(8, window.innerWidth * 0.006);
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const preferredLeft = anchorRect.left + (anchorRect.width / 2) - (tooltipRect.width / 2);
|
||||
let left = Math.min(
|
||||
Math.max(margin, preferredLeft),
|
||||
Math.max(margin, window.innerWidth - tooltipRect.width - margin)
|
||||
);
|
||||
let top = anchorRect.top - tooltipRect.height - margin;
|
||||
if (top < margin) {
|
||||
top = anchorRect.bottom + margin;
|
||||
}
|
||||
top = Math.min(
|
||||
Math.max(margin, top),
|
||||
Math.max(margin, window.innerHeight - tooltipRect.height - margin)
|
||||
);
|
||||
|
||||
tooltip.style.left = `${left}px`;
|
||||
tooltip.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
renderLine({ paragraph, line, lineIndex, contentTopLines, lineHeight, maxLineWidth }) {
|
||||
const lineWidth = Number(line.measure || maxLineWidth);
|
||||
const lineOffset = Number(line.offset || 0);
|
||||
@@ -311,6 +488,7 @@ class LayoutRendererModule extends BaseModule {
|
||||
if (lastChild && isTrailingPunctuation) {
|
||||
syllable += node.value;
|
||||
lastChild.innerHTML = syllable;
|
||||
this.decorateInlineWord(lastChild, stack);
|
||||
currentLeft += node.width || 0;
|
||||
continue;
|
||||
}
|
||||
@@ -330,6 +508,7 @@ class LayoutRendererModule extends BaseModule {
|
||||
word.style.clipPath = 'inset(0 100% 0 0)';
|
||||
syllable = node.value;
|
||||
word.innerHTML = syllable;
|
||||
this.decorateInlineWord(word, stack);
|
||||
stack[stack.length - 1].appendChild(word);
|
||||
lastChild = word;
|
||||
currentLeft += node.width || 0;
|
||||
|
||||
@@ -18,6 +18,7 @@ class MarkupParserModule extends BaseModule {
|
||||
'parse',
|
||||
'parseParagraph',
|
||||
'parseInline',
|
||||
'extractGlossaryTags',
|
||||
'parseImageOptions',
|
||||
'parseSfxOptions',
|
||||
'parseMusicOptions',
|
||||
@@ -227,6 +228,21 @@ class MarkupParserModule extends BaseModule {
|
||||
return this.smartypants(plain).replace(/\s{2,}/g, ' ').trim();
|
||||
}
|
||||
|
||||
extractGlossaryTags(tags = []) {
|
||||
if (!Array.isArray(tags)) return [];
|
||||
|
||||
return tags
|
||||
.filter(tag => String(tag?.key || '').toLowerCase() === 'gloss')
|
||||
.map(tag => {
|
||||
const term = String(tag?.value || '').trim();
|
||||
const definition = String(tag?.param || '').trim();
|
||||
if (!term || !definition) return null;
|
||||
return { term, definition };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.term.length - a.term.length);
|
||||
}
|
||||
|
||||
smartypants(text) {
|
||||
const result = String(text)
|
||||
.replace(/---/g, '\u2014')
|
||||
|
||||
@@ -499,6 +499,8 @@ class SentenceQueueModule extends BaseModule {
|
||||
blockId: metadata.blockId ?? null,
|
||||
gameId: metadata.gameId ?? null,
|
||||
paragraphIndex: metadata.paragraphIndex ?? null,
|
||||
layoutText: metadata.layoutText || text,
|
||||
glossaryEntries: Array.isArray(metadata.glossaryEntries) ? metadata.glossaryEntries : [],
|
||||
isFirstParagraphInChapter: Boolean(metadata.isFirstParagraphInChapter),
|
||||
role: metadata.role || (metadata.type === 'heading' ? 'chapter-heading' : 'body'),
|
||||
dropCap: Boolean(metadata.dropCap),
|
||||
@@ -629,6 +631,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
lines: layout.lines || null,
|
||||
processedText: layout.processedText || text,
|
||||
sourceLayoutText: layoutText,
|
||||
glossaryEntries: Array.isArray(metadata.glossaryEntries) ? metadata.glossaryEntries : [],
|
||||
measures,
|
||||
lineOffsets,
|
||||
indentWidth,
|
||||
|
||||
@@ -58,6 +58,7 @@ class SocketClientModule extends BaseModule {
|
||||
'normalizeHistoryBlock',
|
||||
'dispatchTurnTags',
|
||||
'isTimedCueTag',
|
||||
'isRenderMetadataTag',
|
||||
'cueMarkersFromTags',
|
||||
'dispatchChoices',
|
||||
'dispatchInputMode',
|
||||
@@ -360,12 +361,22 @@ class SocketClientModule extends BaseModule {
|
||||
: { role: pendingParagraph || null, cueTags: [] };
|
||||
const tags = Array.isArray(paragraph?.tags) ? paragraph.tags : [];
|
||||
const { blocks, paragraphRole } = this.blocksFromTags(tags, turnId);
|
||||
const text = String(paragraph?.text || '').trim();
|
||||
const rawText = String(paragraph?.text || '').trim();
|
||||
const markupParser = this.getModule('markup-parser');
|
||||
const parsedParagraph = rawText && markupParser && typeof markupParser.parseParagraph === 'function'
|
||||
? markupParser.parseParagraph(rawText)
|
||||
: null;
|
||||
const text = String(parsedParagraph?.text || rawText).trim();
|
||||
const layoutText = parsedParagraph?.layoutText || paragraph.layoutText || text;
|
||||
const glossaryEntries = markupParser && typeof markupParser.extractGlossaryTags === 'function'
|
||||
? markupParser.extractGlossaryTags(tags)
|
||||
: [];
|
||||
const cueTags = tags.filter(tag => this.isTimedCueTag(tag));
|
||||
const deferredTags = tags.filter(tag => this.isDeferredPopupTag(tag));
|
||||
const immediateTags = tags.filter(tag =>
|
||||
!this.isStructuralTag(tag) &&
|
||||
!this.isTimedCueTag(tag) &&
|
||||
!this.isRenderMetadataTag(tag) &&
|
||||
!this.isDeferredPopupTag(tag)
|
||||
);
|
||||
|
||||
@@ -390,6 +401,7 @@ class SocketClientModule extends BaseModule {
|
||||
const role = pending.role || paragraphRole || 'body';
|
||||
const cueMarkers = [
|
||||
...(Array.isArray(paragraph.cueMarkers) ? paragraph.cueMarkers : []),
|
||||
...(Array.isArray(parsedParagraph?.cueMarkers) ? parsedParagraph.cueMarkers : []),
|
||||
...this.cueMarkersFromTags([
|
||||
...(Array.isArray(pending.cueTags) ? pending.cueTags : []),
|
||||
...cueTags
|
||||
@@ -398,7 +410,8 @@ class SocketClientModule extends BaseModule {
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
text,
|
||||
layoutText: paragraph.layoutText || text,
|
||||
layoutText,
|
||||
glossaryEntries,
|
||||
cueMarkers,
|
||||
deferredTags: [
|
||||
...(Array.isArray(pending.deferredTags) ? pending.deferredTags : []),
|
||||
@@ -467,6 +480,11 @@ class SocketClientModule extends BaseModule {
|
||||
return ['sfx', 'sound', 'audio'].includes(key);
|
||||
}
|
||||
|
||||
isRenderMetadataTag(tag) {
|
||||
const key = String(tag?.key || '').toLowerCase();
|
||||
return ['gloss'].includes(key);
|
||||
}
|
||||
|
||||
isDeferredPopupTag(tag) {
|
||||
const key = String(tag?.key || '').toLowerCase();
|
||||
return ['alert', 'achievement', 'score', 'error'].includes(key);
|
||||
|
||||
@@ -1477,6 +1477,7 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
addTopSpace: Boolean(item.addTopSpace ?? item.metadata?.addTopSpace),
|
||||
paragraphIndex: item.paragraphIndex ?? item.metadata?.paragraphIndex,
|
||||
cueMarkers: item.cueMarkers || item.metadata?.cueMarkers || [],
|
||||
glossaryEntries: item.glossaryEntries || item.metadata?.glossaryEntries || [],
|
||||
turnId: item.turnId ?? item.metadata?.turnId,
|
||||
blockId: item.blockId ?? item.metadata?.blockId,
|
||||
gameId: item.gameId ?? item.metadata?.gameId
|
||||
|
||||
@@ -276,10 +276,10 @@ export class InkEngine {
|
||||
for (const part of parts) {
|
||||
if (!node) return null;
|
||||
|
||||
if (Array.isArray(node) && node.length > 0 && this.isNamedContainerMap(node[node.length - 1]) && part in node[node.length - 1]) {
|
||||
node = node[node.length - 1][part];
|
||||
} else if (Array.isArray(node) && /^\d+$/.test(part)) {
|
||||
if (Array.isArray(node) && /^\d+$/.test(part)) {
|
||||
node = node[Number(part)];
|
||||
} else if (Array.isArray(node)) {
|
||||
node = this.findNamedInkChild(node, part);
|
||||
} else if (this.isNamedContainerMap(node) && part in node) {
|
||||
node = node[part];
|
||||
} else {
|
||||
@@ -290,6 +290,38 @@ export class InkEngine {
|
||||
return node;
|
||||
}
|
||||
|
||||
private findNamedInkChild(container: any[], part: string): any {
|
||||
for (let index = container.length - 1; index >= 0; index -= 1) {
|
||||
const item = container[index];
|
||||
|
||||
if (this.isNamedContainerMap(item) && part in item) {
|
||||
return item[part];
|
||||
}
|
||||
|
||||
if (!Array.isArray(item)) continue;
|
||||
|
||||
const namedMap = this.getInkContainerMap(item);
|
||||
if (namedMap?.['#n'] === part) {
|
||||
return item;
|
||||
}
|
||||
if (namedMap && part in namedMap) {
|
||||
return namedMap[part];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getInkContainerMap(container: any[]): Record<string, unknown> | null {
|
||||
for (let index = container.length - 1; index >= 0; index -= 1) {
|
||||
const item = container[index];
|
||||
if (this.isNamedContainerMap(item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isNamedContainerMap(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user