diff --git a/CLIENT_TODO.md b/CLIENT_TODO.md
index 87b9f74..7d4a0c9 100644
--- a/CLIENT_TODO.md
+++ b/CLIENT_TODO.md
@@ -40,6 +40,7 @@ Required module rules:
- Every app module declares all required dependencies in its `dependencies` list.
- The loader loads module scripts, resolves the dependency graph, initializes modules in dependency order, awaits async initialization, and only then hides the loading overlay.
- Modules must rely on the loader for dependency readiness. Do not add fallback paths for missing dependencies inside modules.
+- Do not add fallback code that bypasses an authoritative module, service, parser, state store, or API to hide an architectural problem. If such a fallback already exists or seems tempting, stop and report the architectural mismatch before changing code.
- Module states are `PENDING`, `LOADING`, `WAITING`, `INITIALIZING`, `FINISHED`, and `ERROR`.
- Modules must report real state transitions. A module must not report `FINISHED` until its critical initialization is actually ready.
- `setTimeout` must not be used to paper over dependency or async ordering bugs. It is acceptable inside isolated scheduling systems such as animation timing, debounce, throttle, or browser rendering workarounds when documented by context.
diff --git a/NOTE.md b/NOTE.md
new file mode 100644
index 0000000..30f2627
--- /dev/null
+++ b/NOTE.md
@@ -0,0 +1,24 @@
+Now let's fix the history scrolling:
+1.) Make sure
does not scroll! This should have no scrollbar or scrolling css whatsoever. It's fixed size. The #story_scrollbar should have a fixed height of 100% of #page_right and be placed absolutely over the parent, so it cannot be influenced by anything next or below it and it does not let clicks bubble through it to anything else below (no unpausing story on scrollbar usage). Neither #story or anything inside it should have a scrollbar!
+2.) Every text or image block that is inserted into the dom should update it's entry in the historyDB to include it's height in lines. Every time this happens the height is added to a game engine value that stores the length of the game history in lines. The #page_rigiht is considered to have a fixed height in lines (calculate id). The #story_scrollbar is calculated using the position and size in lines of the current page view to the game history. For this calculation the history ends at the latestRenderedBlockId, not the latestBlockId.
+3.) When a new block is put into the dom, the #story div is moved up (dynamically with an ease in/ease out animation) within the #page_rigt div until its bottom edge aligns with the bottom edge of it's parent (that's scrolling to the bottom). Any time this position changes the scrollbar is updated accordingly.
+4.) Scrolling using the arrow up or arrow down keys or the mousewheel moves the pane smoothly up and down until it's end. There should be a value storing the block currently shown in the center of the page. The system dynamically adds and removes blocks to the top and bottom of the #paragraphs list so that there are 20 blocks above and 20 blocks below the central block (if available) at all times.
+5.) Any kind of manual scrolling, be it via mousewheel, arrow keys up/down, or using the scrollbar immediately disables autoplay.
+6.) Inserting images should first insert the image covered, then scroll down, and only once the scrolling animation finished start the revealing fade in of the image which should take 2 seconds.
+
+
+Scrolling using the arrow keys or mousewheel works fine now, But I found the following bugs already:
+1.) Autoplay does not disable on scrolling.
+2.) Restoring the chapter beginning shows the chapter heading as a normal line of text and no longer produces the drop-cap.
+3.) Loading a game that was saved while choices were present does not restore the choice dialog.
+4.) The game now seems to always scroll to the center of the page to add/animate in new content. Please keep this position at the page bottom as it was, only adding as much space as needed for the new content (like before).
+5.) Make sure autoscrolling or manual scrolling always stops at the nearest line boundary, so no cut of lines can be visible.
+6.) Suggest a plan how to fix the scrollbar controls. They look correct now. But moving them with the mouse feels clunky and somethimes it triggers strange behavior. We need a solution where the bar can be moved freele up and down without doing anything yet. Once released the content between the current position and the target position including the necessary margins are loaded before the scrolling is animated and then the now unneccessary blocks unloaded.
+
+
+That sounds still too complicated! Why invalidate the layout index?
+Assume the following:
+1.) The #right_page div has a size relative to the window. There is ONE line height value, which is a divisor or the page height/the page height has a fixed number of lines: Line height = Page height/fixed number of lines.
+2.) All content has an exact multiple of line height as height all margins and paddings included.
+3.) Therefore any coordinates or pixel sizes of the virtual content pane can be derived mathematically from line coordinates.
+4.) Scrolling means translating the content vertically (with ease in/eas out animation) to the closest position where the page edges aligns with line edges.
diff --git a/data/ink-src/kaiserpunk.ink b/data/ink-src/kaiserpunk.ink
index fbef562..e57d934 100644
--- a/data/ink-src/kaiserpunk.ink
+++ b/data/ink-src/kaiserpunk.ink
@@ -1,15 +1,12 @@
-// Kaiserpunk Horror Intro
-// Ink source file draft
-// Covers: opening train journey, class selection, name builder, baggage definition,
-// mirror/self-description, supernatural stance, spiritual-sense selection,
-// first Viktor relationship choices, and arrival at Eibenreith.
+// Eibenreith: A Kaiserpunk Adventure
+// Ink intro draft
+// Compile-safe revision: protagonist speech only appears through explicit choices.
VAR birth_class = "unset"
VAR title_part = ""
VAR given_names = ""
VAR common_name = ""
VAR surname = ""
-VAR full_name = ""
VAR baggage_style = "unset"
VAR hair_detail = "unset"
@@ -17,9 +14,9 @@ VAR complexion_detail = "unset"
VAR face_detail = "unset"
VAR outfit_detail = "unset"
-VAR supernatural_belief = "unset" // believer, sceptic, performer, undecided
-VAR supernatural_senses = "unset" // genuine, faked, repressed, ambiguous
-VAR viktor_relation = "unset" // trust, tension, utility, dependence, provocation
+VAR supernatural_belief = "unset"
+VAR supernatural_senses = "unset"
+VAR viktor_relation = "unset"
VAR lover = 0
VAR sapphic = 0
@@ -65,28 +62,37 @@ The form of address is technically correct if you are noble, excessive if you ar
You look around the compartment before you answer. The answer comes from somewhere older than the letter in your reticule. It comes from the place you began.
-* [The compartment seems built for people who never wonder whether they belong in it.] #class:noble
+* [The compartment seems built for people who never wonder whether they belong in it.]
+ #class:noble
~ birth_class = "noble"
~ class_confidence += 2
~ court_loyalty += 1
+ The compartment seems built for people who never wonder whether they belong in it.
+
It is not luxury that unsettles you. Luxury is only wood, cloth, brass, service, silence. What matters is whether the servants glance twice, whether the guard lowers his voice, whether another passenger weighs your gloves and decides not to ask your business.
You were born among people who understood such things before they understood kindness.
-> class_noble_background
-* [You count the cost of each detail before you can stop yourself.] #class:middle
+* [You count the cost of each detail before you can stop yourself.]
+ #class:middle
~ birth_class = "middle"
~ class_confidence += 1
+ You count the cost of each detail before you can stop yourself.
+
The upholstery, the lamps, the polished veneer, the quiet attendance at stations: none of it is magical. It is paid for. Accounted for. Itemised somewhere by someone with ink on his cuffs and a wife who knows how long candles may be burned before the household budget complains.
You were not born to this compartment, but you were born close enough to study its rules.
-> class_middle_background
-* [You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.] #class:working
+* [You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.]
+ #class:working
~ birth_class = "working"
~ class_confidence -= 1
+ You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.
+
The velvet looks soft enough to swallow fingerprints. The brass fittings have been polished by hands that will never sit here. The little curtain strap is worn where other travellers, all of them more certain than you, have touched it without gratitude.
You were not born on this side of service.
@@ -97,75 +103,121 @@ You look around the compartment before you answer. The answer comes from somewhe
Viktor waits for the answer owed to his remark. The train jolts once, then settles again into its hard, confident rhythm.
-* "Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut."
+* ["Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut."]
+ #route:eccentric
~ eccentric += 1
+ "Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut."
+
His brows move almost imperceptibly.
"Then good breeding has military applications," he says.
- * "Only when properly commanded."
- ~ lover += 1
- ~ viktor_suspicion += 1
- "You intend to command it yourself?"
+ -> class_noble_followup_breeding
- "Whenever possible."
-
- * "Only when men mistake silence for obedience."
- ~ sapphic += 1
- ~ viktor_suspicion += 1
- "That is an ambitious distinction."
-
- "It has preserved many women from being understood too early."
-
- * "I prefer any discipline that leaves a clean record."
- ~ detective += 1
- ~ viktor_trust += 1
- "A useful preference," he says. "If sincere."
-
- "If it is not sincere, I have stated it early enough for you to investigate."
-
- -
- -> class_noble_explanation
-
-* "You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes."
+* ["You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes."]
+ #route:detective
~ detective += 1
~ viktor_trust += 1
+ "You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes."
+
"A family education, then."
- * "A family sentence, more often."
- ~ eccentric += 1
- "You speak as if birth were a prison."
+ -> class_noble_followup_family
- "Only when it is furnished well enough for visitors to admire."
-
- * "An education in rooms where every chair has rank."
- ~ class_confidence += 1
- "Then Hohenreith may not surprise you."
-
- "That depends on whether they have better chairs than secrets."
-
- -
- -> class_noble_explanation
-
-* "If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers."
+* ["If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers."]
+ #route:lover
~ lover += 1
~ viktor_suspicion += 1
+ "If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers."
+
The corner of his mouth changes almost too little to notice.
"Officers are less easily bored."
- * "Or less honest about it."
- ~ lover += 1
- "You accuse the army of vanity."
+ -> class_noble_followup_officers
- "No. I accuse men of consistency."
+=== class_noble_followup_breeding ===
- * "Then I must try not to disappoint the army."
- ~ careless += 1
- "That is precisely what I have been asked to prevent."
+* ["Only when properly commanded."]
+ #route:lover
+ ~ lover += 1
+ ~ viktor_suspicion += 1
+ "Only when properly commanded."
- -
- -> class_noble_explanation
+ "You intend to command it yourself?"
+
+ The answer remains in the angle of your glove and the calmness of your stare.
+
+ -> class_noble_explanation
+
+* ["Only when men mistake silence for obedience."]
+ #route:sapphic
+ ~ sapphic += 1
+ ~ viktor_suspicion += 1
+ "Only when men mistake silence for obedience."
+
+ "That is an ambitious distinction."
+
+ The distinction has preserved many women from being understood too early.
+
+ -> class_noble_explanation
+
+* ["I prefer any discipline that leaves a clean record."]
+ #route:detective
+ ~ detective += 1
+ ~ viktor_trust += 1
+ "I prefer any discipline that leaves a clean record."
+
+ "A useful preference," he says. "If sincere."
+
+ The conditional nature of sincerity remains, for the moment, his problem to investigate.
+
+ -> class_noble_explanation
+
+=== class_noble_followup_family ===
+
+* ["A family sentence, more often."]
+ #route:eccentric
+ ~ eccentric += 1
+ "A family sentence, more often."
+
+ "You speak as if birth were a prison."
+
+ The polished furniture answers him better than an argument could.
+
+ -> class_noble_explanation
+
+* ["An education in rooms where every chair has rank."]
+ ~ class_confidence += 1
+ "An education in rooms where every chair has rank."
+
+ "Then Hohenreith may not surprise you."
+
+ The possibility that Hohenreith may possess better secrets than chairs is allowed to stand.
+
+ -> class_noble_explanation
+
+=== class_noble_followup_officers ===
+
+* ["Or less honest about it."]
+ #route:lover
+ ~ lover += 1
+ "Or less honest about it."
+
+ "You accuse the army of vanity."
+
+ The accusation of consistency is the least deniable one.
+
+ -> class_noble_explanation
+
+* ["Then I must try not to disappoint the army."]
+ #route:careless
+ ~ careless += 1
+ "Then I must try not to disappoint the army."
+
+ "That is precisely what I have been asked to prevent."
+
+ -> class_noble_explanation
=== class_noble_explanation ===
@@ -177,7 +229,7 @@ Your reputation as a medium did not descend from heaven. It was assembled from h
Before the court could use you, society had first to invent you.
-Now choose the name by which Vienna invented you.
+Vienna knew you by the name society had made useful.
-> choose_name_noble
@@ -185,69 +237,111 @@ Now choose the name by which Vienna invented you.
Viktor's remark lingers between you with the odour of a polite accusation.
-* "Restraint is easier when one has learned that every mistake is remembered by someone better placed."
+* ["Restraint is easier when one has learned that every mistake is remembered by someone better placed."]
+ #route:detective
~ detective += 1
~ viktor_trust += 1
+ "Restraint is easier when one has learned that every mistake is remembered by someone better placed."
+
Viktor watches you more closely.
"A bitter lesson."
- * "A useful one. Bitterness is merely the taste left by instruction."
- ~ eccentric += 1
- "You collect phrases like weapons."
+ -> class_middle_followup_bitter
- "Only the light ones. Heavy weapons attract permits."
-
- * "A common one. Some people only notice injustice when it reaches their own floor."
- ~ sapphic += 1
- "You have made a study of floors?"
-
- "Of thresholds. They are more honest."
-
- -
- -> class_middle_explanation
-
-* "If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence."
+* ["If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence."]
+ #route:lover
~ lover += 1
~ medium_reputation += 1
+ "If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence."
+
"A method?"
- * "A courtesy. I let them begin with their favorite subject."
- ~ lover += 1
- "Themselves."
+ -> class_middle_followup_method
- "You understand the principle already."
-
- * "An experiment. It has produced reliable results."
- ~ detective += 1
- "Then I am part of your experiment."
-
- "Naturally. You sat opposite me."
-
- -
- -> class_middle_explanation
-
-* "I was considering whether your concern is official, personal, or merely masculine."
+* ["I was considering whether your concern is official, personal, or merely masculine."]
+ #route:eccentric
~ eccentric += 1
~ viktor_suspicion += 1
+ "I was considering whether your concern is official, personal, or merely masculine."
+
His eyes harden by one exact degree.
"Today it is official."
- * "How convenient. The other two may deny responsibility."
- ~ eccentric += 1
- "I advise you not to make wit your first instrument at Hohenreith."
+ -> class_middle_followup_official
- "Then I shall make it the second."
+=== class_middle_followup_bitter ===
- * "Then I shall treat it with the respect due to paper."
- ~ detective += 1
- "Paper has moved armies."
+* ["A useful one. Bitterness is merely the taste left by instruction."]
+ #route:eccentric
+ ~ eccentric += 1
+ "A useful one. Bitterness is merely the taste left by instruction."
- "And buried mistakes."
+ "You collect phrases like weapons."
- -
- -> class_middle_explanation
+ The phrase lies lightly enough that he must decide whether it is ornament or weapon.
+
+ -> class_middle_explanation
+
+* ["A common one. Some people only notice injustice when it reaches their own floor."]
+ #route:sapphic
+ ~ sapphic += 1
+ "A common one. Some people only notice injustice when it reaches their own floor."
+
+ "You have made a study of floors?"
+
+ Thresholds, you have learned, are more honest than floors; they admit that passage is a privilege.
+
+ -> class_middle_explanation
+
+=== class_middle_followup_method ===
+
+* ["A courtesy. I let them begin with their favorite subject."]
+ #route:lover
+ ~ lover += 1
+ "A courtesy. I let them begin with their favorite subject."
+
+ "Themselves."
+
+ His own answer completes the principle neatly enough that further instruction would be vanity.
+
+ -> class_middle_explanation
+
+* ["An experiment. It has produced reliable results."]
+ #route:detective
+ ~ detective += 1
+ "An experiment. It has produced reliable results."
+
+ "Then I am part of your experiment."
+
+ He has sat opposite you long enough to become evidence.
+
+ -> class_middle_explanation
+
+=== class_middle_followup_official ===
+
+* ["How convenient. The other two may deny responsibility."]
+ #route:eccentric
+ ~ eccentric += 1
+ "How convenient. The other two may deny responsibility."
+
+ "I advise you not to make wit your first instrument at Hohenreith."
+
+ The demotion of wit to second instrument remains sufficiently theoretical to be safe.
+
+ -> class_middle_explanation
+
+* ["Then I shall treat it with the respect due to paper."]
+ #route:detective
+ ~ detective += 1
+ "Then I shall treat it with the respect due to paper."
+
+ "Paper has moved armies."
+
+ Paper has also buried mistakes, but not all corrections need to be spoken aloud.
+
+ -> class_middle_explanation
=== class_middle_explanation ===
@@ -259,7 +353,7 @@ The court does not like to admit that it needs middle-class competence. It prefe
Your reputation as a medium gave them a word that sounded less dangerous than investigator.
-Now choose the name under which you entered the salons that first laughed at you, then invited you back.
+The salons that first laughed at you, then invited you back, learned your name before they learned what it cost.
-> choose_name_middle
@@ -267,61 +361,103 @@ Now choose the name under which you entered the salons that first laughed at you
Viktor's courtesy is smooth enough to be handled without fingerprints. You hear, beneath it, the question of how much this compartment has improved you.
-* "Restraint is what people praise when they prefer not to see the effort."
+* ["Restraint is what people praise when they prefer not to see the effort."]
+ #route:detective
~ detective += 1
~ viktor_trust += 1
+ "Restraint is what people praise when they prefer not to see the effort."
+
The newspaper in Viktor's hand creases once.
"You object to being praised?"
- * "Only cheaply."
- ~ eccentric += 1
- "That may be difficult to avoid."
+ -> class_working_followup_praise
- "Then Hohenreith will have to spend more."
-
- * "Only when it hides the person doing the work."
- ~ sapphic += 1
- He studies you as if the answer has come from farther down the train than first class.
-
- -
- -> class_working_explanation
-
-* "I am quiet because people often prefer women of my origin either grateful or invisible. I am deciding which will inconvenience you less."
+* ["I am quiet because people often prefer women of my origin either grateful or invisible."]
+ #route:eccentric
~ eccentric += 1
~ viktor_suspicion += 1
+ "I am quiet because people often prefer women of my origin either grateful or invisible."
+
"I did not ask you to be grateful."
- * "No. You asked me to be manageable."
- ~ eccentric += 1
- "I asked you nothing of the kind."
+ -> class_working_followup_grateful
- "That is the benefit of rank. It asks through furniture."
-
- * "Then I shall postpone gratitude until you deserve it."
- ~ lover += 1
- A pause. Then, very dryly: "A generous arrangement."
-
- -
- -> class_working_explanation
-
-* "I was trying not to touch the upholstery as though it might accuse me."
+* ["I was trying not to touch the upholstery as though it might accuse me."]
+ #route:careless
~ careless += 1
~ viktor_relation = "dependence"
+ "I was trying not to touch the upholstery as though it might accuse me."
+
Something like concern crosses his face, disguised too late as irritation.
"The upholstery has survived ministers. It will survive you."
- * "Then I am already braver than I was."
- ~ careless += 1
- "Courage measured against upholstery is not a military standard."
+ -> class_working_followup_upholstery
- * "I shall take that as reassurance, though you delivered it like a reprimand."
- ~ lover += 1
- "I have had practice with both."
+=== class_working_followup_praise ===
- -
- -> class_working_explanation
+* ["Only cheaply."]
+ #route:eccentric
+ ~ eccentric += 1
+ "Only cheaply."
+
+ "That may be difficult to avoid."
+
+ Hohenreith, if it intends to praise you cheaply, will have to discover the economy of disappointment.
+
+ -> class_working_explanation
+
+* ["Only when it hides the person doing the work."]
+ #route:sapphic
+ ~ sapphic += 1
+ "Only when it hides the person doing the work."
+
+ He studies you as if the answer has come from farther down the train than first class.
+
+ -> class_working_explanation
+
+=== class_working_followup_grateful ===
+
+* ["No. You asked me to be manageable."]
+ #route:eccentric
+ ~ eccentric += 1
+ "No. You asked me to be manageable."
+
+ "I asked you nothing of the kind."
+
+ Rank, unlike men, rarely needs to ask directly. The furniture asks for it.
+
+ -> class_working_explanation
+
+* ["Then I shall postpone gratitude until you deserve it."]
+ #route:lover
+ ~ lover += 1
+ "Then I shall postpone gratitude until you deserve it."
+
+ A pause. Then, very dryly: "A generous arrangement."
+
+ -> class_working_explanation
+
+=== class_working_followup_upholstery ===
+
+* ["Then I am already braver than I was."]
+ #route:careless
+ ~ careless += 1
+ "Then I am already braver than I was."
+
+ "Courage measured against upholstery is not a military standard."
+
+ -> class_working_explanation
+
+* ["I shall take that as reassurance, though you delivered it like a reprimand."]
+ #route:lover
+ ~ lover += 1
+ "I shall take that as reassurance, though you delivered it like a reprimand."
+
+ "I have had practice with both."
+
+ -> class_working_explanation
=== class_working_explanation ===
@@ -335,15 +471,12 @@ You rose by talent, patronage, imitation, nerve, and the terrible convenience of
The court has placed you in first class because it needs what birth did not give you.
-Now choose the name you carried upward, altered perhaps in pronunciation, never quite cleansed of where it began.
+The name you carried upward was altered perhaps in pronunciation, never quite cleansed of where it began.
-> choose_name_working
=== choose_name_noble ===
-// Noble title is fixed for the canonical lower-noble route.
-// Full names are formal; common_name is used for narration and dialogue.
-
* [Valerie Eleonore Josepha]
~ given_names = "Valerie Eleonore Josepha"
~ common_name = "Valerie"
@@ -555,15 +688,9 @@ A simple name can be a burden in Vienna. It tells people how little they must pr
=== assemble_full_name ===
{birth_class == "noble":
- ~ full_name = given_names + " " + title_part + " " + surname
+ On visiting cards, in letters, in the cautious mouths of servants, you are {given_names} {title_part} {surname}.
- else:
- ~ full_name = title_part + " " + given_names + " " + surname
-}
-
-{birth_class == "noble":
- On visiting cards, in letters, in the cautious mouths of servants, you are {full_name}.
-- else:
- On railway documents, hotel ledgers, and the tongues of people who have not yet decided how much respect you deserve, you are {full_name}.
+ On railway documents, hotel ledgers, and the tongues of people who have not yet decided how much respect you deserve, you are {title_part} {given_names} {surname}.
}
But in the private chamber where a name is first answered before it is performed, you are {common_name}.
@@ -578,7 +705,7 @@ When the mountains return, they seem closer.
The black glass gives you the woman who will arrive in Eibenreith before any rumour has time to improve her.
-Choose what the window catches first.
+The window catches one thing before the rest.
* [Dark ash-brown hair pinned with almost severe care.]
~ hair_detail = "dark ash-brown"
@@ -682,36 +809,49 @@ The comital family at Jagdhaus Hohenreith has asked for discretion. Vienna has a
Before this journey, before this train, before the mountains began taking the sky piece by piece, belief had already taken its position in you.
-* [The dead are not silent. The living are merely poor listeners.] #supernatural:believer
+* [The dead are not silent. The living are merely poor listeners.]
+ #supernatural:believer
~ supernatural_belief = "believer"
~ medium_reputation += 1
~ supernatural_exposure += 1
+ The dead are not silent. The living are merely poor listeners.
+
You have always thought disbelief a provincial arrogance of the educated. There are pressures in rooms where grief has been. There are words people speak before they know they have spoken. There are dreams that arrive with mud on their hems.
Perhaps the world is not haunted. Perhaps it is simply crowded.
-> spiritual_senses
-* [The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.] #supernatural:sceptic #route:detective
+* [The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.]
+ #supernatural:sceptic
+ #route:detective
~ supernatural_belief = "sceptic"
~ detective += 1
+ The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.
+
The word spirit covers too much and explains too little. You have watched respectable people call an echo a message, a coincidence a sign, a trembling hand an angelic visitation. Men of science can be fools, but fools with candles and planchettes are no improvement.
If Hohenreith has ghosts, you expect them to keep accounts, write letters, leave footprints, and benefit someone.
-> spiritual_senses
-* [Belief is a costume. You wear it because men insist on dressing you in it.] #supernatural:performer
+* [Belief is a costume. You wear it because men insist on dressing you in it.]
+ #supernatural:performer
~ supernatural_belief = "performer"
~ medium_reputation += 2
+ Belief is a costume. You wear it because men insist on dressing you in it.
+
You discovered early that men who distrust a woman's mind will sometimes worship her nerves. A conclusion from evidence irritates them. A vision, sighed through lowered lashes, makes them lean closer.
Very well. Let them lean.
-> spiritual_senses
-* [You have learned not to decide too early.] #supernatural:undecided
+* [You have learned not to decide too early.]
+ #supernatural:undecided
~ supernatural_belief = "undecided"
+ You have learned not to decide too early.
+
There are things you can explain, things you cannot yet explain, and things that explanation damages before it helps. You have made a profession of standing at thresholds with a face composed enough for both sides to continue speaking.
Hohenreith will have to show you what kind of case it is.
@@ -726,36 +866,50 @@ People call a woman sensitive when they want her perceptions to sound like an il
Beneath reputation and performance, memory has its own testimony.
-* [There have been moments you cannot explain away.] #powers:genuine
+* [There have been moments you cannot explain away.]
+ #powers:genuine
~ supernatural_senses = "genuine"
~ supernatural_exposure += 2
+ There have been moments you cannot explain away.
+
Once, as a child, you knew before the telegram came. Once, in a crowded room, a stranger's grief entered you with such force that your own knees failed. Once, in a mirror, you saw a door behind you that was not in the room when you turned.
You learned caution after that. It is unwise for a woman to know things before a man has asked her opinion.
-> viktor_first_exchange
-* [Everything you do can be explained by observation, timing, and nerve.] #powers:faked #route:detective
+* [Everything you do can be explained by observation, timing, and nerve.]
+ #powers:faked
+ #route:detective
~ supernatural_senses = "faked"
~ detective += 1
+ Everything you do can be explained by observation, timing, and nerve.
+
You notice rings removed too recently, mourning gloves worn too carefully, letters folded and refolded until the crease gives away the reader's obsession. You hear servants misname guests, mothers pause before daughters' rooms, officers lie by becoming too exact.
The dead have never told you anything. The living cannot stop telling you everything.
-> viktor_first_exchange
-* [Something happens, but never when summoned.] #powers:ambiguous
+* [Something happens, but never when summoned.]
+ #powers:ambiguous
~ supernatural_senses = "ambiguous"
~ supernatural_exposure += 1
+ Something happens, but never when summoned.
+
Your reputation depends upon command. The truth, if truth it is, has no respect for appointments.
Sometimes a room changes pressure around you. Sometimes a face acquires an old expression no living person taught it. Sometimes names arrive before introductions. But the harder you reach, the more ordinary the world becomes.
-> viktor_first_exchange
-* [You buried the first signs so thoroughly that even you do not know what remains.] #powers:repressed #route:eccentric
+* [You buried the first signs so thoroughly that even you do not know what remains.]
+ #powers:repressed
+ #route:eccentric
~ supernatural_senses = "repressed"
~ eccentric += 1
+ You buried the first signs so thoroughly that even you do not know what remains.
+
There are childhood memories sealed behind politeness: a nursery mirror turned to the wall, a nurse dismissed without reference, your mother's hand tightening around your wrist until the bones complained.
You became strange afterward in ways society found easier to admire than understand.
@@ -770,162 +924,246 @@ Viktor opens a leather folder and removes a memorandum. He does not hand it to y
"When we leave the railway," he says, "we will be met by a coach from Hohenreith. From that moment, appearances matter. Your hosts have been told that I assist with correspondence, travel, and practical arrangements. They need not be troubled with military definitions."
-* "And the villagers?"
+* ["And the villagers?"]
+ "And the villagers?"
+
"The villagers need not be troubled with anything."
There it is: the empire in miniature. A man, a folder, a locked sentence.
-* "How merciful. The empire has spared them vocabulary."
+ -> viktor_restraint_advice
+
+* ["How merciful. The empire has spared them vocabulary."]
+ #route:eccentric
~ eccentric += 1
~ viktor_suspicion += 1
+ "How merciful. The empire has spared them vocabulary."
+
"The empire has spared them alarm," Viktor says.
- "It often uses the two interchangeably."
+ The empire's habit of confusing ignorance with calm does not require stating to remain present.
- There it is: the empire in miniature. A man, a folder, a locked sentence.
+ -> viktor_restraint_advice
-* "You mean they are not to know whether I am guest, tool, or warning."
+* ["You mean they are not to know whether I am guest, tool, or warning."]
+ #route:detective
~ detective += 1
~ viktor_trust += 1
+ "You mean they are not to know whether I am guest, tool, or warning."
+
"I mean they are to know only what steadies the situation."
- "That is not a contradiction."
+ The answer steadies nothing; that is not a contradiction.
- There it is: the empire in miniature. A man, a folder, a locked sentence.
+ -> viktor_restraint_advice
--
+=== viktor_restraint_advice ===
"You will be addressed according to the station you present," he continues. "The Graf's household will observe rank. Servants will observe what the household observes. Villagers may observe less and remember more. I advise restraint."
The advice is sound. That makes it no less irritating.
-* "If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods." #route:lover
+* ["If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods."]
+ #route:lover
~ lover += 1
~ viktor_relation = "provocation"
~ viktor_trust -= 1
~ viktor_suspicion += 1
+ "If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods."
+
For the first time, amusement almost reaches his mouth.
"A dangerous doctrine."
- * "A practical one."
- "You intend to practice it at Hohenreith?"
+ -> viktor_lover_followup
- * "Only where patriotism requires sacrifice."
- ~ lover += 1
- He looks down at the memorandum, but not quickly enough to conceal that he is reassessing you.
-
- * "Only where men mistake desire for judgement."
- ~ lover += 1
- "That may include more territory than the maps admit."
-
- -
- -> viktor_explains_orders
-
- * "Dangerous doctrines travel best in good gloves."
- ~ eccentric += 1
- "You intend to charm Hohenreith into confession?"
-
- "Only if Hohenreith insists on being charmed."
-
- -> viktor_explains_orders
-
-* "If you wish me to pass as harmless, you must stop warning me like a gaoler." #route:sapphic
+* ["If you wish me to pass as harmless, you must stop warning me like a gaoler."]
+ #route:sapphic
~ sapphic += 1
~ viktor_relation = "tension"
~ viktor_suspicion += 1
+ "If you wish me to pass as harmless, you must stop warning me like a gaoler."
+
His gaze sharpens.
"I am not your gaoler."
- * "No. A gaoler is at least honest about the key."
- The words surprise you by leaving a mark. Not on him, perhaps. On yourself. The closer the train carries you to Amalia's world, though you do not yet know her face, the more intolerable it seems that every female life there might be guarded by men who call the guarding concern.
+ -> viktor_sapphic_followup
- * "Then do not stand between me and every locked door before I have touched the handle."
- ~ viktor_trust += 1
- "Some doors are locked for cause."
-
- "Then the cause had better survive examination."
-
- -
- Viktor folds the memorandum once, precisely.
-
- -> viktor_explains_orders
-
-* "Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?" #route:detective
+* ["Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?"]
+ #route:detective
~ detective += 1
~ viktor_relation = "professional"
~ viktor_trust += 1
+ "Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?"
+
He gives the smallest nod, as if you have chosen the only answer fit for adults.
"They know that you come recommended. They suspect that you may be able to settle disturbances without police, priest, or press. You are permitted to verify fraud, coercion, threat to public order, or credible phenomena not presently classifiable."
- * "Credible phenomena not presently classifiable."
- "That is the phrase."
+ -> viktor_detective_followup
- "A bureaucratic ghost."
-
- "The safest kind."
-
- * "And if the phenomena become classifiable?"
- "Then we classify them before others do."
-
- "Spoken like a man who has seen reports bury graves."
-
- -
- -> viktor_explains_orders
-
-* "I shall do my best not to faint unless it is useful." #route:careless
+* ["I shall do my best not to faint unless it is useful."]
+ #route:careless
~ careless += 1
~ viktor_relation = "dependence"
~ viktor_trust -= 1
+ "I shall do my best not to faint unless it is useful."
+
Something in his expression tightens; not contempt exactly, but readiness.
"I would prefer you did not faint at all."
- * "How ungallant."
- "How practical."
+ -> viktor_careless_followup
- "Then you must be practical for both of us. I have never trusted the floor in strange houses."
-
- His answer is delayed by half a breath.
-
- "That, gnädiges Fräulein, is precisely what concerns me."
-
- * "Then you must remain close enough to catch me."
- ~ lover += 1
- "My orders did not specify theatrical collapses."
-
- "How careless of them."
-
- -
- -> viktor_explains_orders
-
-* "Restraint is what timid people call obedience after they have forgotten who trained them." #route:eccentric
+* ["Restraint is what timid people call obedience after they have forgotten who trained them."]
+ #route:eccentric
~ eccentric += 1
~ viktor_relation = "challenge"
~ viktor_suspicion += 2
+ "Restraint is what timid people call obedience after they have forgotten who trained them."
+
Viktor studies you as he might study an unfamiliar weapon found in luggage.
"You enjoy making enemies."
- * "No. I dislike the laziness of letting fools remain undecided."
- "At Hohenreith, that dislike may become expensive."
+ -> viktor_eccentric_followup
- "Then the Graf should have invited someone cheaper."
+=== viktor_lover_followup ===
- * "Enemies are merely people honest enough to stand in the right place."
- "You speak as though conflict were housekeeping."
+* ["A practical one."]
+ "A practical one."
- "It is. One discovers what belongs where."
+ "You intend to practice it at Hohenreith?"
- -
- The wheels strike a curve. The compartment leans. For a moment the two of you are held in the same narrow imbalance.
+ -> viktor_lover_second
- -> viktor_explains_orders
+* ["Dangerous doctrines travel best in good gloves."]
+ #route:eccentric
+ ~ eccentric += 1
+ "Dangerous doctrines travel best in good gloves."
+
+ "You intend to charm Hohenreith into confession?"
+
+ If Hohenreith insists on being charmed, it will hardly be your fault.
+
+ -> viktor_explains_orders
+
+=== viktor_lover_second ===
+
+* ["Only where patriotism requires sacrifice."]
+ #route:lover
+ ~ lover += 1
+ "Only where patriotism requires sacrifice."
+
+ He looks down at the memorandum, but not quickly enough to conceal that he is reassessing you.
+
+ -> viktor_explains_orders
+
+* ["Only where men mistake desire for judgement."]
+ #route:lover
+ ~ lover += 1
+ "Only where men mistake desire for judgement."
+
+ "That may include more territory than the maps admit."
+
+ -> viktor_explains_orders
+
+=== viktor_sapphic_followup ===
+
+* ["No. A gaoler is at least honest about the key."]
+ "No. A gaoler is at least honest about the key."
+
+ The words surprise you by leaving a mark. Not on him, perhaps. On yourself. The closer the train carries you to Amalia's world, though you do not yet know her face, the more intolerable it seems that every female life there might be guarded by men who call the guarding concern.
+
+ Viktor folds the memorandum once, precisely.
+
+ -> viktor_explains_orders
+
+* ["Then do not stand between me and every locked door before I have touched the handle."]
+ ~ viktor_trust += 1
+ "Then do not stand between me and every locked door before I have touched the handle."
+
+ "Some doors are locked for cause."
+
+ Any cause worthy of the lock should survive the indignity of being examined.
+
+ Viktor folds the memorandum once, precisely.
+
+ -> viktor_explains_orders
+
+=== viktor_detective_followup ===
+
+* ["Credible phenomena not presently classifiable."]
+ "Credible phenomena not presently classifiable."
+
+ "That is the phrase."
+
+ The phrase files itself in your mind as a bureaucratic ghost.
+
+ "The safest kind," he says.
+
+ -> viktor_explains_orders
+
+* ["And if the phenomena become classifiable?"]
+ "And if the phenomena become classifiable?"
+
+ "Then we classify them before others do."
+
+ The sentence has the chill of a report written quickly over a grave.
+
+ -> viktor_explains_orders
+
+=== viktor_careless_followup ===
+
+* ["How ungallant."]
+ "How ungallant."
+
+ "How practical."
+
+ The burden of practicality passes toward him as gracefully as a fainting couch dragged into a field hospital.
+
+ His answer is delayed by half a breath.
+
+ "That, gnädiges Fräulein, is precisely what concerns me."
+
+ -> viktor_explains_orders
+
+* ["Then you must remain close enough to catch me."]
+ #route:lover
+ ~ lover += 1
+ "Then you must remain close enough to catch me."
+
+ "My orders did not specify theatrical collapses."
+
+ The omission does not flatter the thoroughness of his superiors.
+
+ -> viktor_explains_orders
+
+=== viktor_eccentric_followup ===
+
+* ["No. I dislike the laziness of letting fools remain undecided."]
+ "No. I dislike the laziness of letting fools remain undecided."
+
+ "At Hohenreith, that dislike may become expensive."
+
+ If the Graf wanted docility, he could have invited someone cheaper.
+
+ -> viktor_explains_orders
+
+* ["Enemies are merely people honest enough to stand in the right place."]
+ "Enemies are merely people honest enough to stand in the right place."
+
+ "You speak as though conflict were housekeeping."
+
+ Conflict has always been a form of housekeeping; one discovers what belongs where.
+
+ -> viktor_explains_orders
=== viktor_explains_orders ===
+The wheels strike a curve. The compartment leans. For a moment the two of you are held in the same narrow imbalance.
+
Viktor gives you the memorandum at last.
The document is not long. That is part of its menace. Long documents invite argument; short ones carry authority.
@@ -940,53 +1178,110 @@ No one has written the word daughter.
Yet the omissions arrange themselves around the page like furniture around a corpse.
-* "There is another instruction."
+* ["There is another instruction."]
+ "There is another instruction."
+
Viktor does not ask how you know.
"There is always another instruction," he says.
-* "Your version is shorter than your silence. That means there is another instruction."
+ -> viktor_second_instruction
+
+* ["Your version is shorter than your silence. That means there is another instruction."]
+ #route:detective
~ detective += 1
~ viktor_trust += 1
+ "Your version is shorter than your silence. That means there is another instruction."
+
Viktor does not ask how you know.
"There is always another instruction," he says.
-* "How touching. Vienna trusts us both so little it had to divide the distrust."
+ -> viktor_second_instruction
+
+* ["How touching. Vienna trusts us both so little it had to divide the distrust."]
+ #route:eccentric
~ eccentric += 1
~ viktor_suspicion += 1
+ "How touching. Vienna trusts us both so little it had to divide the distrust."
+
Viktor does not ask how you know.
"There is always another instruction," he says.
--
+ -> viktor_second_instruction
+
+=== viktor_second_instruction ===
+
+* ["For you."]
+ "For you."
-* "For you."
"Yes."
-* "Concerning me."
+ -> viktor_instruction_concerns
+
+* ["Concerning me."]
+ "Concerning me."
+
"Partly."
-* "Concerning whether I am fraud, fool, or useful animal."
+ -> train_slows
+
+* ["Concerning whether I am fraud, fool, or useful animal."]
~ viktor_suspicion += 1
+ "Concerning whether I am fraud, fool, or useful animal."
+
"Partly," he says, and this time the honesty has a blade in it.
--
+ -> train_slows
+
+=== viktor_instruction_concerns ===
+
+* ["Concerning me."]
+ "Concerning me."
+
+ "Partly."
+
+ -> train_slows
+
+* ["Concerning whether I am fraud, fool, or useful animal."]
+ ~ viktor_suspicion += 1
+ "Concerning whether I am fraud, fool, or useful animal."
+
+ "Partly," he says, and this time the honesty has a blade in it.
+
+ -> train_slows
+
+=== train_slows ===
The train begins to slow. The rhythm changes first in the floor, then in the window, then in the body. Houses gather beside the line. A station roof appears between drifting smoke and the dark combs of forested slopes. #sfx[steam-whistle.ogg]
-* "Then I shall try to be worth the ink."
+* ["Then I shall try to be worth the ink."]
+ "Then I shall try to be worth the ink."
+
"I sincerely hope so."
-* "Then I shall disappoint the instruction as creatively as circumstances permit."
+ -> train_slows_end
+
+* ["Then I shall disappoint the instruction as creatively as circumstances permit."]
+ #route:eccentric
~ eccentric += 1
+ "Then I shall disappoint the instruction as creatively as circumstances permit."
+
"I sincerely hope you do not."
-* "Then keep your second instruction, Herr Nowak. I prefer primary sources."
+ -> train_slows_end
+
+* ["Then keep your second instruction, Herr Nowak. I prefer primary sources."]
+ #route:detective
~ detective += 1
+ "Then keep your second instruction, Herr Nowak. I prefer primary sources."
+
"A preference not always granted in imperial service."
--
+ -> train_slows_end
+
+=== train_slows_end ===
You cannot decide whether his answer is an insult, a prayer, or his first honest sentence.
@@ -1002,32 +1297,32 @@ Your luggage descends in stages.
* [A disciplined official set: trunk, dispatch case, hatbox, and black séance case.]
~ baggage_style = "official"
- First comes a sober travelling trunk with brass corners dulled by use, then a dispatch case, then a hatbox, then the narrow black case whose contents would embarrass both a priest and a conjurer if either searched it without imagination.
~ detective += 1
+ First comes a sober travelling trunk with brass corners dulled by use, then a dispatch case, then a hatbox, then the narrow black case whose contents would embarrass both a priest and a conjurer if either searched it without imagination.
-> station_luggage_common
* [An elegant noblewoman's luggage: too correct to be accidental.]
~ baggage_style = "elegant"
- First comes a large trunk in dark leather, then a second smaller one for linen, then a round hatbox, a fitted toilette case, and a reticule kept too close to your hand for any porter to misunderstand its importance.
~ class_confidence += 1
+ First comes a large trunk in dark leather, then a second smaller one for linen, then a round hatbox, a fitted toilette case, and a reticule kept too close to your hand for any porter to misunderstand its importance.
-> station_luggage_common
-* [A performer’s luggage: harmless on top, less harmless beneath.]
+* [A performer's luggage: harmless on top, less harmless beneath.]
~ baggage_style = "performer"
- First comes a respectable trunk, then a hatbox, then a travelling case of gloves, veils, ribbons, calling cards, and the small objects by which a room may be persuaded to believe in forces already present.
~ medium_reputation += 1
+ First comes a respectable trunk, then a hatbox, then a travelling case of gloves, veils, ribbons, calling cards, and the small objects by which a room may be persuaded to believe in forces already present.
-> station_luggage_common
* [A practical assortment that betrays too much preparation.]
~ baggage_style = "practical"
- First comes a battered trunk reinforced at the corners, then a leather case with notebooks, pencils, folded maps, spare gloves, a hand-lamp, and enough small necessities to offend anyone who prefers women decorative.
~ detective += 1
+ First comes a battered trunk reinforced at the corners, then a leather case with notebooks, pencils, folded maps, spare gloves, a hand-lamp, and enough small necessities to offend anyone who prefers women decorative.
-> station_luggage_common
* [An excessive pile that makes concealment impossible.]
~ baggage_style = "excessive"
- First comes one trunk, then another, then a hatbox, then a rug, then a dressing case, then the narrow black case, then a smaller parcel you had forgotten had survived packing. By the end even Viktor looks faintly outnumbered.
~ careless += 1
+ First comes one trunk, then another, then a hatbox, then a rug, then a dressing case, then the narrow black case, then a smaller parcel you had forgotten had survived packing. By the end even Viktor looks faintly outnumbered.
-> station_luggage_common
=== station_luggage_common ===
@@ -1102,95 +1397,160 @@ Viktor has turned slightly toward the same slope.
"Did you see something?"
-* "A woman in the wood, perhaps. Or a stone that wanted to be one." #route:eccentric #statue_hint
+* ["A woman in the wood, perhaps. Or a stone that wanted to be one."]
+ #route:eccentric
+ #statue_hint
~ eccentric += 1
~ viktor_suspicion += 1
+ "A woman in the wood, perhaps. Or a stone that wanted to be one."
+
He studies the passing trees.
"A local shrine?"
- * "If it is a shrine, it has not been loved recently."
- "You speak as if stones notice neglect."
+ -> statue_eccentric_followup
- "Do soldiers not?"
-
- He does not answer.
-
- * "No. Shrines face the faithful. That thing was listening sideways."
- ~ supernatural_exposure += 1
- Viktor's hand rests on the coach strap, still and ready.
-
- -
- -> coach_nears_village
-
-* "A marker. I would like to know where that path leads." #route:detective #statue_hint
+* ["A marker. I would like to know where that path leads."]
+ #route:detective
+ #statue_hint
~ detective += 1
~ viktor_trust += 1
+ "A marker. I would like to know where that path leads."
+
"You saw a path?"
- * "Not clearly. Enough to ask later."
- Viktor looks back through the small rear window. The bend has already erased the slope.
+ -> statue_detective_followup
- "Ask carefully. Places people fail to mention are often more informative than those they recommend."
-
- * "Only a suggestion of one. If it exists, someone maintains the absence of it."
- ~ detective += 1
- "You make absences sound expensive."
-
- "They usually are."
-
- -
- -> coach_nears_village
-
-* "Only trees. The sort that make one grateful for gentlemen with revolvers." #route:careless
+* ["Only trees. The sort that make one grateful for gentlemen with revolvers."]
+ #route:careless
~ careless += 1
~ viktor_relation = "dependence"
+ "Only trees. The sort that make one grateful for gentlemen with revolvers."
+
His expression darkens by one official degree.
"A revolver is a poor instrument against trees."
- * "Then I shall rely on your conversation to intimidate them."
- The driver pretends not to hear. His shoulders, however, hear everything.
+ -> statue_careless_followup
- * "How unfortunate. You seemed so professionally reassuring."
- ~ lover += 1
- "I prefer enemies that identify themselves."
-
- -
- -> coach_nears_village
-
-* "Would you believe me if I said I had?" #route:lover
+* ["Would you believe me if I said I had?"]
+ #route:lover
~ lover += 1
~ viktor_suspicion += 1
+ "Would you believe me if I said I had?"
+
"That would depend on what advantage you expected from the answer."
- * "Herr Nowak. You wound me."
- "Not yet."
+ -> statue_lover_followup
- It is the first thing he has said all day that almost sounds like flirtation, though perhaps only because danger has a talent for borrowing warmer clothes.
-
- * "Then watch the slope, not my intentions. One of them may be useful."
- ~ viktor_trust += 1
- He obeys without admitting that he has done so.
-
- -
- -> coach_nears_village
-
-* "No." #route:sapphic
+* ["No."]
+ #route:sapphic
~ sapphic += 1
+ "No."
+
The denial is too quick, and you both hear it.
You are not thinking of the stone now. You are thinking of the young woman waiting somewhere ahead: the Graf's daughter, the reason carefully not written into the memorandum, the stranger whose household has summoned you under a title both absurd and useful.
- * "It was only shadow."
- If this place keeps women in stone, you think, what does it do to them in houses?
+ -> statue_sapphic_followup
- * "Or if I did, I prefer not to have it explained before I understand why it matters."
- ~ detective += 1
- If this place keeps women in stone, you think, what does it do to them in houses?
+=== statue_eccentric_followup ===
- -
- -> coach_nears_village
+* ["If it is a shrine, it has not been loved recently."]
+ "If it is a shrine, it has not been loved recently."
+
+ "You speak as if stones notice neglect."
+
+ Soldiers, too, notice neglect. His silence admits enough.
+
+ He does not answer.
+
+ -> coach_nears_village
+
+* ["No. Shrines face the faithful. That thing was listening sideways."]
+ ~ supernatural_exposure += 1
+ "No. Shrines face the faithful. That thing was listening sideways."
+
+ Viktor's hand rests on the coach strap, still and ready.
+
+ -> coach_nears_village
+
+=== statue_detective_followup ===
+
+* ["Not clearly. Enough to ask later."]
+ "Not clearly. Enough to ask later."
+
+ Viktor looks back through the small rear window. The bend has already erased the slope.
+
+ "Ask carefully. Places people fail to mention are often more informative than those they recommend."
+
+ -> coach_nears_village
+
+* ["Only a suggestion of one. If it exists, someone maintains the absence of it."]
+ #route:detective
+ ~ detective += 1
+ "Only a suggestion of one. If it exists, someone maintains the absence of it."
+
+ "You make absences sound expensive."
+
+ They usually are; absence is expensive when someone maintains it.
+
+ -> coach_nears_village
+
+=== statue_careless_followup ===
+
+* ["Then I shall rely on your conversation to intimidate them."]
+ "Then I shall rely on your conversation to intimidate them."
+
+ The driver pretends not to hear. His shoulders, however, hear everything.
+
+ -> coach_nears_village
+
+* ["How unfortunate. You seemed so professionally reassuring."]
+ #route:lover
+ ~ lover += 1
+ "How unfortunate. You seemed so professionally reassuring."
+
+ "I prefer enemies that identify themselves."
+
+ -> coach_nears_village
+
+=== statue_lover_followup ===
+
+* ["Herr Nowak. You wound me."]
+ "Herr Nowak. You wound me."
+
+ "Not yet."
+
+ It is the first thing he has said all day that almost sounds like flirtation, though perhaps only because danger has a talent for borrowing warmer clothes.
+
+ -> coach_nears_village
+
+* ["Then watch the slope, not my intentions. One of them may be useful."]
+ ~ viktor_trust += 1
+ "Then watch the slope, not my intentions. One of them may be useful."
+
+ He obeys without admitting that he has done so.
+
+ -> coach_nears_village
+
+=== statue_sapphic_followup ===
+
+* ["It was only shadow."]
+ "It was only shadow."
+
+ If this place keeps women in stone, you think, what does it do to them in houses?
+
+ -> coach_nears_village
+
+* ["Or if I did, I prefer not to have it explained before I understand why it matters."]
+ #route:detective
+ ~ detective += 1
+ "Or if I did, I prefer not to have it explained before I understand why it matters."
+
+ If this place keeps women in stone, you think, what does it do to them in houses?
+
+ -> coach_nears_village
=== coach_nears_village ===
@@ -1216,32 +1576,57 @@ Beside you, Viktor lowers his voice.
"Remember: at Hohenreith, every courtesy will mean something. Here, every silence will."
-* "Then we are already being received."
+* ["Then we are already being received."]
+ #route:detective
~ detective += 1
+ "Then we are already being received."
+
"Yes," he says. "And examined."
-* "You make it sound as if the village outranks the Graf."
+ -> village_final_image
+
+* ["You make it sound as if the village outranks the Graf."]
+ #route:eccentric
~ eccentric += 1
+ "You make it sound as if the village outranks the Graf."
+
"No," Viktor says. "Only as if it may have survived more than one."
-* "How fortunate that I packed several silences."
+ -> village_final_image
+
+* ["How fortunate that I packed several silences."]
+ #route:lover
~ lover += 1
+ "How fortunate that I packed several silences."
+
His mouth almost moves. "Use the plainest one first."
-* "I dislike being watched by people who will not introduce themselves."
+ -> village_final_image
+
+* ["I dislike being watched by people who will not introduce themselves."]
+ #route:careless
~ careless += 1
+ "I dislike being watched by people who will not introduce themselves."
+
"That," he says, "is unlikely to improve today."
-* "If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts."
+ -> village_final_image
+
+* ["If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts."]
+ #route:sapphic
~ sapphic += 1
+ "If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts."
+
Viktor glances at you, but whatever answer he considers, he keeps it behind his teeth.
--
+ -> village_final_image
+
+=== village_final_image ===
The horses draw the coach past the churchyard wall. Above it, on the old plaster beside the gate, a faded painted woman looks down from beneath a flaking blue mantle. Her hands are folded in prayer. Her eyes, damaged by weather, no longer point in the same direction.
For one breath, as the wheels pass over a buried runnel of water, the painted face seems less like the Holy Mother than like a mask put on something that had been waiting longer.
-Then the coach enters the village proper, and the road bends toward the unseen height where Jagdhaus Hohenreith stands above Eibenreith under its newer name.
+Then the coach enters the village proper, and the road bends toward the unseen height where Jagdhaus Hohenreith stands above Eibenreith under its newer name
--> END
+-> DONE
\ No newline at end of file
diff --git a/data/ink/kaiserpunk.ink.json b/data/ink/kaiserpunk.ink.json
index 5ada41c..83fe341 100644
--- a/data/ink/kaiserpunk.ink.json
+++ b/data/ink/kaiserpunk.ink.json
@@ -1 +1 @@
-{"inkVersion":21,"root":[[{"->":"intro_train"},["done",{"#n":"g-0"}],null],"done",{"intro_train":[["^The train has left Vienna behind, though Vienna has not yet left you. ","#","^chapter[Eibenreith] ","/#","#","^music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)","/#","\n","^It clings to the black gloss of your travelling boots, to the cut of your coat, to the stiff little prison of your gloves. It lives in the seal upon the letter folded inside your reticule, in the thin scent of coal smoke that has insinuated itself even into first-class upholstery, in the fact that Herr Viktor Nowak sits opposite you as if the carriage were a field office and not a compartment lined in velvet, polished wood, and brass.","\n","^Outside the window, the last outskirts of the capital have broken apart into winter-browned fields and villages with church towers too small to compete with the engine's whistle. The rails take the land without asking permission. Embankments cut through orchards. Telegraph poles pass at regular intervals, each one vanishing behind you like a thought dismissed too quickly. ","#","^sfx[steam-whistle.ogg]","/#","\n","^You had expected the train to feel like a triumph of the age.","\n","^Instead it feels like an argument. ","#","^image[suedbahn.png](landscape)","/#","\n","^The machine throws itself southward with a violence that polite society would never admit to admiring. The lamps tremble in their fittings. Your cup rattles against its saucer. Beyond the glass, the country begins to rise, first gently, then with a firmer will, until the line itself seems to negotiate with the mountains through stone arches, black tunnels, and viaducts thrown across ravines with all the confidence of imperial engineering.","\n","^Viktor has not looked impressed once.","\n","^His civilian clothes are correct enough to pass without comment: dark frock coat, sober waistcoat, gloves, collar immaculate, the posture of a man who has never truly sat at ease in his life. But no tailor can disguise discipline. It remains in his shoulders, in the economy of his movements, in the way his eyes measure doors, windows, luggage rack, corridor, your face, then the door again.","\n","^On the paperwork he is your secretary and travelling companion.","\n","^In truth, he is an officer lent to a delicate matter by channels that prefer not to be named. Rittmeister Viktor Alois Nowak, though no one at Jagdhaus Hohenreith is expected to call him that. Your hosts have asked for a medium. The Cabinet has sent them you. The military has sent him to make certain that you do not become a scandal before you become useful.","\n","^He folds the newspaper, though you are quite certain he had not been reading it.","\n","^\"You have been very quiet, gnädiges Fräulein. For a lady on her first official journey, you show remarkable restraint.\"","\n","^The form of address is technically correct if you are noble, excessive if you are not, and perfectly chosen because he does not yet know which part of you is useful, which part is costume, and which part is threat.","\n","^You look around the compartment before you answer. The answer comes from somewhere older than the letter in your reticule. It comes from the place you began.","\n","ev","str","^The compartment seems built for people who never wonder whether they belong in it.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^You count the cost of each detail before you can stop yourself.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["^ ","#","^class:noble","/#","\n","ev","str","^noble","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},2,"+",{"VAR=":"class_confidence","re":true},"/ev","ev",{"VAR?":"court_loyalty"},1,"+",{"VAR=":"court_loyalty","re":true},"/ev","^It is not luxury that unsettles you. Luxury is only wood, cloth, brass, service, silence. What matters is whether the servants glance twice, whether the guard lowers his voice, whether another passenger weighs your gloves and decides not to ask your business.","\n","^You were born among people who understood such things before they understood kindness.","\n",{"->":"class_noble_background"},{"#f":5}],"c-1":["^ ","#","^class:middle","/#","\n","ev","str","^middle","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^The upholstery, the lamps, the polished veneer, the quiet attendance at stations: none of it is magical. It is paid for. Accounted for. Itemised somewhere by someone with ink on his cuffs and a wife who knows how long candles may be burned before the household budget complains.","\n","^You were not born to this compartment, but you were born close enough to study its rules.","\n",{"->":"class_middle_background"},{"#f":5}],"c-2":["^ ","#","^class:working","/#","\n","ev","str","^working","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"-",{"VAR=":"class_confidence","re":true},"/ev","^The velvet looks soft enough to swallow fingerprints. The brass fittings have been polished by hands that will never sit here. The little curtain strap is worn where other travellers, all of them more certain than you, have touched it without gratitude.","\n","^You were not born on this side of service.","\n",{"->":"class_working_background"},{"#f":5}]}],null],"class_noble_background":[["^Viktor waits for the answer owed to his remark. The train jolts once, then settles again into its hard, confident rhythm.","\n",["ev",{"^->":"class_noble_background.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Only when properly commanded.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Only when men mistake silence for obedience.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"I prefer any discipline that leaves a clean record.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"class_noble_background.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^His brows move almost imperceptibly.","\n","^\"Then good breeding has military applications,\" he says.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"class_noble_background.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"You intend to command it yourself?\"","\n","^\"Whenever possible.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"class_noble_background.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"That is an ambitious distinction.\"","\n","^\"It has preserved many women from being understood too early.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["ev",{"^->":"class_noble_background.0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"A useful preference,\" he says. \"If sincere.\"","\n","^\"If it is not sincere, I have stated it early enough for you to investigate.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"class_noble_explanation"},["ev",{"^->":"class_noble_background.0.g-0.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-5","flg":18},{"s":["^\"A family sentence, more often.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-6","flg":18},{"s":["^\"An education in rooms where every chair has rank.\"",{"->":"$r","var":true},null]}],{"c-4":["ev",{"^->":"class_noble_background.0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"A family education, then.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["ev",{"^->":"class_noble_background.0.g-0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"You speak as if birth were a prison.\"","\n","^\"Only when it is furnished well enough for visitors to admire.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["ev",{"^->":"class_noble_background.0.g-0.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^\"Then Hohenreith may not surprise you.\"","\n","^\"That depends on whether they have better chairs than secrets.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"class_noble_explanation"},["ev",{"^->":"class_noble_background.0.g-1.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-7","flg":18},{"s":["^\"If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.g-1.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-8","flg":18},{"s":["^\"Or less honest about it.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_noble_background.0.g-1.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-9","flg":18},{"s":["^\"Then I must try not to disappoint the army.\"",{"->":"$r","var":true},null]}],{"c-7":["ev",{"^->":"class_noble_background.0.g-1.c-7.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^The corner of his mouth changes almost too little to notice.","\n","^\"Officers are less easily bored.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["ev",{"^->":"class_noble_background.0.g-1.c-8.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"You accuse the army of vanity.\"","\n","^\"No. I accuse men of consistency.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-9":["ev",{"^->":"class_noble_background.0.g-1.c-9.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"That is precisely what I have been asked to prevent.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":[{"->":"class_noble_explanation"},null]}],null],"class_noble_explanation":["^You learned young that every room contains a court, even when no emperor is present. A girl of your rank is trained to enter, to bow, to be introduced, to be placed, to speak only enough, to understand more than she admits, and to know that a family name can be both a key and a chain.","\n","^Your own family possesses no grand seat, no army of retainers, no ancient right to command provinces. But your name opened drawing-room doors in Vienna, and once inside those rooms you learned to make people repeat stories they had meant only to hint at. You learned how widows speak when priests are absent, how officers lie when flattered, how old men confess when they believe themselves admired, and how a young woman may be underestimated so consistently that underestimation becomes a profession.","\n","^Your reputation as a medium did not descend from heaven. It was assembled from half-lights, correct guesses, careful silences, and the willingness of better-born fools to mistake performance for revelation.","\n","^Before the court could use you, society had first to invent you.","\n","^Now choose the name by which Vienna invented you.","\n",{"->":"choose_name_noble"},null],"class_middle_background":[["^Viktor's remark lingers between you with the odour of a polite accusation.","\n",["ev",{"^->":"class_middle_background.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Restraint is easier when one has learned that every mistake is remembered by someone better placed.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_middle_background.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"A useful one. Bitterness is merely the taste left by instruction.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_middle_background.0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"A common one. Some people only notice injustice when it reaches their own floor.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"class_middle_background.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^Viktor watches you more closely.","\n","^\"A bitter lesson.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"class_middle_background.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"You collect phrases like weapons.\"","\n","^\"Only the light ones. Heavy weapons attract permits.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"class_middle_background.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"You have made a study of floors?\"","\n","^\"Of thresholds. They are more honest.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"class_middle_explanation"},["ev",{"^->":"class_middle_background.0.g-0.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_middle_background.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"A courtesy. I let them begin with their favorite subject.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_middle_background.0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-5","flg":18},{"s":["^\"An experiment. It has produced reliable results.\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"class_middle_background.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^\"A method?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["ev",{"^->":"class_middle_background.0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Themselves.\"","\n","^\"You understand the principle already.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["ev",{"^->":"class_middle_background.0.g-0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then I am part of your experiment.\"","\n","^\"Naturally. You sat opposite me.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"class_middle_explanation"},["ev",{"^->":"class_middle_background.0.g-1.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-6","flg":18},{"s":["^\"I was considering whether your concern is official, personal, or merely masculine.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_middle_background.0.g-1.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-7","flg":18},{"s":["^\"How convenient. The other two may deny responsibility.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_middle_background.0.g-1.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-8","flg":18},{"s":["^\"Then I shall treat it with the respect due to paper.\"",{"->":"$r","var":true},null]}],{"c-6":["ev",{"^->":"class_middle_background.0.g-1.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^His eyes harden by one exact degree.","\n","^\"Today it is official.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["ev",{"^->":"class_middle_background.0.g-1.c-7.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"I advise you not to make wit your first instrument at Hohenreith.\"","\n","^\"Then I shall make it the second.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["ev",{"^->":"class_middle_background.0.g-1.c-8.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Paper has moved armies.\"","\n","^\"And buried mistakes.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":[{"->":"class_middle_explanation"},null]}],null],"class_middle_explanation":["^You were born in that broad, anxious territory between deference and ambition. Your family had books, invoices, respectability, perhaps a piano no one played well enough, perhaps a father with an office, a mother with callers, brothers who were expected to advance, and daughters who were expected not to make advancement look hungry.","\n","^You learned accounts before etiquette, etiquette before French, and French before you learned how easily a woman with a calm voice could make men explain themselves. You rose because you listened. You rose because you understood that fraud, faith, medicine, gossip, politics, and grief all use the same doors into the human mind.","\n","^The court does not like to admit that it needs middle-class competence. It prefers to borrow it, dress it properly, and call it discretion.","\n","^Your reputation as a medium gave them a word that sounded less dangerous than investigator.","\n","^Now choose the name under which you entered the salons that first laughed at you, then invited you back.","\n",{"->":"choose_name_middle"},null],"class_working_background":[["^Viktor's courtesy is smooth enough to be handled without fingerprints. You hear, beneath it, the question of how much this compartment has improved you.","\n",["ev",{"^->":"class_working_background.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Restraint is what people praise when they prefer not to see the effort.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_working_background.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Only cheaply.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_working_background.0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Only when it hides the person doing the work.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"class_working_background.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^The newspaper in Viktor's hand creases once.","\n","^\"You object to being praised?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"class_working_background.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"That may be difficult to avoid.\"","\n","^\"Then Hohenreith will have to spend more.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"class_working_background.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^He studies you as if the answer has come from farther down the train than first class.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"class_working_explanation"},["ev",{"^->":"class_working_background.0.g-0.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"I am quiet because people often prefer women of my origin either grateful or invisible. I am deciding which will inconvenience you less.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_working_background.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"No. You asked me to be manageable.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_working_background.0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-5","flg":18},{"s":["^\"Then I shall postpone gratitude until you deserve it.\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"class_working_background.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"I did not ask you to be grateful.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["ev",{"^->":"class_working_background.0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"I asked you nothing of the kind.\"","\n","^\"That is the benefit of rank. It asks through furniture.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["ev",{"^->":"class_working_background.0.g-0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^A pause. Then, very dryly: \"A generous arrangement.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"class_working_explanation"},["ev",{"^->":"class_working_background.0.g-1.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-6","flg":18},{"s":["^\"I was trying not to touch the upholstery as though it might accuse me.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_working_background.0.g-1.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-7","flg":18},{"s":["^\"Then I am already braver than I was.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"class_working_background.0.g-1.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-8","flg":18},{"s":["^\"I shall take that as reassurance, though you delivered it like a reprimand.\"",{"->":"$r","var":true},null]}],{"c-6":["ev",{"^->":"class_working_background.0.g-1.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^Something like concern crosses his face, disguised too late as irritation.","\n","^\"The upholstery has survived ministers. It will survive you.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["ev",{"^->":"class_working_background.0.g-1.c-7.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"Courage measured against upholstery is not a military standard.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["ev",{"^->":"class_working_background.0.g-1.c-8.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"I have had practice with both.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":[{"->":"class_working_explanation"},null]}],null],"class_working_explanation":["^You were born among people who owned little but obligations. Work had a sound before it had a meaning: water, broom, bootsteps, breath, the clatter of dishes, the cough of men coming in from cold yards, women counting coins under their breath. You learned early that the high-born are not more observant than others. They are merely less often required to observe.","\n","^That was your first advantage.","\n","^A servant knows which door matters because she uses the others. A seamstress learns bodies because she measures them. A maid learns secrets because fine people leave their souls lying about like gloves, certain that no one beneath them has hands.","\n","^You rose by talent, patronage, imitation, nerve, and the terrible convenience of being believed harmless. By the time Vienna began whispering that you saw more than respectable people saw, you had already spent years seeing what respectable people missed.","\n","^The court has placed you in first class because it needs what birth did not give you.","\n","^Now choose the name you carried upward, altered perhaps in pronunciation, never quite cleansed of where it began.","\n",{"->":"choose_name_working"},null],"choose_name_noble":[["ev","str","^Valerie Eleonore Josepha","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Helene Cäcilie Franziska","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Clara Theresia Leopoldine","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Sophie Eleonore Auguste","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Mathilde Josepha Henriette","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Therese Valerie Franziska","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Ilona Theresia Eleonore","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Valerie Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Valerie","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-1":["\n","ev","str","^Helene Cäcilie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-2":["\n","ev","str","^Clara Theresia Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-3":["\n","ev","str","^Sophie Eleonore Auguste","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Sophie","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-4":["\n","ev","str","^Mathilde Josepha Henriette","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Mathilde","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-5":["\n","ev","str","^Therese Valerie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-6":["\n","ev","str","^Ilona Theresia Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Ilona","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-7":["\n","ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Zdenka","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}]}],null],"choose_surname_noble":[["^Your title is fixed by birth and by the careful modesty of your family: not countess, not princess, not one of the brilliant names that gather ambassadors and creditors like dust.","\n","^A Freiin. Baronial. Usable. Admitted, but not enthroned.","\n","ev","str","^Freiin von Rauhenfels","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Freiin von Traunegg","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Freiin von Ebenwald","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Freiin von Arnsberg","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Freiin von Reichenau","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Freiin von Waldstätten","/str","/ev",{"*":".^.c-5","flg":20},{"c-0":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rauhenfels","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Traunegg","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Ebenwald","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Arnsberg","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Reichenau","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Waldstätten","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"choose_name_middle":[["ev","str","^Clara Eleonore","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Anna Katharina","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Helene Theresia","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Rosa Franziska","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Johanna Elise","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Katharina Sophie","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Therese Leopoldine","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Magdalena Cäcilie","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Clara Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-1":["\n","ev","str","^Anna Katharina","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-2":["\n","ev","str","^Helene Theresia","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-3":["\n","ev","str","^Rosa Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-4":["\n","ev","str","^Johanna Elise","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Johanna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-5":["\n","ev","str","^Katharina Sophie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Katharina","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-6":["\n","ev","str","^Therese Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-7":["\n","ev","str","^Magdalena Cäcilie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Magdalena","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}]}],null],"choose_surname_middle":[["^Your family name contains no particle to soften the ascent. It must stand upright by itself.","\n","ev","str","^Leitner","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Wagner","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Kellner","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Baumgartner","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Fischer","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Schmid","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Pichler","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Rosenfeld","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Leitner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wagner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Kellner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Baumgartner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Fischer","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schmid","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-6":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-7":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rosenfeld","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"choose_name_working":[["ev","str","^Anna","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Klara","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Agnes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Leni","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Rosa","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Gertrud","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Elisabeth","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Franziska","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Anna","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-1":["\n","ev","str","^Klara","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Klara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-2":["\n","ev","str","^Agnes","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Agnes","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-3":["\n","ev","str","^Leni","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Leni","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-4":["\n","ev","str","^Rosa","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-5":["\n","ev","str","^Gertrud","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Gertrud","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-6":["\n","ev","str","^Elisabeth","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Elisabeth","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-7":["\n","ev","str","^Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Franziska","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}]}],null],"choose_surname_working":[["^A simple name can be a burden in Vienna. It tells people how little they must pretend to respect you before you have spoken.","\n","ev","str","^Pichler","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Huber","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Maier","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Gruber","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Schuster","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Krenn","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Wolf","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Moser","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Huber","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Maier","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Gruber","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schuster","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Krenn","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-6":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wolf","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-7":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Moser","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"assemble_full_name":["ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"given_names"},"str","^ ","/str","+",{"VAR?":"title_part"},"+","str","^ ","/str","+",{"VAR?":"surname"},"+","/ev",{"VAR=":"full_name","re":true},{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"title_part"},"str","^ ","/str","+",{"VAR?":"given_names"},"+","str","^ ","/str","+",{"VAR?":"surname"},"+","/ev",{"VAR=":"full_name","re":true},{"->":".^.^.^.9"},null]}],"nop","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^On visiting cards, in letters, in the cautious mouths of servants, you are ","ev",{"VAR?":"full_name"},"out","/ev","^.","\n",{"->":".^.^.^.20"},null]}],[{"->":".^.b"},{"b":["\n","^On railway documents, hotel ledgers, and the tongues of people who have not yet decided how much respect you deserve, you are ","ev",{"VAR?":"full_name"},"out","/ev","^.","\n",{"->":".^.^.^.20"},null]}],"nop","\n","^But in the private chamber where a name is first answered before it is performed, you are ","ev",{"VAR?":"common_name"},"out","/ev","^.","\n","^Viktor has waited through your silence with a soldier's patience and a jailer's courtesy. The train enters another tunnel. For several seconds the compartment window gives you back only your own reflection: your hat, your pale face above the dark collar, your eyes too steady or not steady enough.","\n","^When the mountains return, they seem closer.","\n",{"->":"mirror_definition"},null],"mirror_definition":[["^The black glass gives you the woman who will arrive in Eibenreith before any rumour has time to improve her.","\n","^Choose what the window catches first.","\n","ev","str","^Dark ash-brown hair pinned with almost severe care.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Chestnut hair arranged to look softer than the mind beneath it.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Cool fair hair made austere by dark travelling clothes.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Black-brown hair with a few escaped wisps already refusing discipline.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^dark ash-brown","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has dark ash-brown hair, almost black where the tunnel has not quite left it, pinned with the severity of a woman who knows that disarray is forgiven less easily in the young.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-1":["\n","ev","str","^chestnut","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has chestnut hair, warm where the lamp touches it, arranged with enough softness to flatter and enough control to warn the attentive.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-2":["\n","ev","str","^cool fair","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has cool fair hair, not golden enough for sentimental painters, but pale enough that the dark travelling clothes make your face appear more deliberate than gentle.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-3":["\n","ev","str","^black-brown","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has black-brown hair, glossy in the lamp's weak tremor, with two escaped wisps at the temple already committing small treasons against the pins.","\n",{"->":"mirror_complexion"},{"#f":5}]}],null],"mirror_complexion":[["^The window deepens, and with it the face.","\n","ev","str","^A fair, cool complexion that looks almost bloodless in railway light.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A clear complexion that still remembers the city more than the sun.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A face that has learned to look fragile when fragility is useful.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A slightly warmer complexion that makes the severity of the outfit less forgiving.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^fair and cool","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is fair and cool, made paler by smoke, glass, and the faint greenish cast of mountain light.","\n",{"->":"mirror_face"},{"#f":5}],"c-1":["\n","ev","str","^clear and sheltered","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion has the clarity of rooms, gloves, and shaded streets; not sickly, but visibly protected from the labour that browns other lives.","\n",{"->":"mirror_face"},{"#f":5}],"c-2":["\n","ev","str","^delicately pale","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is delicately pale, the sort physicians and foolish men read too eagerly, and which you have never felt obliged to correct every time.","\n",{"->":"mirror_face"},{"#f":5}],"c-3":["\n","ev","str","^warm fair","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is warmer than Vienna's winter light would prefer, and that warmth makes the severe coat and collar seem chosen rather than imposed.","\n",{"->":"mirror_face"},{"#f":5}]}],null],"mirror_face":[["^The window keeps you just long enough to make judgement impolite.","\n","ev","str","^A long oval face, observant before it is beautiful.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A composed face that seems trained equally for salons and interrogations.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A pretty face made less harmless by the steadiness of the eyes.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A severe face that becomes almost vulnerable only when caught unprepared.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^long oval and observant","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is long and oval, with watchful eyes, a straight nose, and a mouth that knows how often women are punished for amusement before men call them clever.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-1":["\n","ev","str","^composed and trained","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is composed rather than soft, with brows dark enough to sharpen silence and a mouth whose politeness has not yet promised mercy.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-2":["\n","ev","str","^pretty but steady","/str","/ev",{"VAR=":"face_detail","re":true},"^The face might be called pretty by people who do not like to work harder for adjectives, but the eyes disturb the compliment by appearing to have heard it before and found it insufficient.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-3":["\n","ev","str","^severe and guarded","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is severe at first glance, guarded at the second, and only after that does it betray how young a woman may still be while carrying herself like a sealed document.","\n",{"->":"mirror_outfit"},{"#f":5}]}],null],"mirror_outfit":[["^The rest of the reflection is costume, armour, and evidence.","\n","ev","str","^Dark charcoal-plum travelling wool, restrained but expensive.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A severe black-brown coat and skirt, softened only by ivory at throat and cuffs.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Bottle-green details hidden in an otherwise sombre travelling suit.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A more fashionable ensemble, dark and narrow, calculated to be remembered.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^charcoal-plum travelling wool","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a tailored travelling ensemble of dark charcoal wool with a plum undertone, high at the throat, close at the waist, expensive in the way that avoids asking to be admired.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-1":["\n","ev","str","^black-brown travelling suit","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a black-brown travelling suit and long coat, softened only by ivory at throat and cuffs, the lace narrow enough to seem like restraint rather than decoration.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-2":["\n","ev","str","^sombre suit with bottle-green details","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a sombre travelling suit with bottle-green details so discreet that only close attention discovers them, which is nearly the point.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-3":["\n","ev","str","^fashionable dark travelling ensemble","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a dark, narrow, more fashionable travelling ensemble, the hat and veil chosen with just enough theatrical instinct to make sceptical people use the word instinctive.","\n",{"->":"supernatural_stance"},{"#f":5}]}],null],"supernatural_stance":[["^The letter of commission in your reticule does not call you an investigator.","\n","^It calls you, in prose dry enough to pass through any number of offices, a woman whose unusual spiritual reputation has recommended her to a delicate household matter. The phrasing is exquisite. It neither affirms nor denies. It permits everyone involved to believe afterward that they had believed nothing improper.","\n","^The comital family at Jagdhaus Hohenreith has asked for discretion. Vienna has answered with a sealed letter, a woman reputed to speak with what is hidden, and a man opposite her who has orders of his own.","\n","^Before this journey, before this train, before the mountains began taking the sky piece by piece, belief had already taken its position in you.","\n","ev","str","^The dead are not silent. The living are merely poor listeners.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Belief is a costume. You wear it because men insist on dressing you in it.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^You have learned not to decide too early.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","#","^supernatural:believer","/#","\n","ev","str","^believer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^You have always thought disbelief a provincial arrogance of the educated. There are pressures in rooms where grief has been. There are words people speak before they know they have spoken. There are dreams that arrive with mud on their hems.","\n","^Perhaps the world is not haunted. Perhaps it is simply crowded.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-1":["^ ","#","^supernatural:sceptic ","/#","#","^route:detective","/#","\n","ev","str","^sceptic","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^The word spirit covers too much and explains too little. You have watched respectable people call an echo a message, a coincidence a sign, a trembling hand an angelic visitation. Men of science can be fools, but fools with candles and planchettes are no improvement.","\n","^If Hohenreith has ghosts, you expect them to keep accounts, write letters, leave footprints, and benefit someone.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-2":["^ ","#","^supernatural:performer","/#","\n","ev","str","^performer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},2,"+",{"VAR=":"medium_reputation","re":true},"/ev","^You discovered early that men who distrust a woman's mind will sometimes worship her nerves. A conclusion from evidence irritates them. A vision, sighed through lowered lashes, makes them lean closer.","\n","^Very well. Let them lean.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-3":["^ ","#","^supernatural:undecided","/#","\n","ev","str","^undecided","/str","/ev",{"VAR=":"supernatural_belief","re":true},"^There are things you can explain, things you cannot yet explain, and things that explanation damages before it helps. You have made a profession of standing at thresholds with a face composed enough for both sides to continue speaking.","\n","^Hohenreith will have to show you what kind of case it is.","\n",{"->":"spiritual_senses"},{"#f":5}]}],null],"spiritual_senses":[["^Belief is one matter. Experience is another.","\n","^People call a woman sensitive when they want her perceptions to sound like an illness. They call her hysterical when those perceptions inconvenience them. They call her inspired when they need her, and unstable when they do not.","\n","^Beneath reputation and performance, memory has its own testimony.","\n","ev","str","^There have been moments you cannot explain away.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Everything you do can be explained by observation, timing, and nerve.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Something happens, but never when summoned.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^You buried the first signs so thoroughly that even you do not know what remains.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["^ ","#","^powers:genuine","/#","\n","ev","str","^genuine","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},2,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Once, as a child, you knew before the telegram came. Once, in a crowded room, a stranger's grief entered you with such force that your own knees failed. Once, in a mirror, you saw a door behind you that was not in the room when you turned.","\n","^You learned caution after that. It is unwise for a woman to know things before a man has asked her opinion.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-1":["^ ","#","^powers:faked ","/#","#","^route:detective","/#","\n","ev","str","^faked","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^You notice rings removed too recently, mourning gloves worn too carefully, letters folded and refolded until the crease gives away the reader's obsession. You hear servants misname guests, mothers pause before daughters' rooms, officers lie by becoming too exact.","\n","^The dead have never told you anything. The living cannot stop telling you everything.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-2":["^ ","#","^powers:ambiguous","/#","\n","ev","str","^ambiguous","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Your reputation depends upon command. The truth, if truth it is, has no respect for appointments.","\n","^Sometimes a room changes pressure around you. Sometimes a face acquires an old expression no living person taught it. Sometimes names arrive before introductions. But the harder you reach, the more ordinary the world becomes.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-3":["^ ","#","^powers:repressed ","/#","#","^route:eccentric","/#","\n","ev","str","^repressed","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^There are childhood memories sealed behind politeness: a nursery mirror turned to the wall, a nurse dismissed without reference, your mother's hand tightening around your wrist until the bones complained.","\n","^You became strange afterward in ways society found easier to admire than understand.","\n",{"->":"viktor_first_exchange"},{"#f":5}]}],null],"viktor_first_exchange":[["^The train emerges from the tunnel into a pale afternoon cut by dark firs and white rock. Far below, water shows itself only in flashes. The valley is no longer a view from a salon painting. It has depth enough to hide things.","\n","^Viktor opens a leather folder and removes a memorandum. He does not hand it to you at once.","\n","^\"When we leave the railway,\" he says, \"we will be met by a coach from Hohenreith. From that moment, appearances matter. Your hosts have been told that I assist with correspondence, travel, and practical arrangements. They need not be troubled with military definitions.\"","\n",["ev",{"^->":"viktor_first_exchange.0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"And the villagers?\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.7.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"How merciful. The empire has spared them vocabulary.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.8.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"You mean they are not to know whether I am guest, tool, or warning.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"viktor_first_exchange.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.6.s"},[{"#n":"$r2"}],"\n","^\"The villagers need not be troubled with anything.\"","\n","^There it is: the empire in miniature. A man, a folder, a locked sentence.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"viktor_first_exchange.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.7.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"The empire has spared them alarm,\" Viktor says.","\n","^\"It often uses the two interchangeably.\"","\n","^There it is: the empire in miniature. A man, a folder, a locked sentence.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"viktor_first_exchange.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.8.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"I mean they are to know only what steadies the situation.\"","\n","^\"That is not a contradiction.\"","\n","^There it is: the empire in miniature. A man, a folder, a locked sentence.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"You will be addressed according to the station you present,\" he continues. \"The Graf's household will observe rank. Servants will observe what the household observes. Villagers may observe less and remember more. I advise restraint.\"","\n","^The advice is sound. That makes it no less irritating.","\n",["ev",{"^->":"viktor_first_exchange.0.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods.\" ","#","^route:lover","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"A practical one.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-5","flg":18},{"s":["^\"Only where patriotism requires sacrifice.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-0.7.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-6","flg":18},{"s":["^\"Only where men mistake desire for judgement.\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"viktor_first_exchange.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev","str","^provocation","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^For the first time, amusement almost reaches his mouth.","\n","^\"A dangerous doctrine.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["ev",{"^->":"viktor_first_exchange.0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^\"You intend to practice it at Hohenreith?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["ev",{"^->":"viktor_first_exchange.0.g-0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.6.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^He looks down at the memorandum, but not quickly enough to conceal that he is reassessing you.","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-6":["ev",{"^->":"viktor_first_exchange.0.g-0.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.7.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"That may include more territory than the maps admit.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"viktor_explains_orders"},["ev",{"^->":"viktor_first_exchange.0.g-1.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-7","flg":18},{"s":["^\"Dangerous doctrines travel best in good gloves.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-1.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-8","flg":18},{"s":["^\"If you wish me to pass as harmless, you must stop warning me like a gaoler.\" ","#","^route:sapphic","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-1.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-9","flg":18},{"s":["^\"No. A gaoler is at least honest about the key.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-1.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-10","flg":18},{"s":["^\"Then do not stand between me and every locked door before I have touched the handle.\"",{"->":"$r","var":true},null]}],{"c-7":["ev",{"^->":"viktor_first_exchange.0.g-1.c-7.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"You intend to charm Hohenreith into confession?\"","\n","^\"Only if Hohenreith insists on being charmed.\"","\n",{"->":"viktor_explains_orders"},{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["ev",{"^->":"viktor_first_exchange.0.g-1.c-8.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev","str","^tension","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^His gaze sharpens.","\n","^\"I am not your gaoler.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-9":["ev",{"^->":"viktor_first_exchange.0.g-1.c-9.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","^The words surprise you by leaving a mark. Not on him, perhaps. On yourself. The closer the train carries you to Amalia's world, though you do not yet know her face, the more intolerable it seems that every female life there might be guarded by men who call the guarding concern.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-10":["ev",{"^->":"viktor_first_exchange.0.g-1.c-10.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Some doors are locked for cause.\"","\n","^\"Then the cause had better survive examination.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":["^Viktor folds the memorandum once, precisely.","\n",{"->":"viktor_explains_orders"},["ev",{"^->":"viktor_first_exchange.0.g-2.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-11","flg":18},{"s":["^\"Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?\" ","#","^route:detective","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-2.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-12","flg":18},{"s":["^\"Credible phenomena not presently classifiable.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-2.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-13","flg":18},{"s":["^\"And if the phenomena become classifiable?\"",{"->":"$r","var":true},null]}],{"c-11":["ev",{"^->":"viktor_first_exchange.0.g-2.c-11.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev","str","^professional","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^He gives the smallest nod, as if you have chosen the only answer fit for adults.","\n","^\"They know that you come recommended. They suspect that you may be able to settle disturbances without police, priest, or press. You are permitted to verify fraud, coercion, threat to public order, or credible phenomena not presently classifiable.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-12":["ev",{"^->":"viktor_first_exchange.0.g-2.c-12.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"\n","^\"That is the phrase.\"","\n","^\"A bureaucratic ghost.\"","\n","^\"The safest kind.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-13":["ev",{"^->":"viktor_first_exchange.0.g-2.c-13.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^\"Then we classify them before others do.\"","\n","^\"Spoken like a man who has seen reports bury graves.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":[{"->":"viktor_explains_orders"},["ev",{"^->":"viktor_first_exchange.0.g-3.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-14","flg":18},{"s":["^\"I shall do my best not to faint unless it is useful.\" ","#","^route:careless","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-3.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-15","flg":18},{"s":["^\"How ungallant.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-3.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-16","flg":18},{"s":["^\"Then you must remain close enough to catch me.\"",{"->":"$r","var":true},null]}],{"c-14":["ev",{"^->":"viktor_first_exchange.0.g-3.c-14.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","^Something in his expression tightens; not contempt exactly, but readiness.","\n","^\"I would prefer you did not faint at all.\"","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-15":["ev",{"^->":"viktor_first_exchange.0.g-3.c-15.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^\"How practical.\"","\n","^\"Then you must be practical for both of us. I have never trusted the floor in strange houses.\"","\n","^His answer is delayed by half a breath.","\n","^\"That, gnädiges Fräulein, is precisely what concerns me.\"","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-16":["ev",{"^->":"viktor_first_exchange.0.g-3.c-16.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"My orders did not specify theatrical collapses.\"","\n","^\"How careless of them.\"","\n",{"->":".^.^.^.g-4"},{"#f":5}]}],"g-4":[{"->":"viktor_explains_orders"},["ev",{"^->":"viktor_first_exchange.0.g-4.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-17","flg":18},{"s":["^\"Restraint is what timid people call obedience after they have forgotten who trained them.\" ","#","^route:eccentric","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-4.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-18","flg":18},{"s":["^\"No. I dislike the laziness of letting fools remain undecided.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_first_exchange.0.g-4.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-19","flg":18},{"s":["^\"Enemies are merely people honest enough to stand in the right place.\"",{"->":"$r","var":true},null]}],{"c-17":["ev",{"^->":"viktor_first_exchange.0.g-4.c-17.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev","str","^challenge","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},2,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^Viktor studies you as he might study an unfamiliar weapon found in luggage.","\n","^\"You enjoy making enemies.\"","\n",{"->":".^.^.^.g-5"},{"#f":5}],"c-18":["ev",{"^->":"viktor_first_exchange.0.g-4.c-18.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^\"At Hohenreith, that dislike may become expensive.\"","\n","^\"Then the Graf should have invited someone cheaper.\"","\n",{"->":".^.^.^.g-5"},{"#f":5}],"c-19":["ev",{"^->":"viktor_first_exchange.0.g-4.c-19.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","^\"You speak as though conflict were housekeeping.\"","\n","^\"It is. One discovers what belongs where.\"","\n",{"->":".^.^.^.g-5"},{"#f":5}]}],"g-5":["^The wheels strike a curve. The compartment leans. For a moment the two of you are held in the same narrow imbalance.","\n",{"->":"viktor_explains_orders"},null]}],null],"viktor_explains_orders":[["^Viktor gives you the memorandum at last.","\n","^The document is not long. That is part of its menace. Long documents invite argument; short ones carry authority.","\n","^A comital household. A hunting residence in Upper Styria, not the family's principal seat. Reports of disturbances among servants and villagers. No police action requested. No public ecclesiastical inquiry desired. No press. No correspondence beyond approved channels. Your presence to be explained as a discreet consultation requested by the family. Herr Nowak to assist in practical matters.","\n","^No one has written the word ghost.","\n","^No one has written the word fraud.","\n","^No one has written the word daughter.","\n","^Yet the omissions arrange themselves around the page like furniture around a corpse.","\n",["ev",{"^->":"viktor_explains_orders.0.14.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"There is another instruction.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_explains_orders.0.15.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Your version is shorter than your silence. That means there is another instruction.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_explains_orders.0.16.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"How touching. Vienna trusts us both so little it had to divide the distrust.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"viktor_explains_orders.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.14.s"},[{"#n":"$r2"}],"\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"viktor_explains_orders.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.15.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"viktor_explains_orders.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.16.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[["ev",{"^->":"viktor_explains_orders.0.g-0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"For you.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_explains_orders.0.g-0.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"Concerning me.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_explains_orders.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-5","flg":18},{"s":["^\"Concerning whether I am fraud, fool, or useful animal.\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"viktor_explains_orders.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^\"Yes.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["ev",{"^->":"viktor_explains_orders.0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","^\"Partly.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["ev",{"^->":"viktor_explains_orders.0.g-0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Partly,\" he says, and this time the honesty has a blade in it.","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^The train begins to slow. The rhythm changes first in the floor, then in the window, then in the body. Houses gather beside the line. A station roof appears between drifting smoke and the dark combs of forested slopes. ","#","^sfx[steam-whistle.ogg]","/#","\n",["ev",{"^->":"viktor_explains_orders.0.g-1.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-6","flg":18},{"s":["^\"Then I shall try to be worth the ink.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_explains_orders.0.g-1.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-7","flg":18},{"s":["^\"Then I shall disappoint the instruction as creatively as circumstances permit.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"viktor_explains_orders.0.g-1.7.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-8","flg":18},{"s":["^\"Then keep your second instruction, Herr Nowak. I prefer primary sources.\"",{"->":"$r","var":true},null]}],{"c-6":["ev",{"^->":"viktor_explains_orders.0.g-1.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^\"I sincerely hope so.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["ev",{"^->":"viktor_explains_orders.0.g-1.c-7.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.6.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"I sincerely hope you do not.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["ev",{"^->":"viktor_explains_orders.0.g-1.c-8.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.7.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"A preference not always granted in imperial service.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":["^You cannot decide whether his answer is an insult, a prayer, or his first honest sentence.","\n",{"->":"railway_station"},null]}],null],"railway_station":[["^The station is small enough that the train seems briefly embarrassed to stop there. ","#","^chapter[The Station] ","/#","#","^image[muerzzuschlag.png](portrait)","/#","\n","^A porter in a cap too large for him hurries along the platform. A woman with a basket steps back from the steam as if from an animal. Somewhere beyond the station building, a cart horse stamps at frozen mud. The signboard gives the place a name you have seen in the timetable but will not remember with affection.","\n","^Your luggage descends in stages.","\n","ev","str","^A disciplined official set: trunk, dispatch case, hatbox, and black séance case.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^An elegant noblewoman's luggage: too correct to be accidental.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A performer’s luggage: harmless on top, less harmless beneath.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A practical assortment that betrays too much preparation.","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^An excessive pile that makes concealment impossible.","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","ev","str","^official","/str","/ev",{"VAR=":"baggage_style","re":true},"^First comes a sober travelling trunk with brass corners dulled by use, then a dispatch case, then a hatbox, then the narrow black case whose contents would embarrass both a priest and a conjurer if either searched it without imagination.","\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev",{"->":"station_luggage_common"},{"#f":5}],"c-1":["\n","ev","str","^elegant","/str","/ev",{"VAR=":"baggage_style","re":true},"^First comes a large trunk in dark leather, then a second smaller one for linen, then a round hatbox, a fitted toilette case, and a reticule kept too close to your hand for any porter to misunderstand its importance.","\n","ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev",{"->":"station_luggage_common"},{"#f":5}],"c-2":["\n","ev","str","^performer","/str","/ev",{"VAR=":"baggage_style","re":true},"^First comes a respectable trunk, then a hatbox, then a travelling case of gloves, veils, ribbons, calling cards, and the small objects by which a room may be persuaded to believe in forces already present.","\n","ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev",{"->":"station_luggage_common"},{"#f":5}],"c-3":["\n","ev","str","^practical","/str","/ev",{"VAR=":"baggage_style","re":true},"^First comes a battered trunk reinforced at the corners, then a leather case with notebooks, pencils, folded maps, spare gloves, a hand-lamp, and enough small necessities to offend anyone who prefers women decorative.","\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev",{"->":"station_luggage_common"},{"#f":5}],"c-4":["\n","ev","str","^excessive","/str","/ev",{"VAR=":"baggage_style","re":true},"^First comes one trunk, then another, then a hatbox, then a rug, then a dressing case, then the narrow black case, then a smaller parcel you had forgotten had survived packing. By the end even Viktor looks faintly outnumbered.","\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev",{"->":"station_luggage_common"},{"#f":5}]}],null],"station_luggage_common":["^Viktor oversees the transfer with clipped civility. He does not carry like a servant. He directs like a man pretending not to command.","\n","^The coach from Hohenreith waits beyond the station yard: dark green paint, black wheels, the comital crest discreetly worn on the door, and two horses already restless beneath harness. The driver removes his hat when he sees you. Not too deeply. Deep enough for rank, not deep enough for reverence. ","#","^sfx[horse-neigh.ogg]","/#","\n","^\"Gnädiges Fräulein? Herr Sekretär?\"","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^He has been told enough to place you. That is a courtesy. It is also a warning.","\n",{"->":".^.^.^.18"},null]}],[{"->":".^.b"},{"b":["\n","^He hesitates over you by the smallest measure. The hesitation is not rudeness. It is calculation. First-class carriage, court letter, no title beyond Fräulein, and a man beside you who looks like he has arrested people for less than staring.","\n",{"->":".^.^.^.18"},null]}],"nop","\n","^Viktor answers before you can.","\n","^\"From Jagdhaus Hohenreith?\"","\n","^\"Jawohl, Herr Sekretär. The road is passable. If the mist holds, we should reach Eibenreith before dark.\"","\n","^The word enters the air without ceremony.","\n","^Eibenreith.","\n","^Not Hohenreith, the name printed on the invitation in a clean hand. Eibenreith: the village below it. A smaller name. Older in the mouth. A name with roots rather than stationery.","\n",{"->":"coach_journey"},null],"coach_journey":[["^The coach leaves the station behind and with it the last easy evidence of empire. ","#","^chapter[The Graben] ","/#","#","^music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)","/#","\n","^At first the road follows a valley where telegraph wire still keeps company with it and the river moves in a pale, stony bed. Sawmills, fenced meadows, and farmhouses appear and vanish behind stands of spruce. The mountains do not rise all at once. They advance by jurisdiction. A wooded slope claims the left-hand sky, then a grey wall of limestone closes the north, then another ridge gathers to the east until even the clouds seem to have entered service.","\n","^The driver names places when Viktor asks, but the names are local and practical, meant for men who know which bridge floods and which farm breeds stubborn horses. Somewhere beyond the visible ridges, he says, lies the great white back of the Hochschwab. Eastward, beyond forest and pass, the Hohe Veitsch keeps its own weather. He says this not as a guide would say it, but as a man explaining neighbours who may or may not be in a temper.","\n","^The main valley narrows.","\n","^The road turns from it into a side Graben, and the change is immediate. Sound alters. The wheels no longer ring against open distance but grind between banks, roots, and wet stone. The air smells of leaf mould, resin, and cold water. Yews appear among the firs in dark, improbable patience, their needles too black for the afternoon.","\n","^\"Eibenreither Graben,\" the driver says, and crosses himself so quickly that the gesture might have been meant for a rut in the road.","\n","^Viktor notices. Of course he notices.","\n","^\"Bad road?\" he asks.","\n","^\"Old road,\" the driver says.","\n","^No one speaks for a while.","\n","^You watch the trees.","\n","^There are forests that invite stories because they are pretty, and forests that reject stories because whatever happened there did not require witnesses. This one belongs to the second kind. Its trunks stand close, not wildly, but with the air of a crowd making room for something carried through it long ago. The snow that remains in hollows is not clean. It has gathered needles, bark, and a yellowish stain where water has risen underneath.","\n","^On a slope above the road, half swallowed by undergrowth, you glimpse stone.","\n","^A shrine, perhaps. A boundary marker. A figure. The coach has passed before your eyes can persuade themselves of its shape. For one instant you are left with the impression of a woman's head inclined not in prayer, but in listening. ","#","^image[statue.png](square)","/#","\n","ev",{"VAR?":"supernatural_senses"},"str","^genuine","/str","==",{"VAR?":"supernatural_senses"},"str","^ambiguous","/str","==","||",{"VAR?":"supernatural_senses"},"str","^repressed","/str","==","||","/ev",[{"->":".^.b","c":true},{"b":["\n","^The back of your neck tightens.","\n","^Not fear. Recognition would be worse.","\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev",{"->":".^.^.^.58"},null]}],[{"->":".^.b"},{"b":["\n","^You tell yourself that old stone, seen through moving branches, will become whatever the mind is cowardly enough to supply.","\n",{"->":".^.^.^.58"},null]}],"nop","\n","^Viktor has turned slightly toward the same slope.","\n","^\"Did you see something?\"","\n",["ev",{"^->":"coach_journey.0.64.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"A woman in the wood, perhaps. Or a stone that wanted to be one.\" ","#","^route:eccentric ","/#","#","^statue_hint","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.65.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"If it is a shrine, it has not been loved recently.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.66.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"No. Shrines face the faithful. That thing was listening sideways.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"coach_journey.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.64.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^He studies the passing trees.","\n","^\"A local shrine?\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"coach_journey.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.65.s"},[{"#n":"$r2"}],"\n","^\"You speak as if stones notice neglect.\"","\n","^\"Do soldiers not?\"","\n","^He does not answer.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"coach_journey.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.66.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Viktor's hand rests on the coach strap, still and ready.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"coach_nears_village"},["ev",{"^->":"coach_journey.0.g-0.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"A marker. I would like to know where that path leads.\" ","#","^route:detective ","/#","#","^statue_hint","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"Not clearly. Enough to ask later.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-5","flg":18},{"s":["^\"Only a suggestion of one. If it exists, someone maintains the absence of it.\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"coach_journey.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"You saw a path?\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["ev",{"^->":"coach_journey.0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^Viktor looks back through the small rear window. The bend has already erased the slope.","\n","^\"Ask carefully. Places people fail to mention are often more informative than those they recommend.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-5":["ev",{"^->":"coach_journey.0.g-0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"You make absences sound expensive.\"","\n","^\"They usually are.\"","\n",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":[{"->":"coach_nears_village"},["ev",{"^->":"coach_journey.0.g-1.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-6","flg":18},{"s":["^\"Only trees. The sort that make one grateful for gentlemen with revolvers.\" ","#","^route:careless","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-1.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-7","flg":18},{"s":["^\"Then I shall rely on your conversation to intimidate them.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-1.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-8","flg":18},{"s":["^\"How unfortunate. You seemed so professionally reassuring.\"",{"->":"$r","var":true},null]}],{"c-6":["ev",{"^->":"coach_journey.0.g-1.c-6.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^His expression darkens by one official degree.","\n","^\"A revolver is a poor instrument against trees.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-7":["ev",{"^->":"coach_journey.0.g-1.c-7.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^The driver pretends not to hear. His shoulders, however, hear everything.","\n",{"->":".^.^.^.g-2"},{"#f":5}],"c-8":["ev",{"^->":"coach_journey.0.g-1.c-8.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"I prefer enemies that identify themselves.\"","\n",{"->":".^.^.^.g-2"},{"#f":5}]}],"g-2":[{"->":"coach_nears_village"},["ev",{"^->":"coach_journey.0.g-2.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-9","flg":18},{"s":["^\"Would you believe me if I said I had?\" ","#","^route:lover","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-2.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-10","flg":18},{"s":["^\"Herr Nowak. You wound me.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-2.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-11","flg":18},{"s":["^\"Then watch the slope, not my intentions. One of them may be useful.\"",{"->":"$r","var":true},null]}],{"c-9":["ev",{"^->":"coach_journey.0.g-2.c-9.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"That would depend on what advantage you expected from the answer.\"","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-10":["ev",{"^->":"coach_journey.0.g-2.c-10.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^\"Not yet.\"","\n","^It is the first thing he has said all day that almost sounds like flirtation, though perhaps only because danger has a talent for borrowing warmer clothes.","\n",{"->":".^.^.^.g-3"},{"#f":5}],"c-11":["ev",{"^->":"coach_journey.0.g-2.c-11.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^He obeys without admitting that he has done so.","\n",{"->":".^.^.^.g-3"},{"#f":5}]}],"g-3":[{"->":"coach_nears_village"},["ev",{"^->":"coach_journey.0.g-3.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-12","flg":18},{"s":["^\"No.\" ","#","^route:sapphic","/#",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-3.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-13","flg":18},{"s":["^\"It was only shadow.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_journey.0.g-3.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-14","flg":18},{"s":["^\"Or if I did, I prefer not to have it explained before I understand why it matters.\"",{"->":"$r","var":true},null]}],{"c-12":["ev",{"^->":"coach_journey.0.g-3.c-12.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^The denial is too quick, and you both hear it.","\n","^You are not thinking of the stone now. You are thinking of the young woman waiting somewhere ahead: the Graf's daughter, the reason carefully not written into the memorandum, the stranger whose household has summoned you under a title both absurd and useful.","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-13":["ev",{"^->":"coach_journey.0.g-3.c-13.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^If this place keeps women in stone, you think, what does it do to them in houses?","\n",{"->":".^.^.^.g-4"},{"#f":5}],"c-14":["ev",{"^->":"coach_journey.0.g-3.c-14.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^If this place keeps women in stone, you think, what does it do to them in houses?","\n",{"->":".^.^.^.g-4"},{"#f":5}]}],"g-4":[{"->":"coach_nears_village"},null]}],null],"coach_nears_village":[["^The Graben opens reluctantly.","\n","^First comes the smell of smoke. Then a roof, low and dark with weather. Then another. Then a church tower, not high, not graceful, but thick-shouldered and pale against the slope behind it. Its walls look older than the village around them and less certain of victory. The windows are small. The churchyard wall holds the road at a distance, as if the dead require fortification from the living, or the living from something else. ","#","^chapter[Eibenreith Village] ","/#","#","^sfx[church-bells.ogg](max=8, fade) ","/#","#","^image[eibenreith.png](landscape)","/#","\n","^Eibenreith appears not as a village in a picture appears, all at once and composed for admiration, but by fragments.","\n","^A woman in a dark kerchief pauses with a pail in her hand. A boy stops driving geese and lets them complain around his boots. Two men outside a shed end their conversation at the same moment without looking at each other. Curtains stir in windows where no one admits to standing. A blacksmith's sign moves slightly in air you cannot feel. Water runs somewhere under boards, under stone, under the road itself, quick and cold and hidden.","\n","^The houses are not poor, not exactly. Many are solid, whitewashed, shingled, kept with the stubborn decency of people who repair what they cannot replace. Yet something in their arrangement troubles the eye. They turn toward the church but not fully. They keep the road but lean from it. They leave, between yard and fence and woodpile, narrow passages where shadow gathers too early.","\n","^The coach slows.","\n","^No one runs to greet it.","\n","^No one needs to. News has already entered the village by means faster than railway, telegraph, or imperial seal.","\n","^You sit very straight as Eibenreith takes its first look at you.","\n","^Beside you, Viktor lowers his voice.","\n","^\"Remember: at Hohenreith, every courtesy will mean something. Here, every silence will.\"","\n",["ev",{"^->":"coach_nears_village.0.31.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Then we are already being received.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_nears_village.0.32.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"You make it sound as if the village outranks the Graf.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_nears_village.0.33.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"How fortunate that I packed several silences.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_nears_village.0.34.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"I dislike being watched by people who will not introduce themselves.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"coach_nears_village.0.35.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"coach_nears_village.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.31.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Yes,\" he says. \"And examined.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"coach_nears_village.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.32.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"No,\" Viktor says. \"Only as if it may have survived more than one.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"coach_nears_village.0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.33.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^His mouth almost moves. \"Use the plainest one first.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-3":["ev",{"^->":"coach_nears_village.0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.34.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"That,\" he says, \"is unlikely to improve today.\"","\n",{"->":".^.^.g-0"},{"#f":5}],"c-4":["ev",{"^->":"coach_nears_village.0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.35.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^Viktor glances at you, but whatever answer he considers, he keeps it behind his teeth.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^The horses draw the coach past the churchyard wall. Above it, on the old plaster beside the gate, a faded painted woman looks down from beneath a flaking blue mantle. Her hands are folded in prayer. Her eyes, damaged by weather, no longer point in the same direction.","\n","^For one breath, as the wheels pass over a buried runnel of water, the painted face seems less like the Holy Mother than like a mask put on something that had been waiting longer.","\n","^Then the coach enters the village proper, and the road bends toward the unseen height where Jagdhaus Hohenreith stands above Eibenreith under its newer name.","\n","end",null]}],null],"global decl":["ev","str","^unset","/str",{"VAR=":"birth_class"},"str","^","/str",{"VAR=":"title_part"},"str","^","/str",{"VAR=":"given_names"},"str","^","/str",{"VAR=":"common_name"},"str","^","/str",{"VAR=":"surname"},"str","^","/str",{"VAR=":"full_name"},"str","^unset","/str",{"VAR=":"baggage_style"},"str","^unset","/str",{"VAR=":"hair_detail"},"str","^unset","/str",{"VAR=":"complexion_detail"},"str","^unset","/str",{"VAR=":"face_detail"},"str","^unset","/str",{"VAR=":"outfit_detail"},"str","^unset","/str",{"VAR=":"supernatural_belief"},"str","^unset","/str",{"VAR=":"supernatural_senses"},"str","^unset","/str",{"VAR=":"viktor_relation"},0,{"VAR=":"lover"},0,{"VAR=":"sapphic"},0,{"VAR=":"detective"},0,{"VAR=":"careless"},0,{"VAR=":"eccentric"},0,{"VAR=":"class_confidence"},0,{"VAR=":"medium_reputation"},0,{"VAR=":"court_loyalty"},0,{"VAR=":"viktor_trust"},0,{"VAR=":"viktor_suspicion"},0,{"VAR=":"supernatural_exposure"},"/ev","end",null]}],"listDefs":{}}
\ No newline at end of file
+{"inkVersion":21,"root":[[{"->":"intro_train"},["done",{"#n":"g-0"}],null],"done",{"intro_train":[["^The train has left Vienna behind, though Vienna has not yet left you. ","#","^chapter[Eibenreith] ","/#","#","^music[Kaiserpunk Waltz.mp3](crossfade, loop, lead=8)","/#","\n","^It clings to the black gloss of your travelling boots, to the cut of your coat, to the stiff little prison of your gloves. It lives in the seal upon the letter folded inside your reticule, in the thin scent of coal smoke that has insinuated itself even into first-class upholstery, in the fact that Herr Viktor Nowak sits opposite you as if the carriage were a field office and not a compartment lined in velvet, polished wood, and brass.","\n","^Outside the window, the last outskirts of the capital have broken apart into winter-browned fields and villages with church towers too small to compete with the engine's whistle. The rails take the land without asking permission. Embankments cut through orchards. Telegraph poles pass at regular intervals, each one vanishing behind you like a thought dismissed too quickly. ","#","^sfx[steam-whistle.ogg]","/#","\n","^You had expected the train to feel like a triumph of the age.","\n","^Instead it feels like an argument. ","#","^image[suedbahn.png](landscape)","/#","\n","^The machine throws itself southward with a violence that polite society would never admit to admiring. The lamps tremble in their fittings. Your cup rattles against its saucer. Beyond the glass, the country begins to rise, first gently, then with a firmer will, until the line itself seems to negotiate with the mountains through stone arches, black tunnels, and viaducts thrown across ravines with all the confidence of imperial engineering.","\n","^Viktor has not looked impressed once.","\n","^His civilian clothes are correct enough to pass without comment: dark frock coat, sober waistcoat, gloves, collar immaculate, the posture of a man who has never truly sat at ease in his life. But no tailor can disguise discipline. It remains in his shoulders, in the economy of his movements, in the way his eyes measure doors, windows, luggage rack, corridor, your face, then the door again.","\n","^On the paperwork he is your secretary and travelling companion.","\n","^In truth, he is an officer lent to a delicate matter by channels that prefer not to be named. Rittmeister Viktor Alois Nowak, though no one at Jagdhaus Hohenreith is expected to call him that. Your hosts have asked for a medium. The Cabinet has sent them you. The military has sent him to make certain that you do not become a scandal before you become useful.","\n","^He folds the newspaper, though you are quite certain he had not been reading it.","\n","^\"You have been very quiet, gnädiges Fräulein. For a lady on her first official journey, you show remarkable restraint.\"","\n","^The form of address is technically correct if you are noble, excessive if you are not, and perfectly chosen because he does not yet know which part of you is useful, which part is costume, and which part is threat.","\n","^You look around the compartment before you answer. The answer comes from somewhere older than the letter in your reticule. It comes from the place you began.","\n","ev","str","^The compartment seems built for people who never wonder whether they belong in it.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^You count the cost of each detail before you can stop yourself.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^class:noble","/#","ev","str","^noble","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},2,"+",{"VAR=":"class_confidence","re":true},"/ev","ev",{"VAR?":"court_loyalty"},1,"+",{"VAR=":"court_loyalty","re":true},"/ev","^The compartment seems built for people who never wonder whether they belong in it.","\n","^It is not luxury that unsettles you. Luxury is only wood, cloth, brass, service, silence. What matters is whether the servants glance twice, whether the guard lowers his voice, whether another passenger weighs your gloves and decides not to ask your business.","\n","^You were born among people who understood such things before they understood kindness.","\n",{"->":"class_noble_background"},{"#f":5}],"c-1":["\n","#","^class:middle","/#","ev","str","^middle","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^You count the cost of each detail before you can stop yourself.","\n","^The upholstery, the lamps, the polished veneer, the quiet attendance at stations: none of it is magical. It is paid for. Accounted for. Itemised somewhere by someone with ink on his cuffs and a wife who knows how long candles may be burned before the household budget complains.","\n","^You were not born to this compartment, but you were born close enough to study its rules.","\n",{"->":"class_middle_background"},{"#f":5}],"c-2":["\n","#","^class:working","/#","ev","str","^working","/str","/ev",{"VAR=":"birth_class","re":true},"ev",{"VAR?":"class_confidence"},1,"-",{"VAR=":"class_confidence","re":true},"/ev","^You notice first how clean everything is, and how carefully one must sit so as not to betray noticing.","\n","^The velvet looks soft enough to swallow fingerprints. The brass fittings have been polished by hands that will never sit here. The little curtain strap is worn where other travellers, all of them more certain than you, have touched it without gratitude.","\n","^You were not born on this side of service.","\n",{"->":"class_working_background"},{"#f":5}]}],null],"class_noble_background":[["^Viktor waits for the answer owed to his remark. The train jolts once, then settles again into its hard, confident rhythm.","\n","ev","str","^\"Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Restraint is not a virtue, Herr Nowak. It is often only good breeding with its mouth shut.\"","\n","^His brows move almost imperceptibly.","\n","^\"Then good breeding has military applications,\" he says.","\n",{"->":"class_noble_followup_breeding"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"You need not test whether I can sit still, Herr Nowak. I was trained by people with less patience and sharper eyes.\"","\n","^\"A family education, then.\"","\n",{"->":"class_noble_followup_family"},{"#f":5}],"c-2":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"If this is remarkable restraint, Herr Nowak, I fear you have mostly escorted officers.\"","\n","^The corner of his mouth changes almost too little to notice.","\n","^\"Officers are less easily bored.\"","\n",{"->":"class_noble_followup_officers"},{"#f":5}]}],null],"class_noble_followup_breeding":[["ev","str","^\"Only when properly commanded.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only when men mistake silence for obedience.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"I prefer any discipline that leaves a clean record.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Only when properly commanded.\"","\n","^\"You intend to command it yourself?\"","\n","^The answer remains in the angle of your glove and the calmness of your stare.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Only when men mistake silence for obedience.\"","\n","^\"That is an ambitious distinction.\"","\n","^The distinction has preserved many women from being understood too early.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"I prefer any discipline that leaves a clean record.\"","\n","^\"A useful preference,\" he says. \"If sincere.\"","\n","^The conditional nature of sincerity remains, for the moment, his problem to investigate.","\n",{"->":"class_noble_explanation"},{"#f":5}]}],null],"class_noble_followup_family":[["ev","str","^\"A family sentence, more often.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"An education in rooms where every chair has rank.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"A family sentence, more often.\"","\n","^\"You speak as if birth were a prison.\"","\n","^The polished furniture answers him better than an argument could.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^\"An education in rooms where every chair has rank.\"","\n","^\"Then Hohenreith may not surprise you.\"","\n","^The possibility that Hohenreith may possess better secrets than chairs is allowed to stand.","\n",{"->":"class_noble_explanation"},{"#f":5}]}],null],"class_noble_followup_officers":[["ev","str","^\"Or less honest about it.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I must try not to disappoint the army.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Or less honest about it.\"","\n","^\"You accuse the army of vanity.\"","\n","^The accusation of consistency is the least deniable one.","\n",{"->":"class_noble_explanation"},{"#f":5}],"c-1":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"Then I must try not to disappoint the army.\"","\n","^\"That is precisely what I have been asked to prevent.\"","\n",{"->":"class_noble_explanation"},{"#f":5}]}],null],"class_noble_explanation":["^You learned young that every room contains a court, even when no emperor is present. A girl of your rank is trained to enter, to bow, to be introduced, to be placed, to speak only enough, to understand more than she admits, and to know that a family name can be both a key and a chain.","\n","^Your own family possesses no grand seat, no army of retainers, no ancient right to command provinces. But your name opened drawing-room doors in Vienna, and once inside those rooms you learned to make people repeat stories they had meant only to hint at. You learned how widows speak when priests are absent, how officers lie when flattered, how old men confess when they believe themselves admired, and how a young woman may be underestimated so consistently that underestimation becomes a profession.","\n","^Your reputation as a medium did not descend from heaven. It was assembled from half-lights, correct guesses, careful silences, and the willingness of better-born fools to mistake performance for revelation.","\n","^Before the court could use you, society had first to invent you.","\n","^Vienna knew you by the name society had made useful.","\n",{"->":"choose_name_noble"},null],"class_middle_background":[["^Viktor's remark lingers between you with the odour of a polite accusation.","\n","ev","str","^\"Restraint is easier when one has learned that every mistake is remembered by someone better placed.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"I was considering whether your concern is official, personal, or merely masculine.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Restraint is easier when one has learned that every mistake is remembered by someone better placed.\"","\n","^Viktor watches you more closely.","\n","^\"A bitter lesson.\"","\n",{"->":"class_middle_followup_bitter"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^\"If I am quiet, Herr Nowak, it is because men explain themselves faster when they dislike the silence.\"","\n","^\"A method?\"","\n",{"->":"class_middle_followup_method"},{"#f":5}],"c-2":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"I was considering whether your concern is official, personal, or merely masculine.\"","\n","^His eyes harden by one exact degree.","\n","^\"Today it is official.\"","\n",{"->":"class_middle_followup_official"},{"#f":5}]}],null],"class_middle_followup_bitter":[["ev","str","^\"A useful one. Bitterness is merely the taste left by instruction.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"A common one. Some people only notice injustice when it reaches their own floor.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"A useful one. Bitterness is merely the taste left by instruction.\"","\n","^\"You collect phrases like weapons.\"","\n","^The phrase lies lightly enough that he must decide whether it is ornament or weapon.","\n",{"->":"class_middle_explanation"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"A common one. Some people only notice injustice when it reaches their own floor.\"","\n","^\"You have made a study of floors?\"","\n","^Thresholds, you have learned, are more honest than floors; they admit that passage is a privilege.","\n",{"->":"class_middle_explanation"},{"#f":5}]}],null],"class_middle_followup_method":[["ev","str","^\"A courtesy. I let them begin with their favorite subject.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"An experiment. It has produced reliable results.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"A courtesy. I let them begin with their favorite subject.\"","\n","^\"Themselves.\"","\n","^His own answer completes the principle neatly enough that further instruction would be vanity.","\n",{"->":"class_middle_explanation"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"An experiment. It has produced reliable results.\"","\n","^\"Then I am part of your experiment.\"","\n","^He has sat opposite you long enough to become evidence.","\n",{"->":"class_middle_explanation"},{"#f":5}]}],null],"class_middle_followup_official":[["ev","str","^\"How convenient. The other two may deny responsibility.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I shall treat it with the respect due to paper.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"How convenient. The other two may deny responsibility.\"","\n","^\"I advise you not to make wit your first instrument at Hohenreith.\"","\n","^The demotion of wit to second instrument remains sufficiently theoretical to be safe.","\n",{"->":"class_middle_explanation"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then I shall treat it with the respect due to paper.\"","\n","^\"Paper has moved armies.\"","\n","^Paper has also buried mistakes, but not all corrections need to be spoken aloud.","\n",{"->":"class_middle_explanation"},{"#f":5}]}],null],"class_middle_explanation":["^You were born in that broad, anxious territory between deference and ambition. Your family had books, invoices, respectability, perhaps a piano no one played well enough, perhaps a father with an office, a mother with callers, brothers who were expected to advance, and daughters who were expected not to make advancement look hungry.","\n","^You learned accounts before etiquette, etiquette before French, and French before you learned how easily a woman with a calm voice could make men explain themselves. You rose because you listened. You rose because you understood that fraud, faith, medicine, gossip, politics, and grief all use the same doors into the human mind.","\n","^The court does not like to admit that it needs middle-class competence. It prefers to borrow it, dress it properly, and call it discretion.","\n","^Your reputation as a medium gave them a word that sounded less dangerous than investigator.","\n","^The salons that first laughed at you, then invited you back, learned your name before they learned what it cost.","\n",{"->":"choose_name_middle"},null],"class_working_background":[["^Viktor's courtesy is smooth enough to be handled without fingerprints. You hear, beneath it, the question of how much this compartment has improved you.","\n","ev","str","^\"Restraint is what people praise when they prefer not to see the effort.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"I am quiet because people often prefer women of my origin either grateful or invisible.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"I was trying not to touch the upholstery as though it might accuse me.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Restraint is what people praise when they prefer not to see the effort.\"","\n","^The newspaper in Viktor's hand creases once.","\n","^\"You object to being praised?\"","\n",{"->":"class_working_followup_praise"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"I am quiet because people often prefer women of my origin either grateful or invisible.\"","\n","^\"I did not ask you to be grateful.\"","\n",{"->":"class_working_followup_grateful"},{"#f":5}],"c-2":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^\"I was trying not to touch the upholstery as though it might accuse me.\"","\n","^Something like concern crosses his face, disguised too late as irritation.","\n","^\"The upholstery has survived ministers. It will survive you.\"","\n",{"->":"class_working_followup_upholstery"},{"#f":5}]}],null],"class_working_followup_praise":[["ev","str","^\"Only cheaply.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only when it hides the person doing the work.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Only cheaply.\"","\n","^\"That may be difficult to avoid.\"","\n","^Hohenreith, if it intends to praise you cheaply, will have to discover the economy of disappointment.","\n",{"->":"class_working_explanation"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"Only when it hides the person doing the work.\"","\n","^He studies you as if the answer has come from farther down the train than first class.","\n",{"->":"class_working_explanation"},{"#f":5}]}],null],"class_working_followup_grateful":[["ev","str","^\"No. You asked me to be manageable.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I shall postpone gratitude until you deserve it.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"No. You asked me to be manageable.\"","\n","^\"I asked you nothing of the kind.\"","\n","^Rank, unlike men, rarely needs to ask directly. The furniture asks for it.","\n",{"->":"class_working_explanation"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Then I shall postpone gratitude until you deserve it.\"","\n","^A pause. Then, very dryly: \"A generous arrangement.\"","\n",{"->":"class_working_explanation"},{"#f":5}]}],null],"class_working_followup_upholstery":[["ev","str","^\"Then I am already braver than I was.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"I shall take that as reassurance, though you delivered it like a reprimand.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"Then I am already braver than I was.\"","\n","^\"Courage measured against upholstery is not a military standard.\"","\n",{"->":"class_working_explanation"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"I shall take that as reassurance, though you delivered it like a reprimand.\"","\n","^\"I have had practice with both.\"","\n",{"->":"class_working_explanation"},{"#f":5}]}],null],"class_working_explanation":["^You were born among people who owned little but obligations. Work had a sound before it had a meaning: water, broom, bootsteps, breath, the clatter of dishes, the cough of men coming in from cold yards, women counting coins under their breath. You learned early that the high-born are not more observant than others. They are merely less often required to observe.","\n","^That was your first advantage.","\n","^A servant knows which door matters because she uses the others. A seamstress learns bodies because she measures them. A maid learns secrets because fine people leave their souls lying about like gloves, certain that no one beneath them has hands.","\n","^You rose by talent, patronage, imitation, nerve, and the terrible convenience of being believed harmless. By the time Vienna began whispering that you saw more than respectable people saw, you had already spent years seeing what respectable people missed.","\n","^The court has placed you in first class because it needs what birth did not give you.","\n","^The name you carried upward was altered perhaps in pronunciation, never quite cleansed of where it began.","\n",{"->":"choose_name_working"},null],"choose_name_noble":[["ev","str","^Valerie Eleonore Josepha","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Helene Cäcilie Franziska","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Clara Theresia Leopoldine","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Sophie Eleonore Auguste","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Mathilde Josepha Henriette","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Therese Valerie Franziska","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Ilona Theresia Eleonore","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Valerie Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Valerie","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-1":["\n","ev","str","^Helene Cäcilie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-2":["\n","ev","str","^Clara Theresia Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-3":["\n","ev","str","^Sophie Eleonore Auguste","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Sophie","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-4":["\n","ev","str","^Mathilde Josepha Henriette","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Mathilde","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-5":["\n","ev","str","^Therese Valerie Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-6":["\n","ev","str","^Ilona Theresia Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Ilona","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}],"c-7":["\n","ev","str","^Zdenka Eleonore Josepha","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Zdenka","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_noble"},{"#f":5}]}],null],"choose_surname_noble":[["^Your title is fixed by birth and by the careful modesty of your family: not countess, not princess, not one of the brilliant names that gather ambassadors and creditors like dust.","\n","^A Freiin. Baronial. Usable. Admitted, but not enthroned.","\n","ev","str","^Freiin von Rauhenfels","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Freiin von Traunegg","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Freiin von Ebenwald","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Freiin von Arnsberg","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Freiin von Reichenau","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Freiin von Waldstätten","/str","/ev",{"*":".^.c-5","flg":20},{"c-0":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rauhenfels","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Traunegg","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Ebenwald","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Arnsberg","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Reichenau","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Freiin von","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Waldstätten","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"choose_name_middle":[["ev","str","^Clara Eleonore","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Anna Katharina","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Helene Theresia","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Rosa Franziska","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Johanna Elise","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Katharina Sophie","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Therese Leopoldine","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Magdalena Cäcilie","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Clara Eleonore","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Clara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-1":["\n","ev","str","^Anna Katharina","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-2":["\n","ev","str","^Helene Theresia","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Helene","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-3":["\n","ev","str","^Rosa Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-4":["\n","ev","str","^Johanna Elise","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Johanna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-5":["\n","ev","str","^Katharina Sophie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Katharina","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-6":["\n","ev","str","^Therese Leopoldine","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Therese","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}],"c-7":["\n","ev","str","^Magdalena Cäcilie","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Magdalena","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_middle"},{"#f":5}]}],null],"choose_surname_middle":[["^Your family name contains no particle to soften the ascent. It must stand upright by itself.","\n","ev","str","^Leitner","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Wagner","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Kellner","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Baumgartner","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Fischer","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Schmid","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Pichler","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Rosenfeld","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Leitner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wagner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Kellner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Baumgartner","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Fischer","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schmid","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-6":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-7":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Rosenfeld","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"choose_name_working":[["ev","str","^Anna","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Klara","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Agnes","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Leni","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Rosa","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Gertrud","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Elisabeth","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Franziska","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Anna","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Anna","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-1":["\n","ev","str","^Klara","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Klara","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-2":["\n","ev","str","^Agnes","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Agnes","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-3":["\n","ev","str","^Leni","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Leni","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-4":["\n","ev","str","^Rosa","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Rosa","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-5":["\n","ev","str","^Gertrud","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Gertrud","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-6":["\n","ev","str","^Elisabeth","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Elisabeth","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}],"c-7":["\n","ev","str","^Franziska","/str","/ev",{"VAR=":"given_names","re":true},"ev","str","^Franziska","/str","/ev",{"VAR=":"common_name","re":true},{"->":"choose_surname_working"},{"#f":5}]}],null],"choose_surname_working":[["^A simple name can be a burden in Vienna. It tells people how little they must pretend to respect you before you have spoken.","\n","ev","str","^Pichler","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Huber","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Maier","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Gruber","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^Schuster","/str","/ev",{"*":".^.c-4","flg":20},"ev","str","^Krenn","/str","/ev",{"*":".^.c-5","flg":20},"ev","str","^Wolf","/str","/ev",{"*":".^.c-6","flg":20},"ev","str","^Moser","/str","/ev",{"*":".^.c-7","flg":20},{"c-0":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Pichler","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-1":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Huber","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-2":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Maier","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-3":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Gruber","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-4":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Schuster","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-5":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Krenn","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-6":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Wolf","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}],"c-7":["\n","ev","str","^Fräulein","/str","/ev",{"VAR=":"title_part","re":true},"ev","str","^Moser","/str","/ev",{"VAR=":"surname","re":true},{"->":"assemble_full_name"},{"#f":5}]}],null],"assemble_full_name":["ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^On visiting cards, in letters, in the cautious mouths of servants, you are ","ev",{"VAR?":"given_names"},"out","/ev","^ ","ev",{"VAR?":"title_part"},"out","/ev","^ ","ev",{"VAR?":"surname"},"out","/ev","^.","\n",{"->":".^.^.^.9"},null]}],[{"->":".^.b"},{"b":["\n","^On railway documents, hotel ledgers, and the tongues of people who have not yet decided how much respect you deserve, you are ","ev",{"VAR?":"title_part"},"out","/ev","^ ","ev",{"VAR?":"given_names"},"out","/ev","^ ","ev",{"VAR?":"surname"},"out","/ev","^.","\n",{"->":".^.^.^.9"},null]}],"nop","\n","^But in the private chamber where a name is first answered before it is performed, you are ","ev",{"VAR?":"common_name"},"out","/ev","^.","\n","^Viktor has waited through your silence with a soldier's patience and a jailer's courtesy. The train enters another tunnel. For several seconds the compartment window gives you back only your own reflection: your hat, your pale face above the dark collar, your eyes too steady or not steady enough.","\n","^When the mountains return, they seem closer.","\n",{"->":"mirror_definition"},null],"mirror_definition":[["^The black glass gives you the woman who will arrive in Eibenreith before any rumour has time to improve her.","\n","^The window catches one thing before the rest.","\n","ev","str","^Dark ash-brown hair pinned with almost severe care.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Chestnut hair arranged to look softer than the mind beneath it.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Cool fair hair made austere by dark travelling clothes.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^Black-brown hair with a few escaped wisps already refusing discipline.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^dark ash-brown","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has dark ash-brown hair, almost black where the tunnel has not quite left it, pinned with the severity of a woman who knows that disarray is forgiven less easily in the young.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-1":["\n","ev","str","^chestnut","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has chestnut hair, warm where the lamp touches it, arranged with enough softness to flatter and enough control to warn the attentive.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-2":["\n","ev","str","^cool fair","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has cool fair hair, not golden enough for sentimental painters, but pale enough that the dark travelling clothes make your face appear more deliberate than gentle.","\n",{"->":"mirror_complexion"},{"#f":5}],"c-3":["\n","ev","str","^black-brown","/str","/ev",{"VAR=":"hair_detail","re":true},"^The reflection has black-brown hair, glossy in the lamp's weak tremor, with two escaped wisps at the temple already committing small treasons against the pins.","\n",{"->":"mirror_complexion"},{"#f":5}]}],null],"mirror_complexion":[["^The window deepens, and with it the face.","\n","ev","str","^A fair, cool complexion that looks almost bloodless in railway light.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A clear complexion that still remembers the city more than the sun.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A face that has learned to look fragile when fragility is useful.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A slightly warmer complexion that makes the severity of the outfit less forgiving.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^fair and cool","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is fair and cool, made paler by smoke, glass, and the faint greenish cast of mountain light.","\n",{"->":"mirror_face"},{"#f":5}],"c-1":["\n","ev","str","^clear and sheltered","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion has the clarity of rooms, gloves, and shaded streets; not sickly, but visibly protected from the labour that browns other lives.","\n",{"->":"mirror_face"},{"#f":5}],"c-2":["\n","ev","str","^delicately pale","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is delicately pale, the sort physicians and foolish men read too eagerly, and which you have never felt obliged to correct every time.","\n",{"->":"mirror_face"},{"#f":5}],"c-3":["\n","ev","str","^warm fair","/str","/ev",{"VAR=":"complexion_detail","re":true},"^Your complexion is warmer than Vienna's winter light would prefer, and that warmth makes the severe coat and collar seem chosen rather than imposed.","\n",{"->":"mirror_face"},{"#f":5}]}],null],"mirror_face":[["^The window keeps you just long enough to make judgement impolite.","\n","ev","str","^A long oval face, observant before it is beautiful.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A composed face that seems trained equally for salons and interrogations.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A pretty face made less harmless by the steadiness of the eyes.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A severe face that becomes almost vulnerable only when caught unprepared.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^long oval and observant","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is long and oval, with watchful eyes, a straight nose, and a mouth that knows how often women are punished for amusement before men call them clever.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-1":["\n","ev","str","^composed and trained","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is composed rather than soft, with brows dark enough to sharpen silence and a mouth whose politeness has not yet promised mercy.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-2":["\n","ev","str","^pretty but steady","/str","/ev",{"VAR=":"face_detail","re":true},"^The face might be called pretty by people who do not like to work harder for adjectives, but the eyes disturb the compliment by appearing to have heard it before and found it insufficient.","\n",{"->":"mirror_outfit"},{"#f":5}],"c-3":["\n","ev","str","^severe and guarded","/str","/ev",{"VAR=":"face_detail","re":true},"^The face is severe at first glance, guarded at the second, and only after that does it betray how young a woman may still be while carrying herself like a sealed document.","\n",{"->":"mirror_outfit"},{"#f":5}]}],null],"mirror_outfit":[["^The rest of the reflection is costume, armour, and evidence.","\n","ev","str","^Dark charcoal-plum travelling wool, restrained but expensive.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^A severe black-brown coat and skirt, softened only by ivory at throat and cuffs.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Bottle-green details hidden in an otherwise sombre travelling suit.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A more fashionable ensemble, dark and narrow, calculated to be remembered.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","ev","str","^charcoal-plum travelling wool","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a tailored travelling ensemble of dark charcoal wool with a plum undertone, high at the throat, close at the waist, expensive in the way that avoids asking to be admired.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-1":["\n","ev","str","^black-brown travelling suit","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a black-brown travelling suit and long coat, softened only by ivory at throat and cuffs, the lace narrow enough to seem like restraint rather than decoration.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-2":["\n","ev","str","^sombre suit with bottle-green details","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a sombre travelling suit with bottle-green details so discreet that only close attention discovers them, which is nearly the point.","\n",{"->":"supernatural_stance"},{"#f":5}],"c-3":["\n","ev","str","^fashionable dark travelling ensemble","/str","/ev",{"VAR=":"outfit_detail","re":true},"^You wear a dark, narrow, more fashionable travelling ensemble, the hat and veil chosen with just enough theatrical instinct to make sceptical people use the word instinctive.","\n",{"->":"supernatural_stance"},{"#f":5}]}],null],"supernatural_stance":[["^The letter of commission in your reticule does not call you an investigator.","\n","^It calls you, in prose dry enough to pass through any number of offices, a woman whose unusual spiritual reputation has recommended her to a delicate household matter. The phrasing is exquisite. It neither affirms nor denies. It permits everyone involved to believe afterward that they had believed nothing improper.","\n","^The comital family at Jagdhaus Hohenreith has asked for discretion. Vienna has answered with a sealed letter, a woman reputed to speak with what is hidden, and a man opposite her who has orders of his own.","\n","^Before this journey, before this train, before the mountains began taking the sky piece by piece, belief had already taken its position in you.","\n","ev","str","^The dead are not silent. The living are merely poor listeners.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Belief is a costume. You wear it because men insist on dressing you in it.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^You have learned not to decide too early.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","#","^supernatural:believer","/#","ev","str","^believer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^The dead are not silent. The living are merely poor listeners.","\n","^You have always thought disbelief a provincial arrogance of the educated. There are pressures in rooms where grief has been. There are words people speak before they know they have spoken. There are dreams that arrive with mud on their hems.","\n","^Perhaps the world is not haunted. Perhaps it is simply crowded.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-1":["\n","#","^supernatural:sceptic","/#","#","^route:detective","/#","ev","str","^sceptic","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^The supernatural is usually pain, fraud, fever, inheritance, or bad ventilation.","\n","^The word spirit covers too much and explains too little. You have watched respectable people call an echo a message, a coincidence a sign, a trembling hand an angelic visitation. Men of science can be fools, but fools with candles and planchettes are no improvement.","\n","^If Hohenreith has ghosts, you expect them to keep accounts, write letters, leave footprints, and benefit someone.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-2":["\n","#","^supernatural:performer","/#","ev","str","^performer","/str","/ev",{"VAR=":"supernatural_belief","re":true},"ev",{"VAR?":"medium_reputation"},2,"+",{"VAR=":"medium_reputation","re":true},"/ev","^Belief is a costume. You wear it because men insist on dressing you in it.","\n","^You discovered early that men who distrust a woman's mind will sometimes worship her nerves. A conclusion from evidence irritates them. A vision, sighed through lowered lashes, makes them lean closer.","\n","^Very well. Let them lean.","\n",{"->":"spiritual_senses"},{"#f":5}],"c-3":["\n","#","^supernatural:undecided","/#","ev","str","^undecided","/str","/ev",{"VAR=":"supernatural_belief","re":true},"^You have learned not to decide too early.","\n","^There are things you can explain, things you cannot yet explain, and things that explanation damages before it helps. You have made a profession of standing at thresholds with a face composed enough for both sides to continue speaking.","\n","^Hohenreith will have to show you what kind of case it is.","\n",{"->":"spiritual_senses"},{"#f":5}]}],null],"spiritual_senses":[["^Belief is one matter. Experience is another.","\n","^People call a woman sensitive when they want her perceptions to sound like an illness. They call her hysterical when those perceptions inconvenience them. They call her inspired when they need her, and unstable when they do not.","\n","^Beneath reputation and performance, memory has its own testimony.","\n","ev","str","^There have been moments you cannot explain away.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Everything you do can be explained by observation, timing, and nerve.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Something happens, but never when summoned.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^You buried the first signs so thoroughly that even you do not know what remains.","/str","/ev",{"*":".^.c-3","flg":20},{"c-0":["\n","#","^powers:genuine","/#","ev","str","^genuine","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},2,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^There have been moments you cannot explain away.","\n","^Once, as a child, you knew before the telegram came. Once, in a crowded room, a stranger's grief entered you with such force that your own knees failed. Once, in a mirror, you saw a door behind you that was not in the room when you turned.","\n","^You learned caution after that. It is unwise for a woman to know things before a man has asked her opinion.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-1":["\n","#","^powers:faked","/#","#","^route:detective","/#","ev","str","^faked","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^Everything you do can be explained by observation, timing, and nerve.","\n","^You notice rings removed too recently, mourning gloves worn too carefully, letters folded and refolded until the crease gives away the reader's obsession. You hear servants misname guests, mothers pause before daughters' rooms, officers lie by becoming too exact.","\n","^The dead have never told you anything. The living cannot stop telling you everything.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-2":["\n","#","^powers:ambiguous","/#","ev","str","^ambiguous","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^Something happens, but never when summoned.","\n","^Your reputation depends upon command. The truth, if truth it is, has no respect for appointments.","\n","^Sometimes a room changes pressure around you. Sometimes a face acquires an old expression no living person taught it. Sometimes names arrive before introductions. But the harder you reach, the more ordinary the world becomes.","\n",{"->":"viktor_first_exchange"},{"#f":5}],"c-3":["\n","#","^powers:repressed","/#","#","^route:eccentric","/#","ev","str","^repressed","/str","/ev",{"VAR=":"supernatural_senses","re":true},"ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^You buried the first signs so thoroughly that even you do not know what remains.","\n","^There are childhood memories sealed behind politeness: a nursery mirror turned to the wall, a nurse dismissed without reference, your mother's hand tightening around your wrist until the bones complained.","\n","^You became strange afterward in ways society found easier to admire than understand.","\n",{"->":"viktor_first_exchange"},{"#f":5}]}],null],"viktor_first_exchange":[["^The train emerges from the tunnel into a pale afternoon cut by dark firs and white rock. Far below, water shows itself only in flashes. The valley is no longer a view from a salon painting. It has depth enough to hide things.","\n","^Viktor opens a leather folder and removes a memorandum. He does not hand it to you at once.","\n","^\"When we leave the railway,\" he says, \"we will be met by a coach from Hohenreith. From that moment, appearances matter. Your hosts have been told that I assist with correspondence, travel, and practical arrangements. They need not be troubled with military definitions.\"","\n","ev","str","^\"And the villagers?\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"How merciful. The empire has spared them vocabulary.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"You mean they are not to know whether I am guest, tool, or warning.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"And the villagers?\"","\n","^\"The villagers need not be troubled with anything.\"","\n","^There it is: the empire in miniature. A man, a folder, a locked sentence.","\n",{"->":"viktor_restraint_advice"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"How merciful. The empire has spared them vocabulary.\"","\n","^\"The empire has spared them alarm,\" Viktor says.","\n","^The empire's habit of confusing ignorance with calm does not require stating to remain present.","\n",{"->":"viktor_restraint_advice"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"You mean they are not to know whether I am guest, tool, or warning.\"","\n","^\"I mean they are to know only what steadies the situation.\"","\n","^The answer steadies nothing; that is not a contradiction.","\n",{"->":"viktor_restraint_advice"},{"#f":5}]}],null],"viktor_restraint_advice":[["^\"You will be addressed according to the station you present,\" he continues. \"The Graf's household will observe rank. Servants will observe what the household observes. Villagers may observe less and remember more. I advise restraint.\"","\n","^The advice is sound. That makes it no less irritating.","\n","ev","str","^\"If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"If you wish me to pass as harmless, you must stop warning me like a gaoler.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?\"","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^\"I shall do my best not to faint unless it is useful.\"","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^\"Restraint is what timid people call obedience after they have forgotten who trained them.\"","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev","str","^provocation","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"If gentlemen were less easily led, Herr Nowak, ladies would require fewer methods.\"","\n","^For the first time, amusement almost reaches his mouth.","\n","^\"A dangerous doctrine.\"","\n",{"->":"viktor_lover_followup"},{"#f":5}],"c-1":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","ev","str","^tension","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"If you wish me to pass as harmless, you must stop warning me like a gaoler.\"","\n","^His gaze sharpens.","\n","^\"I am not your gaoler.\"","\n",{"->":"viktor_sapphic_followup"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev","str","^professional","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Then let us be exact. What do they know, what do they suspect, and what am I permitted to verify?\"","\n","^He gives the smallest nod, as if you have chosen the only answer fit for adults.","\n","^\"They know that you come recommended. They suspect that you may be able to settle disturbances without police, priest, or press. You are permitted to verify fraud, coercion, threat to public order, or credible phenomena not presently classifiable.\"","\n",{"->":"viktor_detective_followup"},{"#f":5}],"c-3":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_trust"},1,"-",{"VAR=":"viktor_trust","re":true},"/ev","^\"I shall do my best not to faint unless it is useful.\"","\n","^Something in his expression tightens; not contempt exactly, but readiness.","\n","^\"I would prefer you did not faint at all.\"","\n",{"->":"viktor_careless_followup"},{"#f":5}],"c-4":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev","str","^challenge","/str","/ev",{"VAR=":"viktor_relation","re":true},"ev",{"VAR?":"viktor_suspicion"},2,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Restraint is what timid people call obedience after they have forgotten who trained them.\"","\n","^Viktor studies you as he might study an unfamiliar weapon found in luggage.","\n","^\"You enjoy making enemies.\"","\n",{"->":"viktor_eccentric_followup"},{"#f":5}]}],null],"viktor_lover_followup":[["ev","str","^\"A practical one.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Dangerous doctrines travel best in good gloves.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"A practical one.\"","\n","^\"You intend to practice it at Hohenreith?\"","\n",{"->":"viktor_lover_second"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Dangerous doctrines travel best in good gloves.\"","\n","^\"You intend to charm Hohenreith into confession?\"","\n","^If Hohenreith insists on being charmed, it will hardly be your fault.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_lover_second":[["ev","str","^\"Only where patriotism requires sacrifice.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only where men mistake desire for judgement.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Only where patriotism requires sacrifice.\"","\n","^He looks down at the memorandum, but not quickly enough to conceal that he is reassessing you.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Only where men mistake desire for judgement.\"","\n","^\"That may include more territory than the maps admit.\"","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_sapphic_followup":[["ev","str","^\"No. A gaoler is at least honest about the key.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then do not stand between me and every locked door before I have touched the handle.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"No. A gaoler is at least honest about the key.\"","\n","^The words surprise you by leaving a mark. Not on him, perhaps. On yourself. The closer the train carries you to Amalia's world, though you do not yet know her face, the more intolerable it seems that every female life there might be guarded by men who call the guarding concern.","\n","^Viktor folds the memorandum once, precisely.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Then do not stand between me and every locked door before I have touched the handle.\"","\n","^\"Some doors are locked for cause.\"","\n","^Any cause worthy of the lock should survive the indignity of being examined.","\n","^Viktor folds the memorandum once, precisely.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_detective_followup":[["ev","str","^\"Credible phenomena not presently classifiable.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"And if the phenomena become classifiable?\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Credible phenomena not presently classifiable.\"","\n","^\"That is the phrase.\"","\n","^The phrase files itself in your mind as a bureaucratic ghost.","\n","^\"The safest kind,\" he says.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","^\"And if the phenomena become classifiable?\"","\n","^\"Then we classify them before others do.\"","\n","^The sentence has the chill of a report written quickly over a grave.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_careless_followup":[["ev","str","^\"How ungallant.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then you must remain close enough to catch me.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"How ungallant.\"","\n","^\"How practical.\"","\n","^The burden of practicality passes toward him as gracefully as a fainting couch dragged into a field hospital.","\n","^His answer is delayed by half a breath.","\n","^\"That, gnädiges Fräulein, is precisely what concerns me.\"","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"Then you must remain close enough to catch me.\"","\n","^\"My orders did not specify theatrical collapses.\"","\n","^The omission does not flatter the thoroughness of his superiors.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_eccentric_followup":[["ev","str","^\"No. I dislike the laziness of letting fools remain undecided.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Enemies are merely people honest enough to stand in the right place.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"No. I dislike the laziness of letting fools remain undecided.\"","\n","^\"At Hohenreith, that dislike may become expensive.\"","\n","^If the Graf wanted docility, he could have invited someone cheaper.","\n",{"->":"viktor_explains_orders"},{"#f":5}],"c-1":["\n","^\"Enemies are merely people honest enough to stand in the right place.\"","\n","^\"You speak as though conflict were housekeeping.\"","\n","^Conflict has always been a form of housekeeping; one discovers what belongs where.","\n",{"->":"viktor_explains_orders"},{"#f":5}]}],null],"viktor_explains_orders":[["^The wheels strike a curve. The compartment leans. For a moment the two of you are held in the same narrow imbalance.","\n","^Viktor gives you the memorandum at last.","\n","^The document is not long. That is part of its menace. Long documents invite argument; short ones carry authority.","\n","^A comital household. A hunting residence in Upper Styria, not the family's principal seat. Reports of disturbances among servants and villagers. No police action requested. No public ecclesiastical inquiry desired. No press. No correspondence beyond approved channels. Your presence to be explained as a discreet consultation requested by the family. Herr Nowak to assist in practical matters.","\n","^No one has written the word ghost.","\n","^No one has written the word fraud.","\n","^No one has written the word daughter.","\n","^Yet the omissions arrange themselves around the page like furniture around a corpse.","\n","ev","str","^\"There is another instruction.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Your version is shorter than your silence. That means there is another instruction.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"How touching. Vienna trusts us both so little it had to divide the distrust.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"There is another instruction.\"","\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":"viktor_second_instruction"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Your version is shorter than your silence. That means there is another instruction.\"","\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":"viktor_second_instruction"},{"#f":5}],"c-2":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"How touching. Vienna trusts us both so little it had to divide the distrust.\"","\n","^Viktor does not ask how you know.","\n","^\"There is always another instruction,\" he says.","\n",{"->":"viktor_second_instruction"},{"#f":5}]}],null],"viktor_second_instruction":[["ev","str","^\"For you.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Concerning me.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Concerning whether I am fraud, fool, or useful animal.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"For you.\"","\n","^\"Yes.\"","\n",{"->":"viktor_instruction_concerns"},{"#f":5}],"c-1":["\n","^\"Concerning me.\"","\n","^\"Partly.\"","\n",{"->":"train_slows"},{"#f":5}],"c-2":["\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Concerning whether I am fraud, fool, or useful animal.\"","\n","^\"Partly,\" he says, and this time the honesty has a blade in it.","\n",{"->":"train_slows"},{"#f":5}]}],null],"viktor_instruction_concerns":[["ev","str","^\"Concerning me.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Concerning whether I am fraud, fool, or useful animal.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Concerning me.\"","\n","^\"Partly.\"","\n",{"->":"train_slows"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Concerning whether I am fraud, fool, or useful animal.\"","\n","^\"Partly,\" he says, and this time the honesty has a blade in it.","\n",{"->":"train_slows"},{"#f":5}]}],null],"train_slows":[["^The train begins to slow. The rhythm changes first in the floor, then in the window, then in the body. Houses gather beside the line. A station roof appears between drifting smoke and the dark combs of forested slopes. ","#","^sfx[steam-whistle.ogg]","/#","\n","ev","str","^\"Then I shall try to be worth the ink.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then I shall disappoint the instruction as creatively as circumstances permit.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Then keep your second instruction, Herr Nowak. I prefer primary sources.\"","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["\n","^\"Then I shall try to be worth the ink.\"","\n","^\"I sincerely hope so.\"","\n",{"->":"train_slows_end"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"Then I shall disappoint the instruction as creatively as circumstances permit.\"","\n","^\"I sincerely hope you do not.\"","\n",{"->":"train_slows_end"},{"#f":5}],"c-2":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then keep your second instruction, Herr Nowak. I prefer primary sources.\"","\n","^\"A preference not always granted in imperial service.\"","\n",{"->":"train_slows_end"},{"#f":5}]}],null],"train_slows_end":["^You cannot decide whether his answer is an insult, a prayer, or his first honest sentence.","\n",{"->":"railway_station"},null],"railway_station":[["^The station is small enough that the train seems briefly embarrassed to stop there. ","#","^chapter[The Station] ","/#","#","^image[muerzzuschlag.png](portrait)","/#","\n","^A porter in a cap too large for him hurries along the platform. A woman with a basket steps back from the steam as if from an animal. Somewhere beyond the station building, a cart horse stamps at frozen mud. The signboard gives the place a name you have seen in the timetable but will not remember with affection.","\n","^Your luggage descends in stages.","\n","ev","str","^A disciplined official set: trunk, dispatch case, hatbox, and black séance case.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^An elegant noblewoman's luggage: too correct to be accidental.","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^A performer's luggage: harmless on top, less harmless beneath.","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^A practical assortment that betrays too much preparation.","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^An excessive pile that makes concealment impossible.","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","ev","str","^official","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^First comes a sober travelling trunk with brass corners dulled by use, then a dispatch case, then a hatbox, then the narrow black case whose contents would embarrass both a priest and a conjurer if either searched it without imagination.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-1":["\n","ev","str","^elegant","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"class_confidence"},1,"+",{"VAR=":"class_confidence","re":true},"/ev","^First comes a large trunk in dark leather, then a second smaller one for linen, then a round hatbox, a fitted toilette case, and a reticule kept too close to your hand for any porter to misunderstand its importance.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-2":["\n","ev","str","^performer","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"medium_reputation"},1,"+",{"VAR=":"medium_reputation","re":true},"/ev","^First comes a respectable trunk, then a hatbox, then a travelling case of gloves, veils, ribbons, calling cards, and the small objects by which a room may be persuaded to believe in forces already present.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-3":["\n","ev","str","^practical","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^First comes a battered trunk reinforced at the corners, then a leather case with notebooks, pencils, folded maps, spare gloves, a hand-lamp, and enough small necessities to offend anyone who prefers women decorative.","\n",{"->":"station_luggage_common"},{"#f":5}],"c-4":["\n","ev","str","^excessive","/str","/ev",{"VAR=":"baggage_style","re":true},"ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^First comes one trunk, then another, then a hatbox, then a rug, then a dressing case, then the narrow black case, then a smaller parcel you had forgotten had survived packing. By the end even Viktor looks faintly outnumbered.","\n",{"->":"station_luggage_common"},{"#f":5}]}],null],"station_luggage_common":["^Viktor oversees the transfer with clipped civility. He does not carry like a servant. He directs like a man pretending not to command.","\n","^The coach from Hohenreith waits beyond the station yard: dark green paint, black wheels, the comital crest discreetly worn on the door, and two horses already restless beneath harness. The driver removes his hat when he sees you. Not too deeply. Deep enough for rank, not deep enough for reverence. ","#","^sfx[horse-neigh.ogg]","/#","\n","^\"Gnädiges Fräulein? Herr Sekretär?\"","\n","ev",{"VAR?":"birth_class"},"str","^noble","/str","==","/ev",[{"->":".^.b","c":true},{"b":["\n","^He has been told enough to place you. That is a courtesy. It is also a warning.","\n",{"->":".^.^.^.18"},null]}],[{"->":".^.b"},{"b":["\n","^He hesitates over you by the smallest measure. The hesitation is not rudeness. It is calculation. First-class carriage, court letter, no title beyond Fräulein, and a man beside you who looks like he has arrested people for less than staring.","\n",{"->":".^.^.^.18"},null]}],"nop","\n","^Viktor answers before you can.","\n","^\"From Jagdhaus Hohenreith?\"","\n","^\"Jawohl, Herr Sekretär. The road is passable. If the mist holds, we should reach Eibenreith before dark.\"","\n","^The word enters the air without ceremony.","\n","^Eibenreith.","\n","^Not Hohenreith, the name printed on the invitation in a clean hand. Eibenreith: the village below it. A smaller name. Older in the mouth. A name with roots rather than stationery.","\n",{"->":"coach_journey"},null],"coach_journey":[["^The coach leaves the station behind and with it the last easy evidence of empire. ","#","^chapter[The Graben] ","/#","#","^music[Kaiserpunk Jodler.mp3](crossfade, loop, lead=4)","/#","\n","^At first the road follows a valley where telegraph wire still keeps company with it and the river moves in a pale, stony bed. Sawmills, fenced meadows, and farmhouses appear and vanish behind stands of spruce. The mountains do not rise all at once. They advance by jurisdiction. A wooded slope claims the left-hand sky, then a grey wall of limestone closes the north, then another ridge gathers to the east until even the clouds seem to have entered service.","\n","^The driver names places when Viktor asks, but the names are local and practical, meant for men who know which bridge floods and which farm breeds stubborn horses. Somewhere beyond the visible ridges, he says, lies the great white back of the Hochschwab. Eastward, beyond forest and pass, the Hohe Veitsch keeps its own weather. He says this not as a guide would say it, but as a man explaining neighbours who may or may not be in a temper.","\n","^The main valley narrows.","\n","^The road turns from it into a side Graben, and the change is immediate. Sound alters. The wheels no longer ring against open distance but grind between banks, roots, and wet stone. The air smells of leaf mould, resin, and cold water. Yews appear among the firs in dark, improbable patience, their needles too black for the afternoon.","\n","^\"Eibenreither Graben,\" the driver says, and crosses himself so quickly that the gesture might have been meant for a rut in the road.","\n","^Viktor notices. Of course he notices.","\n","^\"Bad road?\" he asks.","\n","^\"Old road,\" the driver says.","\n","^No one speaks for a while.","\n","^You watch the trees.","\n","^There are forests that invite stories because they are pretty, and forests that reject stories because whatever happened there did not require witnesses. This one belongs to the second kind. Its trunks stand close, not wildly, but with the air of a crowd making room for something carried through it long ago. The snow that remains in hollows is not clean. It has gathered needles, bark, and a yellowish stain where water has risen underneath.","\n","^On a slope above the road, half swallowed by undergrowth, you glimpse stone.","\n","^A shrine, perhaps. A boundary marker. A figure. The coach has passed before your eyes can persuade themselves of its shape. For one instant you are left with the impression of a woman's head inclined not in prayer, but in listening. ","#","^image[statue.png](square)","/#","\n","ev",{"VAR?":"supernatural_senses"},"str","^genuine","/str","==",{"VAR?":"supernatural_senses"},"str","^ambiguous","/str","==","||",{"VAR?":"supernatural_senses"},"str","^repressed","/str","==","||","/ev",[{"->":".^.b","c":true},{"b":["\n","^The back of your neck tightens.","\n","^Not fear. Recognition would be worse.","\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev",{"->":".^.^.^.58"},null]}],[{"->":".^.b"},{"b":["\n","^You tell yourself that old stone, seen through moving branches, will become whatever the mind is cowardly enough to supply.","\n",{"->":".^.^.^.58"},null]}],"nop","\n","^Viktor has turned slightly toward the same slope.","\n","^\"Did you see something?\"","\n","ev","str","^\"A woman in the wood, perhaps. Or a stone that wanted to be one.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"A marker. I would like to know where that path leads.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"Only trees. The sort that make one grateful for gentlemen with revolvers.\"","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^\"Would you believe me if I said I had?\"","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^\"No.\"","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","#","^route:eccentric","/#","#","^statue_hint","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"A woman in the wood, perhaps. Or a stone that wanted to be one.\"","\n","^He studies the passing trees.","\n","^\"A local shrine?\"","\n",{"->":"statue_eccentric_followup"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","#","^statue_hint","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"A marker. I would like to know where that path leads.\"","\n","^\"You saw a path?\"","\n",{"->":"statue_detective_followup"},{"#f":5}],"c-2":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","ev","str","^dependence","/str","/ev",{"VAR=":"viktor_relation","re":true},"^\"Only trees. The sort that make one grateful for gentlemen with revolvers.\"","\n","^His expression darkens by one official degree.","\n","^\"A revolver is a poor instrument against trees.\"","\n",{"->":"statue_careless_followup"},{"#f":5}],"c-3":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","ev",{"VAR?":"viktor_suspicion"},1,"+",{"VAR=":"viktor_suspicion","re":true},"/ev","^\"Would you believe me if I said I had?\"","\n","^\"That would depend on what advantage you expected from the answer.\"","\n",{"->":"statue_lover_followup"},{"#f":5}],"c-4":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"No.\"","\n","^The denial is too quick, and you both hear it.","\n","^You are not thinking of the stone now. You are thinking of the young woman waiting somewhere ahead: the Graf's daughter, the reason carefully not written into the memorandum, the stranger whose household has summoned you under a title both absurd and useful.","\n",{"->":"statue_sapphic_followup"},{"#f":5}]}],null],"statue_eccentric_followup":[["ev","str","^\"If it is a shrine, it has not been loved recently.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"No. Shrines face the faithful. That thing was listening sideways.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"If it is a shrine, it has not been loved recently.\"","\n","^\"You speak as if stones notice neglect.\"","\n","^Soldiers, too, notice neglect. His silence admits enough.","\n","^He does not answer.","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"supernatural_exposure"},1,"+",{"VAR=":"supernatural_exposure","re":true},"/ev","^\"No. Shrines face the faithful. That thing was listening sideways.\"","\n","^Viktor's hand rests on the coach strap, still and ready.","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_detective_followup":[["ev","str","^\"Not clearly. Enough to ask later.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Only a suggestion of one. If it exists, someone maintains the absence of it.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Not clearly. Enough to ask later.\"","\n","^Viktor looks back through the small rear window. The bend has already erased the slope.","\n","^\"Ask carefully. Places people fail to mention are often more informative than those they recommend.\"","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Only a suggestion of one. If it exists, someone maintains the absence of it.\"","\n","^\"You make absences sound expensive.\"","\n","^They usually are; absence is expensive when someone maintains it.","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_careless_followup":[["ev","str","^\"Then I shall rely on your conversation to intimidate them.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"How unfortunate. You seemed so professionally reassuring.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Then I shall rely on your conversation to intimidate them.\"","\n","^The driver pretends not to hear. His shoulders, however, hear everything.","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"How unfortunate. You seemed so professionally reassuring.\"","\n","^\"I prefer enemies that identify themselves.\"","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_lover_followup":[["ev","str","^\"Herr Nowak. You wound me.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Then watch the slope, not my intentions. One of them may be useful.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"Herr Nowak. You wound me.\"","\n","^\"Not yet.\"","\n","^It is the first thing he has said all day that almost sounds like flirtation, though perhaps only because danger has a talent for borrowing warmer clothes.","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","ev",{"VAR?":"viktor_trust"},1,"+",{"VAR=":"viktor_trust","re":true},"/ev","^\"Then watch the slope, not my intentions. One of them may be useful.\"","\n","^He obeys without admitting that he has done so.","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"statue_sapphic_followup":[["ev","str","^\"It was only shadow.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"Or if I did, I prefer not to have it explained before I understand why it matters.\"","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^\"It was only shadow.\"","\n","^If this place keeps women in stone, you think, what does it do to them in houses?","\n",{"->":"coach_nears_village"},{"#f":5}],"c-1":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Or if I did, I prefer not to have it explained before I understand why it matters.\"","\n","^If this place keeps women in stone, you think, what does it do to them in houses?","\n",{"->":"coach_nears_village"},{"#f":5}]}],null],"coach_nears_village":[["^The Graben opens reluctantly.","\n","^First comes the smell of smoke. Then a roof, low and dark with weather. Then another. Then a church tower, not high, not graceful, but thick-shouldered and pale against the slope behind it. Its walls look older than the village around them and less certain of victory. The windows are small. The churchyard wall holds the road at a distance, as if the dead require fortification from the living, or the living from something else. ","#","^chapter[Eibenreith Village] ","/#","#","^sfx[church-bells.ogg](max=8, fade) ","/#","#","^image[eibenreith.png](landscape)","/#","\n","^Eibenreith appears not as a village in a picture appears, all at once and composed for admiration, but by fragments.","\n","^A woman in a dark kerchief pauses with a pail in her hand. A boy stops driving geese and lets them complain around his boots. Two men outside a shed end their conversation at the same moment without looking at each other. Curtains stir in windows where no one admits to standing. A blacksmith's sign moves slightly in air you cannot feel. Water runs somewhere under boards, under stone, under the road itself, quick and cold and hidden.","\n","^The houses are not poor, not exactly. Many are solid, whitewashed, shingled, kept with the stubborn decency of people who repair what they cannot replace. Yet something in their arrangement troubles the eye. They turn toward the church but not fully. They keep the road but lean from it. They leave, between yard and fence and woodpile, narrow passages where shadow gathers too early.","\n","^The coach slows.","\n","^No one runs to greet it.","\n","^No one needs to. News has already entered the village by means faster than railway, telegraph, or imperial seal.","\n","^You sit very straight as Eibenreith takes its first look at you.","\n","^Beside you, Viktor lowers his voice.","\n","^\"Remember: at Hohenreith, every courtesy will mean something. Here, every silence will.\"","\n","ev","str","^\"Then we are already being received.\"","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^\"You make it sound as if the village outranks the Graf.\"","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^\"How fortunate that I packed several silences.\"","/str","/ev",{"*":".^.c-2","flg":20},"ev","str","^\"I dislike being watched by people who will not introduce themselves.\"","/str","/ev",{"*":".^.c-3","flg":20},"ev","str","^\"If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts.\"","/str","/ev",{"*":".^.c-4","flg":20},{"c-0":["\n","#","^route:detective","/#","ev",{"VAR?":"detective"},1,"+",{"VAR=":"detective","re":true},"/ev","^\"Then we are already being received.\"","\n","^\"Yes,\" he says. \"And examined.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-1":["\n","#","^route:eccentric","/#","ev",{"VAR?":"eccentric"},1,"+",{"VAR=":"eccentric","re":true},"/ev","^\"You make it sound as if the village outranks the Graf.\"","\n","^\"No,\" Viktor says. \"Only as if it may have survived more than one.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-2":["\n","#","^route:lover","/#","ev",{"VAR?":"lover"},1,"+",{"VAR=":"lover","re":true},"/ev","^\"How fortunate that I packed several silences.\"","\n","^His mouth almost moves. \"Use the plainest one first.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-3":["\n","#","^route:careless","/#","ev",{"VAR?":"careless"},1,"+",{"VAR=":"careless","re":true},"/ev","^\"I dislike being watched by people who will not introduce themselves.\"","\n","^\"That,\" he says, \"is unlikely to improve today.\"","\n",{"->":"village_final_image"},{"#f":5}],"c-4":["\n","#","^route:sapphic","/#","ev",{"VAR?":"sapphic"},1,"+",{"VAR=":"sapphic","re":true},"/ev","^\"If Amalia has lived under this gaze all her life, I begin to understand why they sent for ghosts.\"","\n","^Viktor glances at you, but whatever answer he considers, he keeps it behind his teeth.","\n",{"->":"village_final_image"},{"#f":5}]}],null],"village_final_image":["^The horses draw the coach past the churchyard wall. Above it, on the old plaster beside the gate, a faded painted woman looks down from beneath a flaking blue mantle. Her hands are folded in prayer. Her eyes, damaged by weather, no longer point in the same direction.","\n","^For one breath, as the wheels pass over a buried runnel of water, the painted face seems less like the Holy Mother than like a mask put on something that had been waiting longer.","\n","^Then the coach enters the village proper, and the road bends toward the unseen height where Jagdhaus Hohenreith stands above Eibenreith under its newer name","\n","done",null],"global decl":["ev","str","^unset","/str",{"VAR=":"birth_class"},"str","^","/str",{"VAR=":"title_part"},"str","^","/str",{"VAR=":"given_names"},"str","^","/str",{"VAR=":"common_name"},"str","^","/str",{"VAR=":"surname"},"str","^unset","/str",{"VAR=":"baggage_style"},"str","^unset","/str",{"VAR=":"hair_detail"},"str","^unset","/str",{"VAR=":"complexion_detail"},"str","^unset","/str",{"VAR=":"face_detail"},"str","^unset","/str",{"VAR=":"outfit_detail"},"str","^unset","/str",{"VAR=":"supernatural_belief"},"str","^unset","/str",{"VAR=":"supernatural_senses"},"str","^unset","/str",{"VAR=":"viktor_relation"},0,{"VAR=":"lover"},0,{"VAR=":"sapphic"},0,{"VAR=":"detective"},0,{"VAR=":"careless"},0,{"VAR=":"eccentric"},0,{"VAR=":"class_confidence"},0,{"VAR=":"medium_reputation"},0,{"VAR=":"court_loyalty"},0,{"VAR=":"viktor_trust"},0,{"VAR=":"viktor_suspicion"},0,{"VAR=":"supernatural_exposure"},"/ev","end",null]}],"listDefs":{}}
\ No newline at end of file
diff --git a/dist/server.js b/dist/server.js
index 0d2daee..7b8a1a4 100644
--- a/dist/server.js
+++ b/dist/server.js
@@ -188,16 +188,6 @@ io.on('connection', (socket) => {
}
}
});
- // Start a new game
- socket.on('startGame', async () => {
- try {
- await handleGameApi(socket, 'newGame', []);
- }
- catch (error) {
- console.error('Error starting game:', error);
- socket.emit('error', { message: 'Failed to start game. Please try again.' });
- }
- });
// Process player command
socket.on('playerCommand', async (data) => {
try {
@@ -218,42 +208,6 @@ io.on('connection', (socket) => {
socket.emit('error', { message: 'Failed to process command. Please try again.' });
}
});
- // Save game state
- socket.on('saveGame', () => {
- try {
- const gameRunner = gameSessions.get(socket.id);
- if (!gameRunner) {
- socket.emit('error', { message: 'Game session not found. Please start a new game.' });
- return;
- }
- socket.data.saveGames.set(1, gameRunner.getGameState());
- socket.emit('gameSaved');
- }
- catch (error) {
- console.error('Error saving game:', error);
- socket.emit('error', { message: 'Failed to save game. Please try again.' });
- }
- });
- // Load game state
- socket.on('loadGame', async () => {
- try {
- const gameRunner = gameSessions.get(socket.id);
- if (!gameRunner) {
- socket.emit('error', { message: 'Game session not found. Please start a new game.' });
- return;
- }
- // Check if there's a saved game
- if (!socket.data.saveGames?.has(1)) {
- socket.emit('error', { message: 'No saved game found.' });
- return;
- }
- await handleGameApi(socket, 'loadGame', [1]);
- }
- catch (error) {
- console.error('Error loading game:', error);
- socket.emit('error', { message: 'Failed to load game. Please try again.' });
- }
- });
// Handle disconnection
socket.on('disconnect', () => {
console.log(`Client disconnected: ${socket.id}`);
diff --git a/dist/server.js.map b/dist/server.js.map
index 8f473c0..ab706ad 100644
--- a/dist/server.js.map
+++ b/dist/server.js.map
@@ -1 +1 @@
-{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/test-server.js b/dist/test-server.js
index 0b3575c..287aad3 100644
--- a/dist/test-server.js
+++ b/dist/test-server.js
@@ -167,17 +167,6 @@ io.on('connection', (socket) => {
}
}
});
- // Start a new game
- socket.on('startGame', async () => {
- try {
- console.log('Starting test game session');
- startDemoGame();
- }
- catch (error) {
- console.error('Error starting game:', error);
- socket.emit('error', { message: 'Failed to start game. Please try again.' });
- }
- });
// Process player command
socket.on('playerCommand', async (data) => {
try {
diff --git a/dist/test-server.js.map b/dist/test-server.js.map
index c9a1cc3..1faaae3 100644
--- a/dist/test-server.js.map
+++ b/dist/test-server.js.map
@@ -1 +1 @@
-{"version":3,"file":"test-server.js","sourceRoot":"","sources":["../src/test-server.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"test-server.js","sourceRoot":"","sources":["../src/test-server.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/public/css/style.css b/public/css/style.css
index 4846eee..7da10ad 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -102,6 +102,7 @@ html, body {
body {
overflow: hidden;
background-image: url('../images/brown-wooden-flooring.jpg');
+ /* background-image: url('../images/table.png'); */
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
@@ -383,6 +384,7 @@ ol.choice {
width: var(--book-width);
height: var(--book-height);
background-image: url('../images/book-3057904.png');
+ /* background-image: url('../images/book.png'); */
background-size: contain; /* Changed from cover to contain */
background-position: center;
background-repeat: no-repeat; /* Prevents repeating the image when aspect ratio doesn't match */
@@ -407,17 +409,20 @@ ol.choice {
}
#story {
- overflow-x: visible;
+ overflow: visible !important;
box-sizing: border-box;
overflow-anchor: none;
text-align: justify;
text-justify: inter-word;
margin-bottom: 0;
line-height: 1.5;
+ will-change: transform;
+ transform: translateY(0);
}
#paragraphs {
box-sizing: border-box;
+ overflow: visible !important;
overflow-anchor: none;
}
@@ -471,7 +476,7 @@ ol.choice {
.story-image-visible img {
opacity: 1;
clip-path: polygon(0 0, 220% 0, 0 220%);
- transition: opacity 900ms ease, clip-path 900ms ease;
+ transition: opacity 2000ms ease, clip-path 2000ms ease;
}
/* #story p span {
@@ -486,8 +491,11 @@ ol.choice {
/* background-color: rgba(200,200,200,0.5); */
right: 7%;
padding-bottom: 0;
- scroll-behavior: smooth;
- overscroll-behavior: contain;
+ overflow: hidden !important;
+ overflow-x: hidden !important;
+ overflow-y: hidden !important;
+ scroll-behavior: auto;
+ overscroll-behavior: none;
overflow-anchor: none;
scrollbar-width: none;
/* transform: translateX(-1%) translateY(2%) rotateX(0deg) rotateY(-1deg) rotateZ(0deg); */
@@ -497,17 +505,29 @@ ol.choice {
display: none;
}
+#page_right *,
+#page_right *::-webkit-scrollbar {
+ scrollbar-width: none;
+}
+
+#page_right *::-webkit-scrollbar {
+ display: none;
+}
+
#story_scrollbar {
- position: sticky;
- float: right;
- top: 0.4rem;
+ position: absolute;
+ top: 0;
+ right: 0.35rem;
+ bottom: 0;
width: 0.22rem;
- height: calc(100% - 0.8rem);
- margin-right: -1.5rem;
+ height: 100%;
+ margin: 0;
border-radius: 999px;
background: rgba(0, 0, 0, 0.12);
z-index: 12;
- pointer-events: none;
+ pointer-events: auto;
+ touch-action: none;
+ cursor: pointer;
}
#story_scrollbar_thumb {
@@ -519,6 +539,7 @@ ol.choice {
border-radius: inherit;
background: rgba(0, 0, 0, 0.72);
transition: top 260ms ease, height 260ms ease;
+ pointer-events: auto;
}
@@ -742,6 +763,7 @@ body:not([data-game-running="true"]) #command_history {
width: 100%;
margin: 0 0 1rem 0;
opacity: 0;
+ visibility: hidden;
transition: opacity 0.45s ease;
pointer-events: none;
}
@@ -750,6 +772,8 @@ body:not([data-game-running="true"]) #command_history {
display: none;
}
+html[data-history-restoring="true"] .story-choices,
+body:not([data-game-running="true"]) .story-choices,
html[data-process-state="waiting-generating"] .story-choices,
html[data-process-state="playing-generating"] .story-choices,
html[data-process-state="playing-ready"] .story-choices,
@@ -760,6 +784,7 @@ html[data-process-state="command-waiting"] .story-choices {
html[data-process-state="ready"] .story-choices[data-choice-ready="true"] {
opacity: 1;
+ visibility: visible;
pointer-events: auto;
}
diff --git a/public/images/book.png b/public/images/book.png
new file mode 100644
index 0000000..6b20cc5
Binary files /dev/null and b/public/images/book.png differ
diff --git a/public/images/table.png b/public/images/table.png
new file mode 100644
index 0000000..22d3e56
Binary files /dev/null and b/public/images/table.png differ
diff --git a/public/js/audio-manager-module.js b/public/js/audio-manager-module.js
index 202114b..7b7ffcd 100644
--- a/public/js/audio-manager-module.js
+++ b/public/js/audio-manager-module.js
@@ -22,6 +22,7 @@ class AudioManagerModule extends BaseModule {
this.activeTtsPlaybackCount = 0;
this.ttsQueueEmpty = true;
this.pendingMusicPlayback = null;
+ this.currentMusicState = null;
this.assetRoots = {
images: '/images/',
music: '/music/',
@@ -203,14 +204,14 @@ class AudioManagerModule extends BaseModule {
this.currentLoop = audio;
} else {
this.currentAudio = audio.cloneNode(true);
- this.currentAudio.volume = this.getSfxVolume();
+ this.setMediaVolume(this.currentAudio, this.getSfxVolume());
this.currentAudio.play().catch(error => {
console.error('Error playing audio:', error);
});
return this.currentAudio;
}
- audio.volume = this.getMusicVolume();
+ this.setMediaVolume(audio, this.getMusicVolume());
audio.play().catch(error => {
console.error('Error playing audio:', error);
});
@@ -233,14 +234,14 @@ class AudioManagerModule extends BaseModule {
}
this.currentLoop = new Audio(url);
this.currentLoop.loop = true;
- this.currentLoop.volume = this.getMusicVolume();
+ this.setMediaVolume(this.currentLoop, this.getMusicVolume());
this.currentLoop.play().catch(error => {
console.error('Error playing audio loop:', error);
});
return this.currentLoop;
} else {
this.currentAudio = new Audio(url);
- this.currentAudio.volume = this.getSfxVolume();
+ this.setMediaVolume(this.currentAudio, this.getSfxVolume());
this.currentAudio.play().catch(error => {
console.error('Error playing audio:', error);
});
@@ -331,19 +332,19 @@ class AudioManagerModule extends BaseModule {
updateVolumes() {
this.sounds.forEach(audio => {
const isMusic = audio.loop;
- audio.volume = this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume);
+ this.setMediaVolume(audio, this.masterVolume * (isMusic ? this.musicVolume : this.sfxVolume));
});
if (this.currentAudio) {
- this.currentAudio.volume = this.masterVolume * this.sfxVolume;
+ this.setMediaVolume(this.currentAudio, this.masterVolume * this.sfxVolume);
}
if (this.currentLoop) {
- this.currentLoop.volume = this.getMusicVolume();
+ this.setMediaVolume(this.currentLoop, this.getMusicVolume());
}
if (this.currentMusic) {
- this.currentMusic.volume = this.getMusicVolume();
+ this.setMediaVolume(this.currentMusic, this.getMusicVolume());
}
}
@@ -351,6 +352,11 @@ class AudioManagerModule extends BaseModule {
return Math.max(0, Math.min(1, Number.isFinite(Number(volume)) ? Number(volume) : 1));
}
+ setMediaVolume(audio, volume) {
+ if (!audio) return;
+ audio.volume = this.clampVolume(volume);
+ }
+
getSfxVolume() {
return this.masterVolume * this.sfxVolume;
}
@@ -387,8 +393,8 @@ class AudioManagerModule extends BaseModule {
const audio = this.currentMusic;
const token = ++this.musicFadeToken;
- const startVolume = audio.volume;
- const targetVolume = this.getUnduckedMusicVolume() * this.musicDuckingFactor;
+ const startVolume = this.clampVolume(audio.volume);
+ const targetVolume = this.clampVolume(this.getUnduckedMusicVolume() * this.musicDuckingFactor);
const start = performance.now();
const tick = () => {
@@ -396,7 +402,7 @@ class AudioManagerModule extends BaseModule {
return;
}
const progress = Math.min(1, (performance.now() - start) / duration);
- audio.volume = startVolume + ((targetVolume - startVolume) * progress);
+ this.setMediaVolume(audio, startVolume + ((targetVolume - startVolume) * progress));
if (progress < 1) {
requestAnimationFrame(tick);
}
@@ -428,7 +434,7 @@ class AudioManagerModule extends BaseModule {
const promise = new Promise((resolve, reject) => {
const audio = new Audio(url);
audio.preload = 'auto';
- audio.volume = this.getSfxVolume();
+ this.setMediaVolume(audio, this.getSfxVolume());
audio.addEventListener('canplaythrough', () => resolve(audio), { once: true });
audio.addEventListener('error', () => reject(new Error(`Failed to preload sound effect: ${url}`)), { once: true });
audio.load();
@@ -556,7 +562,7 @@ class AudioManagerModule extends BaseModule {
try {
const template = await this.preloadSfx(filename);
const audio = template.cloneNode(true);
- audio.volume = this.getSfxVolume();
+ this.setMediaVolume(audio, this.getSfxVolume());
this.currentAudio = audio;
const maxDuration = Math.max(0, Number(options.maxDurationSeconds || options.maxDuration || 0)) * 1000;
const endMode = String(options.endMode || options.mode || 'stop').toLowerCase().startsWith('fade') ? 'fade' : 'stop';
@@ -595,12 +601,12 @@ class AudioManagerModule extends BaseModule {
fadeOutAudio(audio, duration = 1000) {
if (!audio) return Promise.resolve(false);
- const startVolume = audio.volume;
+ const startVolume = this.clampVolume(audio.volume);
const startedAt = performance.now();
return new Promise(resolve => {
const step = () => {
const progress = Math.min(1, (performance.now() - startedAt) / duration);
- audio.volume = startVolume * (1 - progress);
+ this.setMediaVolume(audio, startVolume * (1 - progress));
if (progress < 1 && !audio.paused && !audio.ended) {
requestAnimationFrame(step);
return;
@@ -617,6 +623,8 @@ class AudioManagerModule extends BaseModule {
async playMusic(filename, mode = 'crossfade', options = {}) {
const url = this.getAssetUrl('music', filename);
const shouldLoop = options.loop !== false;
+ const startAt = Math.max(0, Number(options.startAt ?? options.currentTime ?? 0) || 0);
+ const fadeInSeconds = Math.max(0, Number(options.fadeInSeconds ?? options.fadeIn ?? 0) || 0);
if (mode === 'queue' && this.currentMusic && !this.currentMusic.paused) {
this.queuedMusic = { filename, mode: 'cut', options: { loop: shouldLoop } };
@@ -631,22 +639,44 @@ class AudioManagerModule extends BaseModule {
const next = new Audio(url);
next.loop = shouldLoop;
- next.volume = mode === 'crossfade' && this.currentMusic ? 0 : this.getMusicVolume();
+ this.setMediaVolume(next, (mode === 'crossfade' && this.currentMusic) || fadeInSeconds > 0 ? 0 : this.getMusicVolume());
+ if (startAt > 0) {
+ try {
+ next.currentTime = startAt;
+ } catch {
+ next.addEventListener('loadedmetadata', () => {
+ next.currentTime = Math.min(startAt, Number.isFinite(next.duration) ? next.duration : startAt);
+ }, { once: true });
+ }
+ }
next.addEventListener('ended', () => {
if (this.currentMusic === next) {
this.currentMusic = null;
+ this.currentMusicState = null;
}
});
+ const nextState = {
+ filename,
+ url,
+ loop: shouldLoop,
+ mode,
+ startedAt: Date.now()
+ };
if (mode === 'cut' || !this.currentMusic) {
this.stopCurrentMusic();
this.currentMusic = next;
+ this.currentMusicState = nextState;
await this.startMusicAudio(next, filename);
+ if (fadeInSeconds > 0) {
+ this.fadeAudioTo(next, this.getMusicVolume(), fadeInSeconds * 1000);
+ }
return next;
}
const previous = this.currentMusic;
this.currentMusic = next;
+ this.currentMusicState = nextState;
await this.startMusicAudio(next, filename);
this.crossfade(previous, next, 1500);
console.log(`AudioManager: Crossfading music to ${filename}`);
@@ -678,7 +708,7 @@ class AudioManagerModule extends BaseModule {
const pending = this.pendingMusicPlayback;
this.pendingMusicPlayback = null;
- pending.audio.volume = this.getMusicVolume();
+ this.setMediaVolume(pending.audio, this.getMusicVolume());
try {
await pending.audio.play();
@@ -697,17 +727,61 @@ class AudioManagerModule extends BaseModule {
this.currentMusic.pause();
this.currentMusic.currentTime = 0;
this.currentMusic = null;
+ this.currentMusicState = null;
+ }
+
+ getMusicState() {
+ if (!this.currentMusic || this.currentMusic.paused || this.currentMusic.ended || !this.currentMusicState?.filename) {
+ return null;
+ }
+
+ return {
+ filename: this.currentMusicState.filename,
+ currentTime: Math.max(0, Number(this.currentMusic.currentTime || 0)),
+ loop: Boolean(this.currentMusic.loop),
+ mode: this.currentMusicState.mode || 'cut',
+ volume: this.currentMusic.volume,
+ duckingFactor: this.musicDuckingFactor
+ };
+ }
+
+ async restoreMusicState(state = null) {
+ if (!state?.filename) return null;
+ return this.playMusic(state.filename, 'cut', {
+ loop: state.loop !== false,
+ startAt: Number(state.currentTime || 0),
+ fadeInSeconds: 1.5
+ });
+ }
+
+ fadeAudioTo(audio, targetVolume, duration = 1000) {
+ if (!audio) return Promise.resolve(false);
+ const startVolume = this.clampVolume(audio.volume);
+ const target = this.clampVolume(targetVolume);
+ const startedAt = performance.now();
+ return new Promise(resolve => {
+ const step = (now) => {
+ const progress = duration <= 0 ? 1 : Math.min(1, (now - startedAt) / duration);
+ this.setMediaVolume(audio, startVolume + ((target - startVolume) * progress));
+ if (progress < 1 && !audio.paused && !audio.ended) {
+ requestAnimationFrame(step);
+ return;
+ }
+ resolve(true);
+ };
+ requestAnimationFrame(step);
+ });
}
crossfade(previous, next, duration = 1500) {
const start = performance.now();
- const previousStart = previous ? previous.volume : 0;
- const target = this.getMusicVolume();
+ const previousStart = previous ? this.clampVolume(previous.volume) : 0;
+ const target = this.clampVolume(this.getMusicVolume());
const tick = () => {
const progress = Math.min(1, (performance.now() - start) / duration);
- if (previous) previous.volume = previousStart * (1 - progress);
- next.volume = target * progress;
+ if (previous) this.setMediaVolume(previous, previousStart * (1 - progress));
+ this.setMediaVolume(next, target * progress);
if (progress < 1) {
requestAnimationFrame(tick);
@@ -718,7 +792,7 @@ class AudioManagerModule extends BaseModule {
previous.pause();
previous.currentTime = 0;
}
- next.volume = this.getMusicVolume();
+ this.setMediaVolume(next, this.getMusicVolume());
};
tick();
@@ -747,7 +821,7 @@ class AudioManagerModule extends BaseModule {
}
const audio = this.currentAudio;
- const initialVolume = audio.volume;
+ const initialVolume = this.clampVolume(audio.volume);
const volumeStep = initialVolume / (duration / 50);
let currentVolume = initialVolume;
@@ -757,10 +831,10 @@ class AudioManagerModule extends BaseModule {
clearInterval(fadeInterval);
audio.pause();
audio.currentTime = 0;
- audio.volume = initialVolume; // Reset volume for future use
+ this.setMediaVolume(audio, initialVolume); // Reset volume for future use
resolve();
} else {
- audio.volume = currentVolume;
+ this.setMediaVolume(audio, currentVolume);
}
}, 50);
});
@@ -806,7 +880,7 @@ class AudioManagerModule extends BaseModule {
}
// Apply master volume and speech volume
- audio.volume = this.masterVolume * speechVolume * this._ttsVolume;
+ this.setMediaVolume(audio, this.masterVolume * speechVolume * this.ttsVolume);
// Set up cleanup
audio.onended = () => {
diff --git a/public/js/choice-display-module.js b/public/js/choice-display-module.js
index 33f41e2..bba4234 100644
--- a/public/js/choice-display-module.js
+++ b/public/js/choice-display-module.js
@@ -13,7 +13,7 @@ class ChoiceDisplayModule extends BaseModule {
this.container = null;
this.choices = [];
this.inputMode = 'text';
- this.processState = document.documentElement.dataset.processState || 'ready';
+ this.processState = document.documentElement.dataset.processState || 'loading';
this.template = {
cells: {
default: {
@@ -54,6 +54,14 @@ class ChoiceDisplayModule extends BaseModule {
this.addEventListener(document, 'story:process-state', (event) => {
this.handleProcessState(event.detail?.state || 'ready');
});
+ this.addEventListener(document, 'story:turn-start', () => {
+ this.processState = 'waiting-generating';
+ this.render();
+ });
+ this.addEventListener(document, 'story:history-restoring', (event) => {
+ document.documentElement.dataset.historyRestoring = event.detail?.active ? 'true' : 'false';
+ this.render();
+ });
this.addEventListener(document, 'keydown', this.handleKeyDown);
this.reportProgress(100, 'Choice display ready');
@@ -81,6 +89,9 @@ class ChoiceDisplayModule extends BaseModule {
} else if (!commandInput && this.container !== choicesRoot.lastElementChild) {
choicesRoot.appendChild(this.container);
}
+
+ this.container.hidden = true;
+ this.container.dataset.choiceReady = 'false';
}
handleChoices(choices) {
@@ -188,7 +199,13 @@ class ChoiceDisplayModule extends BaseModule {
if (!this.container) return;
this.container.innerHTML = '';
- const readyForChoices = this.inputMode === 'choice' && this.choices.length > 0 && this.processState === 'ready';
+ const restoringHistory = document.documentElement.dataset.historyRestoring === 'true';
+ const gameRunning = document.body?.dataset?.gameRunning === 'true';
+ const readyForChoices = gameRunning &&
+ !restoringHistory &&
+ this.inputMode === 'choice' &&
+ this.choices.length > 0 &&
+ this.processState === 'ready';
this.container.hidden = !readyForChoices;
this.container.dataset.choiceReady = readyForChoices ? 'true' : 'false';
if (this.container.hidden) {
diff --git a/public/js/debug-utils-module.js b/public/js/debug-utils-module.js
index 19fb1f9..26f4743 100644
--- a/public/js/debug-utils-module.js
+++ b/public/js/debug-utils-module.js
@@ -21,7 +21,6 @@ export class DebugUtilsModule extends BaseModule {
// Make utilities available globally for console access
window.DebugUtils = {
- testTextPipeline: this.testTextPipeline.bind(this),
testSocketConnection: this.testSocketConnection.bind(this),
testTTS: this.testTTS.bind(this),
forceReconnect: this.forceReconnect.bind(this)
@@ -32,26 +31,6 @@ export class DebugUtilsModule extends BaseModule {
return true;
}
- /**
- * Test the text processing pipeline with sample text
- * @param {string} text - Test text to process
- * @returns {boolean} - Success status
- */
- testTextPipeline(text = "This is a test sentence. Let's see if it displays correctly!") {
- console.log("Debug: Testing text pipeline with:", text);
-
- // Get the text buffer module properly through dependency system
- const textBuffer = this.getModule('text-buffer');
- if (!textBuffer) {
- console.error("Debug: TextBuffer module not found");
- return false;
- }
-
- textBuffer.addText(text);
- console.log("Debug: Text added to buffer");
- return true;
- }
-
/**
* Test the socket connection
* @returns {boolean} - Success status
diff --git a/public/js/game-loop-module.js b/public/js/game-loop-module.js
index 93cac48..1041eeb 100644
--- a/public/js/game-loop-module.js
+++ b/public/js/game-loop-module.js
@@ -31,6 +31,7 @@ class GameLoopModule extends BaseModule {
'updateUIState',
'refreshGameApiState',
'hasSaveGame',
+ 'queueUnrenderedHistoryBlocks',
'requestStartGame',
'requestSaveGame',
'requestLoadGame',
@@ -217,9 +218,13 @@ class GameLoopModule extends BaseModule {
const response = await socketClient.saveGame(1);
if (response?.success) {
const storyHistory = this.getModule('story-history');
+ const audioManager = this.getModule('audio-manager');
if (storyHistory && typeof storyHistory.saveSlot === 'function') {
await storyHistory.saveSlot(1, {
- inkState: response.savedState || null
+ inkState: response.savedState || null,
+ latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
+ renderedLineCount: storyHistory.renderedLineCount || 0,
+ musicState: audioManager?.getMusicState?.() || null
});
}
this.gameState.canLoad = true;
@@ -246,20 +251,57 @@ class GameLoopModule extends BaseModule {
}
await this.resetClientPlaybackAndDisplay();
+ document.dispatchEvent(new CustomEvent('story:history-restoring', {
+ detail: { active: true, reason: 'load-game' }
+ }));
if (browserSave?.gameId && storyHistory?.setCurrentGame) {
- storyHistory.setCurrentGame(browserSave.gameId, browserSave.latestBlockId || 0);
+ storyHistory.setCurrentGame(
+ browserSave.gameId,
+ browserSave.latestBlockId || 0,
+ browserSave.latestRenderedBlockId || 0,
+ browserSave.renderedLineCount || 0
+ );
}
const uiController = this.getModule('ui-controller');
if (browserSave && uiController?.displayHandler?.restoreFromHistory) {
await uiController.displayHandler.restoreFromHistory(browserSave);
}
+ const audioManager = this.getModule('audio-manager');
+ if (browserSave?.musicState && audioManager?.restoreMusicState) {
+ await audioManager.restoreMusicState(browserSave.musicState);
+ }
+ const hasUnrenderedHistory = browserSave &&
+ Number(browserSave.latestBlockId || 0) > Number(browserSave.latestRenderedBlockId || 0);
+ if (hasUnrenderedHistory) {
+ const sentenceQueue = this.getModule('sentence-queue');
+ sentenceQueue?.pauseBeforeNext?.('load-resume');
+ document.dispatchEvent(new CustomEvent('story:process-state', {
+ detail: { state: 'waiting-generating', reason: 'restoring-pending-output' }
+ }));
+ }
const response = await socketClient.loadGame(1, browserSave?.inkState || null);
+ if (response?.success && hasUnrenderedHistory) {
+ await this.queueUnrenderedHistoryBlocks(browserSave);
+ }
if (response?.success) {
this.gameState.started = true;
this.gameState.canSave = true;
this.gameState.canLoad = true;
this.updateUIState();
}
+ if (!hasUnrenderedHistory) {
+ document.dispatchEvent(new CustomEvent('story:history-restoring', {
+ detail: { active: false, reason: 'load-game-complete' }
+ }));
+ } else {
+ const clearRestoring = () => {
+ document.dispatchEvent(new CustomEvent('story:history-restoring', {
+ detail: { active: false, reason: 'pending-output-drained' }
+ }));
+ document.removeEventListener('tts:queue-empty', clearRestoring);
+ };
+ document.addEventListener('tts:queue-empty', clearRestoring);
+ }
}
async hasSaveGame(slot = 1) {
@@ -272,6 +314,17 @@ class GameLoopModule extends BaseModule {
return socketClient?.hasSaveGame ? socketClient.hasSaveGame(slot) : { success: false, result: false };
}
+ async queueUnrenderedHistoryBlocks(saveRecord = {}) {
+ const storyHistory = this.getModule('story-history');
+ const textBuffer = this.getModule('text-buffer');
+ if (!storyHistory || !textBuffer || typeof textBuffer.addBlocks !== 'function') return;
+ const start = Math.max(1, Number(saveRecord.latestRenderedBlockId || 0) + 1);
+ const end = Math.max(0, Number(saveRecord.latestBlockId || 0));
+ if (end < start) return;
+ const blocks = await storyHistory.getBlocksRange(saveRecord.gameId, start, end);
+ textBuffer.addBlocks(blocks);
+ }
+
async resetClientPlaybackAndDisplay() {
const playbackCoordinator = this.getModule('playback-coordinator');
if (playbackCoordinator && typeof playbackCoordinator.stop === 'function') {
diff --git a/public/js/sentence-queue-module.js b/public/js/sentence-queue-module.js
index 2afc628..3da0915 100644
--- a/public/js/sentence-queue-module.js
+++ b/public/js/sentence-queue-module.js
@@ -23,6 +23,7 @@ class SentenceQueueModule extends BaseModule {
this.autoplay = true;
this.inputMode = 'text';
this.lastContinueAt = 0;
+ this.pauseBeforeNextReason = null;
// Bind methods
this.bindMethods([
@@ -30,6 +31,7 @@ class SentenceQueueModule extends BaseModule {
'addSentence',
'processNextSentence',
'setOnSentenceReady',
+ 'pauseBeforeNext',
'completeSentence',
'getCacheKey',
'getPreparedSentence',
@@ -105,6 +107,10 @@ class SentenceQueueModule extends BaseModule {
this.onSentenceReadyCallback = callback;
}
}
+
+ pauseBeforeNext(reason = 'manual-pause') {
+ this.pauseBeforeNextReason = reason;
+ }
/**
* Add a sentence to the queue
@@ -139,6 +145,12 @@ class SentenceQueueModule extends BaseModule {
const item = this.sentenceQueue[0];
try {
+ if (this.pauseBeforeNextReason) {
+ const reason = this.pauseBeforeNextReason;
+ this.pauseBeforeNextReason = null;
+ await this.waitForManualContinue(reason);
+ }
+
const sentence = await this.getPreparedSentence(item);
// Prefetch far enough ahead that media pauses do not block TTS
@@ -294,6 +306,8 @@ class SentenceQueueModule extends BaseModule {
kind: metadata.type,
text: text || '',
turnId: metadata.turnId ?? null,
+ blockId: metadata.blockId ?? null,
+ gameId: metadata.gameId ?? null,
status: 'ready',
metadata: imageLayout ? { ...metadata, imageLayout } : metadata,
tts: { duration: 0, provider: null, audioData: null, play: null, stop: null, enabled: false },
@@ -325,6 +339,8 @@ class SentenceQueueModule extends BaseModule {
kind: metadata.type === 'heading' ? 'heading' : 'paragraph',
text,
turnId: metadata.turnId ?? null,
+ blockId: metadata.blockId ?? null,
+ gameId: metadata.gameId ?? null,
paragraphIndex: metadata.paragraphIndex ?? null,
isFirstParagraphInChapter: Boolean(metadata.isFirstParagraphInChapter),
role: metadata.role || (metadata.type === 'heading' ? 'chapter-heading' : 'body'),
@@ -746,20 +762,27 @@ class SentenceQueueModule extends BaseModule {
probe.remove();
const pageWidth = storyElement.clientWidth;
- const size = String(metadata.size || 'landscape').toLowerCase();
- const aspect = size === 'portrait' ? (9 / 16) : size === 'square' ? 1 : (16 / 9);
- const imageGap = lineHeight * 0.9;
- const maxWidth = size === 'portrait' ? pageWidth * 0.5 : pageWidth;
- const naturalHeight = maxWidth / aspect;
+ const requestedSize = String(metadata.size || 'landscape').toLowerCase();
+ const size = requestedSize === 'widescreen' ? 'landscape' : requestedSize;
+ const isPortrait = size === 'portrait';
+ const aspect = isPortrait ? (9 / 16) : size === 'square' ? 1 : (16 / 9);
+ const imageGap = lineHeight;
+ const maxOuterWidth = isPortrait ? pageWidth * 0.5 : pageWidth;
+ const maxImageWidth = isPortrait
+ ? Math.max(lineHeight * 4, maxOuterWidth - imageGap)
+ : maxOuterWidth;
+ const naturalHeight = maxImageWidth / aspect;
const imageLineCount = Math.max(1, Math.floor(naturalHeight / lineHeight));
- const height = imageLineCount * lineHeight;
- const width = Math.min(maxWidth, height * aspect);
- const verticalMargin = lineHeight / 2;
- const lineCount = imageLineCount + 1;
+ const verticalMargin = isPortrait ? lineHeight / 2 : 0;
+ const lineCount = isPortrait ? imageLineCount + 1 : imageLineCount;
+ const height = isPortrait
+ ? Math.max(lineHeight, (lineCount * lineHeight) - (verticalMargin * 2))
+ : imageLineCount * lineHeight;
+ const width = Math.min(maxImageWidth, height * aspect);
- if (size === 'portrait') {
+ if (isPortrait) {
this.activeImageWrap = {
- lines: lineCount,
+ lines: lineCount + 1,
width: width + imageGap,
imageWidth: width,
gap: imageGap,
diff --git a/public/js/socket-client-module.js b/public/js/socket-client-module.js
index 5de72c7..5e385a4 100644
--- a/public/js/socket-client-module.js
+++ b/public/js/socket-client-module.js
@@ -9,10 +9,11 @@ class SocketClientModule extends BaseModule {
super('socket-client', 'Socket Client');
// Dependencies
- this.dependencies = ['text-buffer', 'markup-parser'];
+ this.dependencies = ['text-buffer', 'markup-parser', 'story-history'];
this.socket = null;
this.textBuffer = null;
+ this.storyHistory = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
@@ -20,6 +21,8 @@ class SocketClientModule extends BaseModule {
this.url = null;
this.eventListeners = {};
this.defaultHost = 'localhost:3000';
+ this.receivedBlockCounter = 0;
+ this.receivedParagraphCounter = 0;
// Bind methods using parent's bindMethods utility
this.bindMethods([
@@ -44,6 +47,8 @@ class SocketClientModule extends BaseModule {
'setupGameEventHandlers',
'processTurnResult',
'processParagraphResult',
+ 'storeAndQueueBlocks',
+ 'normalizeHistoryBlock',
'dispatchTurnTags',
'isTimedCueTag',
'cueMarkersFromTags',
@@ -93,6 +98,11 @@ class SocketClientModule extends BaseModule {
console.error("Socket Client: Failed to get text-buffer module");
return false;
}
+ this.storyHistory = this.getModule('story-history');
+ if (!this.storyHistory) {
+ console.error("Socket Client: Failed to get story-history module");
+ return false;
+ }
this.reportProgress(50, "Setting up connection parameters");
@@ -191,7 +201,7 @@ class SocketClientModule extends BaseModule {
});
}
- processTurnResult(data) {
+ async processTurnResult(data) {
if (!data) return;
const turnId = Number(data.turnId);
@@ -199,6 +209,10 @@ class SocketClientModule extends BaseModule {
console.error('Socket Client: Invalid TurnResult received', data);
return;
}
+ if (turnId === 1) {
+ this.receivedBlockCounter = 0;
+ this.receivedParagraphCounter = 0;
+ }
if (Array.isArray(data.globalTags) && data.globalTags.length > 0) {
document.dispatchEvent(new CustomEvent('story:global-tags', {
@@ -214,12 +228,24 @@ class SocketClientModule extends BaseModule {
role: null,
cueTags: []
};
+ const turnBlocks = [];
data.paragraphs.forEach((paragraph) => {
- pendingParagraph = this.processParagraphResult(paragraph, turnId, pendingParagraph);
+ const result = this.processParagraphResult(paragraph, turnId, pendingParagraph);
+ pendingParagraph = result.pendingParagraph;
+ turnBlocks.push(...result.blocks);
});
- this.dispatchChoices(Array.isArray(data.choices) ? data.choices : []);
- this.dispatchInputMode(data.inputMode || (Array.isArray(data.choices) && data.choices.length > 0 ? 'choice' : 'text'));
+ await this.storeAndQueueBlocks(turnBlocks);
+
+ const choices = Array.isArray(data.choices) ? data.choices : [];
+ const inputMode = data.inputMode || (choices.length > 0 ? 'choice' : 'text');
+ this.dispatchChoices(choices);
+ this.dispatchInputMode(inputMode);
+ if (turnBlocks.length === 0 && choices.length > 0) {
+ document.dispatchEvent(new CustomEvent('story:process-state', {
+ detail: { state: 'ready', reason: 'choice-only-turn', turnId }
+ }));
+ }
}
dispatchTurnTags(tags, paragraph = null) {
@@ -259,15 +285,16 @@ class SocketClientModule extends BaseModule {
const immediateTags = tags.filter(tag => !this.isStructuralTag(tag) && !this.isTimedCueTag(tag));
this.dispatchTurnTags(immediateTags, paragraph);
- blocks.forEach(block => this.enqueueStructuredBlock(block));
-
if (!text) {
return {
- role: paragraphRole || pending.role || null,
- cueTags: [
- ...(Array.isArray(pending.cueTags) ? pending.cueTags : []),
- ...cueTags
- ]
+ blocks,
+ pendingParagraph: {
+ role: paragraphRole || pending.role || null,
+ cueTags: [
+ ...(Array.isArray(pending.cueTags) ? pending.cueTags : []),
+ ...cueTags
+ ]
+ }
};
}
@@ -279,7 +306,7 @@ class SocketClientModule extends BaseModule {
...cueTags
])
];
- this.enqueueStructuredBlock({
+ blocks.push({
type: 'paragraph',
text,
layoutText: paragraph.layoutText || text,
@@ -291,7 +318,50 @@ class SocketClientModule extends BaseModule {
turnId
});
- return { role: null, cueTags: [] };
+ return { blocks, pendingParagraph: { role: null, cueTags: [] } };
+ }
+
+ async storeAndQueueBlocks(blocks = []) {
+ if (!Array.isArray(blocks) || blocks.length === 0) return;
+
+ if (!this.storyHistory) {
+ this.storyHistory = this.getModule('story-history');
+ }
+
+ const normalizedBlocks = blocks.map(block => this.normalizeHistoryBlock(block));
+ let records = normalizedBlocks;
+ if (this.storyHistory && typeof this.storyHistory.recordBlocks === 'function') {
+ records = await this.storyHistory.recordBlocks(normalizedBlocks);
+ document.dispatchEvent(new CustomEvent('story:history-updated', {
+ detail: {
+ gameId: this.storyHistory.currentGameId || null,
+ latestBlockId: this.storyHistory.nextBlockId - 1
+ }
+ }));
+ } else {
+ console.warn('Socket Client: Story history unavailable; queueing unstored blocks');
+ }
+
+ records.forEach(block => this.enqueueStructuredBlock(block));
+ }
+
+ normalizeHistoryBlock(block) {
+ const type = String(block?.type || 'paragraph');
+ this.receivedBlockCounter += 1;
+ const normalized = {
+ ...block,
+ type,
+ id: block.id || `${type}-${block.turnId || 'turn'}-${this.receivedBlockCounter}`
+ };
+
+ if (type === 'paragraph') {
+ normalized.paragraphIndex = Number.isInteger(block.paragraphIndex)
+ ? block.paragraphIndex
+ : this.receivedParagraphCounter;
+ this.receivedParagraphCounter += 1;
+ }
+
+ return normalized;
}
isStructuralTag(tag) {
diff --git a/public/js/story-history-module.js b/public/js/story-history-module.js
index 70ae4f6..0a1c7b2 100644
--- a/public/js/story-history-module.js
+++ b/public/js/story-history-module.js
@@ -1,7 +1,6 @@
/**
* Story History Module
- * Stores rendered story blocks in IndexedDB and keeps only a short live window
- * in the page DOM.
+ * Stores received story output blocks in IndexedDB and tracks the render playhead.
*/
import { BaseModule } from './base-module.js';
@@ -11,12 +10,14 @@ class StoryHistoryModule extends BaseModule {
this.dependencies = ['persistence-manager'];
this.dbName = 'ttsAudioCacheDB';
- this.dbVersion = 2;
+ this.dbVersion = 3;
this.historyStore = 'storyHistoryStore';
this.saveStore = 'storySaveStore';
this.db = null;
this.currentGameId = null;
this.nextBlockId = 1;
+ this.latestRenderedBlockId = 0;
+ this.renderedLineCount = 0;
this.visibleLimit = 20;
this.bindMethods([
@@ -25,11 +26,18 @@ class StoryHistoryModule extends BaseModule {
'startNewGame',
'setCurrentGame',
'recordBlock',
+ 'recordBlocks',
+ 'markRendered',
+ 'updateBlockMetrics',
'saveSlot',
'loadSlot',
'hasSaveSlot',
'getSaveSlots',
'getBlocks',
+ 'getBlocksRange',
+ 'getWindowForTurn',
+ 'getRenderedLineCount',
+ 'findBlockForLine',
'clearGame',
'tx'
]);
@@ -52,6 +60,11 @@ class StoryHistoryModule extends BaseModule {
};
request.onupgradeneeded = () => {
const db = request.result;
+ if (!db.objectStoreNames.contains('audioCacheStore')) {
+ const audioStore = db.createObjectStore('audioCacheStore', { keyPath: 'hash' });
+ audioStore.createIndex('lastAccessed', 'lastAccessed', { unique: false });
+ audioStore.createIndex('size', 'size', { unique: false });
+ }
if (!db.objectStoreNames.contains(this.historyStore)) {
const historyStore = db.createObjectStore(this.historyStore, { keyPath: 'key' });
historyStore.createIndex('gameId', 'gameId', { unique: false });
@@ -72,14 +85,18 @@ class StoryHistoryModule extends BaseModule {
const gameId = `game-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
this.currentGameId = gameId;
this.nextBlockId = 1;
+ this.latestRenderedBlockId = 0;
+ this.renderedLineCount = 0;
const persistenceManager = this.getModule('persistence-manager');
persistenceManager?.updatePreference?.('app', 'currentGameId', gameId);
return gameId;
}
- setCurrentGame(gameId, latestBlockId = 0) {
+ setCurrentGame(gameId, latestBlockId = 0, latestRenderedBlockId = 0, renderedLineCount = 0) {
this.currentGameId = gameId || this.currentGameId;
this.nextBlockId = Math.max(1, Number(latestBlockId || 0) + 1);
+ this.latestRenderedBlockId = Math.max(0, Number(latestRenderedBlockId || 0));
+ this.renderedLineCount = Math.max(0, Number(renderedLineCount || 0));
}
recordBlock(block) {
@@ -99,6 +116,66 @@ class StoryHistoryModule extends BaseModule {
});
}
+ async recordBlocks(blocks = []) {
+ if (!Array.isArray(blocks) || blocks.length === 0) return [];
+ const records = [];
+ for (const block of blocks) {
+ const record = await this.recordBlock(block);
+ if (record) records.push(record);
+ }
+ return records;
+ }
+
+ markRendered(blockId) {
+ const rendered = Math.max(0, Number(blockId || 0));
+ if (rendered > this.latestRenderedBlockId) {
+ this.latestRenderedBlockId = rendered;
+ }
+ return this.latestRenderedBlockId;
+ }
+
+ async updateBlockMetrics(blockId, metrics = {}) {
+ if (!this.db || !this.currentGameId || blockId == null) return null;
+ const id = Math.max(1, Number(blockId || 1));
+ const key = `${this.currentGameId}:${id}`;
+ const record = await new Promise((resolve, reject) => {
+ const request = this.tx(this.historyStore).get(key);
+ request.onsuccess = () => resolve(request.result || null);
+ request.onerror = () => reject(request.error);
+ });
+ if (!record) return null;
+
+ const lineCount = Math.max(1, Number(metrics.lineCount || record.lineCount || 1));
+ const previousLineCount = Number(record.lineCount || 0);
+ const hadLineStart = Number.isFinite(Number(record.lineStart));
+ const lineStart = hadLineStart
+ ? Math.max(0, Number(record.lineStart))
+ : this.renderedLineCount;
+
+ const updated = {
+ ...record,
+ lineStart,
+ lineCount,
+ heightPx: Math.max(0, Number(metrics.heightPx || record.heightPx || 0)),
+ measuredLineHeightPx: Math.max(0, Number(metrics.lineHeightPx || record.measuredLineHeightPx || 0)),
+ metricsUpdatedAt: Date.now()
+ };
+
+ if (!hadLineStart) {
+ this.renderedLineCount = Math.max(this.renderedLineCount, lineStart + lineCount);
+ } else if (lineStart + previousLineCount >= this.renderedLineCount) {
+ this.renderedLineCount = Math.max(lineStart + lineCount, this.renderedLineCount + (lineCount - previousLineCount));
+ }
+
+ await new Promise((resolve, reject) => {
+ const request = this.tx(this.historyStore, 'readwrite').put(updated);
+ request.onsuccess = () => resolve(true);
+ request.onerror = () => reject(request.error);
+ });
+
+ return updated;
+ }
+
saveSlot(slot = 1, saveData = {}) {
if (!this.db) return Promise.resolve(false);
const record = {
@@ -106,6 +183,8 @@ class StoryHistoryModule extends BaseModule {
...saveData,
gameId: saveData.gameId || this.currentGameId,
latestBlockId: Math.max(0, this.nextBlockId - 1),
+ latestRenderedBlockId: Math.max(0, Number(saveData.latestRenderedBlockId ?? this.latestRenderedBlockId ?? 0)),
+ renderedLineCount: Math.max(0, Number(saveData.renderedLineCount ?? this.renderedLineCount ?? 0)),
savedAt: Date.now()
};
return new Promise((resolve, reject) => {
@@ -158,6 +237,86 @@ class StoryHistoryModule extends BaseModule {
});
}
+ getBlocksRange(gameId = this.currentGameId, startBlockId = 1, endBlockId = Infinity) {
+ if (!this.db || !gameId) return Promise.resolve([]);
+ const start = Math.max(1, Number(startBlockId || 1));
+ const end = Number.isFinite(endBlockId) ? Number(endBlockId) : Number.MAX_SAFE_INTEGER;
+ if (end < start) return Promise.resolve([]);
+
+ return new Promise((resolve, reject) => {
+ const blocks = [];
+ const index = this.tx(this.historyStore).index('gameOrder');
+ const range = IDBKeyRange.bound([gameId, start], [gameId, end]);
+ const request = index.openCursor(range, 'next');
+ request.onsuccess = () => {
+ const cursor = request.result;
+ if (!cursor) {
+ resolve(blocks);
+ return;
+ }
+ blocks.push(cursor.value);
+ cursor.continue();
+ };
+ request.onerror = () => reject(request.error);
+ });
+ }
+
+ async getWindowForTurn(gameId = this.currentGameId, turnId, visibleLimit = this.visibleLimit) {
+ if (!this.db || !gameId || turnId == null) return { blocks: [], targetBlockId: null };
+ const target = await new Promise((resolve, reject) => {
+ const index = this.tx(this.historyStore).index('gameId');
+ const request = index.openCursor(IDBKeyRange.only(gameId), 'next');
+ request.onsuccess = () => {
+ const cursor = request.result;
+ if (!cursor) {
+ resolve(null);
+ return;
+ }
+ if (String(cursor.value?.turnId) === String(turnId)) {
+ resolve(cursor.value);
+ return;
+ }
+ cursor.continue();
+ };
+ request.onerror = () => reject(request.error);
+ });
+
+ if (!target?.blockId) return { blocks: [], targetBlockId: null };
+ const latest = Math.max(0, this.nextBlockId - 1);
+ const limit = Math.max(1, Number(visibleLimit || this.visibleLimit));
+ const halfBefore = Math.floor(limit / 2);
+ const maxStart = Math.max(1, latest - limit + 1);
+ const start = Math.max(1, Math.min(maxStart, target.blockId - halfBefore));
+ const blocks = await this.getBlocksRange(gameId, start, Math.min(latest, start + limit - 1));
+ return { blocks, targetBlockId: target.blockId };
+ }
+
+ async getRenderedLineCount(gameId = this.currentGameId, latestRenderedBlockId = this.latestRenderedBlockId) {
+ const latest = Math.max(0, Number(latestRenderedBlockId || 0));
+ if (!this.db || !gameId || latest <= 0) return 0;
+ const blocks = await this.getBlocksRange(gameId, 1, latest);
+ const measured = blocks.reduce((max, block) => {
+ const start = Number(block.lineStart);
+ const count = Number(block.lineCount);
+ if (!Number.isFinite(start) || !Number.isFinite(count)) return max;
+ return Math.max(max, start + Math.max(1, count));
+ }, 0);
+ this.renderedLineCount = Math.max(this.renderedLineCount, measured);
+ return this.renderedLineCount;
+ }
+
+ async findBlockForLine(gameId = this.currentGameId, line = 0, latestRenderedBlockId = this.latestRenderedBlockId) {
+ const latest = Math.max(0, Number(latestRenderedBlockId || 0));
+ if (!this.db || !gameId || latest <= 0) return null;
+ const targetLine = Math.max(0, Number(line || 0));
+ const blocks = await this.getBlocksRange(gameId, 1, latest);
+ return blocks.find((block) => {
+ const start = Number(block.lineStart);
+ const count = Math.max(1, Number(block.lineCount || 1));
+ return Number.isFinite(start) && targetLine >= start && targetLine < start + count;
+ }) || blocks.at(-1) || null;
+ }
+
clearGame(gameId = this.currentGameId) {
if (!this.db || !gameId) return Promise.resolve();
return new Promise((resolve, reject) => {
diff --git a/public/js/text-buffer-module.js b/public/js/text-buffer-module.js
index ce5d445..cf580da 100644
--- a/public/js/text-buffer-module.js
+++ b/public/js/text-buffer-module.js
@@ -20,9 +20,8 @@ class TextBufferModule extends BaseModule {
// Bind methods using parent's bindMethods utility
this.bindMethods([
- 'addText',
'addBlock',
- 'splitIntoParagraphs',
+ 'addBlocks',
'processNextFromQueue',
'processSentences',
'processNextSentence',
@@ -86,56 +85,6 @@ class TextBufferModule extends BaseModule {
}
}
- /**
- * Add text to the buffer and process sentences
- * @param {string} text - Text to add to the buffer
- */
- addText(text) {
- if (!text) return;
-
- console.log(`TextBuffer: Adding text: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
-
- const blocks = this.markupParser && typeof this.markupParser.parse === 'function'
- ? this.markupParser.parse(text)
- : this.splitIntoParagraphs(text).map(paragraphText => ({
- type: 'paragraph',
- text: paragraphText,
- layoutText: paragraphText,
- cueMarkers: [],
- role: 'body',
- isFirstParagraphInChapter: false
- }));
-
- blocks.forEach(block => {
- if (block.type === 'paragraph') {
- const paragraphId = `paragraph-${this.paragraphCounter + 1}`;
- this.processingQueue.push({
- ...block,
- id: paragraphId,
- paragraphIndex: this.paragraphCounter,
- textBlockId: this.currentTextBlockId
- });
- this.paragraphCounter += 1;
- } else {
- this.processingQueue.push({
- ...block,
- id: `${block.type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
- });
-
- if (block.type === 'image') {
- this.currentTextBlockId += 1;
- }
- }
- });
-
- // Process the queue if not already processing
- if (!this.isProcessingActive && this.onSentenceReadyCallback) {
- this.processNextFromQueue();
- } else {
- console.log('TextBuffer: Text queued for processing');
- }
- }
-
/**
* Add an already parsed render block to the processing queue.
* Engine protocols should prefer this over re-serializing tags into text markup.
@@ -173,20 +122,11 @@ class TextBufferModule extends BaseModule {
}
}
- /**
- * Split an incoming narrative fragment into book paragraphs.
- * Single newlines inside a paragraph are normalized to spaces; blank lines
- * mark distinct paragraphs.
- * @param {string} text - Raw text fragment
- * @returns {Array
} - Normalized paragraph strings
- */
- splitIntoParagraphs(text) {
- return String(text)
- .split(/\n\s*\n/g)
- .map(paragraph => paragraph.replace(/\s*\n\s*/g, ' ').trim())
- .filter(Boolean);
+ addBlocks(blocks = []) {
+ if (!Array.isArray(blocks)) return;
+ blocks.forEach(block => this.addBlock(block));
}
-
+
/**
* Process the next text fragment from the queue
*/
@@ -223,7 +163,7 @@ class TextBufferModule extends BaseModule {
this.isProcessingActive = false;
}
}
-
+
/**
* Process complete sentences in the buffer
*/
diff --git a/public/js/tts-factory-module.js b/public/js/tts-factory-module.js
index f01ac13..0dfa5c3 100644
--- a/public/js/tts-factory-module.js
+++ b/public/js/tts-factory-module.js
@@ -32,7 +32,7 @@ class TTSFactoryModule extends BaseModule {
this.db = null; // Will hold the DB connection
this.dbName = 'ttsAudioCacheDB';
this.storeName = 'audioCacheStore';
- this.dbVersion = 2;
+ this.dbVersion = 3;
this.currentCacheSize = 0; // Track current size in bytes
this.maxCacheSizeBytes = 100 * 1024 * 1024; // 100 MB by default
this.cacheInitialized = false;
diff --git a/public/js/ui-controller-module.js b/public/js/ui-controller-module.js
index 57bfa2f..9920284 100644
--- a/public/js/ui-controller-module.js
+++ b/public/js/ui-controller-module.js
@@ -38,7 +38,6 @@ class UIControllerModule extends BaseModule {
this.bindMethods([
'initialize',
'handleCommand',
- 'displayText',
'setupBookInterface',
'applyBookSizing',
'setupEventListeners',
@@ -264,7 +263,7 @@ class UIControllerModule extends BaseModule {
});
this.addEventListener(document, 'click', (event) => {
- if (event.target && event.target.closest && event.target.closest('#options-modal, #controls, #player_input, #command_input')) {
+ if (event.target && event.target.closest && event.target.closest('#options-modal, #controls, #player_input, #command_input, #story_scrollbar')) {
return;
}
@@ -668,10 +667,9 @@ class UIControllerModule extends BaseModule {
console.log('UI Controller: Command sent successfully');
} else {
console.error('UI Controller: Failed to send command to socket');
- // Display an error message to the user
- this.displayHandler.displayText('⚠️ Unable to send command. Server connection might be lost.', {
- style: { color: '#990000' }
- });
+ document.dispatchEvent(new CustomEvent('story:process-state', {
+ detail: { state: 'ready', reason: 'command-send-failed' }
+ }));
}
} else {
console.error('UI Controller: Socket client not available for sending commands');
@@ -775,10 +773,6 @@ class UIControllerModule extends BaseModule {
}
}
- displayText(text, options = {}) {
- return this.displayHandler.displayText(text, options);
- }
-
clearDisplay() {
this.displayHandler.clear();
}
@@ -796,3 +790,4 @@ const uiController = new UIControllerModule();
// Export the module
export { uiController as UIController };
+
diff --git a/public/js/ui-display-handler-module.js b/public/js/ui-display-handler-module.js
index fe957ae..5317faa 100644
--- a/public/js/ui-display-handler-module.js
+++ b/public/js/ui-display-handler-module.js
@@ -9,7 +9,7 @@ class UIDisplayHandlerModule extends BaseModule {
super('ui-display-handler', 'UI Display Handler');
// Module dependencies
- this.dependencies = ['layout-renderer', 'playback-coordinator', 'game-config', 'localization', 'story-history', 'sentence-queue'];
+ this.dependencies = ['layout-renderer', 'playback-coordinator', 'game-config', 'localization', 'story-history', 'sentence-queue', 'persistence-manager'];
// DOM elements
this.container = null;
@@ -20,7 +20,23 @@ class UIDisplayHandlerModule extends BaseModule {
this.resizeTimer = null;
this.storyResizeObserver = null;
this.lastStoryMetrics = null;
- this.visibleBlockLimit = 20;
+ this.visibleBlockLimit = 41;
+ this.historyBufferBlocks = 20;
+ this.historyWindowStartId = 1;
+ this.historyWindowEndId = 0;
+ this.loadingHistoryPage = false;
+ this.draggingStoryScrollbar = false;
+ this.pendingHistoryWindowRequest = null;
+ this.historyWheelAccumulator = 0;
+ this.storyTopLine = 0;
+ this.storyOffsetPx = 0;
+ this.storyScrollAnimation = null;
+ this.storyScrollAnimationId = 0;
+ this.viewportLineCount = 1;
+ this.lineHeightPx = 24;
+ this.activeCenterBlockId = null;
+ this.historyEnsurePending = false;
+ this.lastEnsuredCenterBucket = null;
// Resources to preload
this.cssPath = '/css/style.css';
@@ -34,21 +50,44 @@ class UIDisplayHandlerModule extends BaseModule {
'initializeContainers',
'applyGameConfig',
'applyTranslations',
- 'displayText',
'renderSentence',
- 'recordRenderedItem',
+ 'markBlockRendered',
'trimVisibleBlocks',
'restoreFromHistory',
'renderStoredItem',
+ 'renderHistoryWindow',
+ 'renderHistoryWindowForTurn',
'loadPreviousHistoryPage',
+ 'loadNextHistoryPage',
+ 'loadHistoryWindowAt',
+ 'shiftHistoryWindow',
+ 'loadAdjacentHistoryBlock',
+ 'insertStoredElement',
+ 'trimVirtualWindow',
+ 'handleHistoryWheel',
+ 'disableAutoplayForManualScroll',
'updateStoryScrollbar',
+ 'handleStoryScrollbarPointer',
'handleDeferredMediaBlock',
'renderImageBlock',
+ 'revealImageBlock',
+ 'resolveImageUrl',
'calculateImageMetrics',
+ 'measureStoryLineHeight',
+ 'measureBlockLines',
+ 'recordRenderedMetrics',
+ 'setVirtualPadding',
+ 'setStoryOffset',
+ 'scrollStoryByLines',
+ 'setStoryTopLine',
+ 'getMaxStoryTopLine',
+ 'ensureLiveTailWindow',
+ 'ensureHistoryWindowForLine',
+ 'loadHistoryWindowAround',
'readFirstFiniteNumber',
'waitForSkippablePause',
'scrollStoryToEnd',
- 'animatePageScroll',
+ 'animateStoryOffset',
'scrollToTurn',
'handleStoryScroll',
'rerenderStory',
@@ -84,6 +123,7 @@ class UIDisplayHandlerModule extends BaseModule {
this.gameConfig = this.getModule('game-config');
this.localization = this.getModule('localization');
this.storyHistory = this.getModule('story-history');
+ this.persistenceManager = this.getModule('persistence-manager');
this.reportProgress(50, "Initializing display containers");
@@ -108,11 +148,22 @@ class UIDisplayHandlerModule extends BaseModule {
this.addEventListener(document, 'story:history-updated', (event) => {
this.updateStoryScrollbar(event.detail || {});
});
- this.addEventListener(document, 'wheel', (event) => {
- if (event.target?.closest?.('#page_right') && event.deltaY < 0 && this.pageRight?.scrollTop <= 2) {
- this.loadPreviousHistoryPage();
+ this.addEventListener(document, 'wheel', this.handleHistoryWheel, { passive: false });
+ this.addEventListener(document, 'keydown', (event) => {
+ const tagName = String(event.target?.tagName || '').toLowerCase();
+ if (['input', 'textarea', 'select'].includes(tagName) || event.altKey || event.ctrlKey || event.metaKey) {
+ return;
}
- }, { passive: true });
+ if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ this.disableAutoplayForManualScroll();
+ this.scrollStoryByLines(-3, true);
+ } else if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ this.disableAutoplayForManualScroll();
+ this.scrollStoryByLines(3, true);
+ }
+ });
this.addEventListener(document, 'story:process-state', (event) => {
const state = event.detail?.state || 'ready';
const remark = document.getElementById('remark_text');
@@ -316,6 +367,27 @@ class UIDisplayHandlerModule extends BaseModule {
storyScrollbar.innerHTML = '';
this.pageRight.appendChild(storyScrollbar);
}
+ const storyScrollbar = document.getElementById('story_scrollbar');
+ if (storyScrollbar && !storyScrollbar.dataset.historyScrollBound) {
+ storyScrollbar.dataset.historyScrollBound = 'true';
+ ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'dblclick'].forEach((type) => {
+ storyScrollbar.addEventListener(type, (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (type === 'pointerdown') {
+ this.handleStoryScrollbarPointer(event);
+ }
+ if (typeof event.stopImmediatePropagation === 'function') {
+ event.stopImmediatePropagation();
+ }
+ }, true);
+ });
+ storyScrollbar.addEventListener('wheel', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.handleHistoryWheel(event);
+ }, { passive: false });
+ }
// Create or find story container
this.container = document.getElementById('story');
@@ -361,10 +433,8 @@ class UIDisplayHandlerModule extends BaseModule {
console.log('UIDisplayHandler: All containers initialized');
this.applyGameConfig(this.gameConfig?.getConfig?.());
this.applyTranslations();
- if (this.pageRight && !this.pageRight.dataset.turnScrollBound) {
- this.pageRight.dataset.turnScrollBound = 'true';
- this.pageRight.addEventListener('scroll', this.handleStoryScroll, { passive: true });
- }
+ this.measureStoryLineHeight();
+ this.setStoryOffset(0);
}
applyGameConfig(config) {
@@ -447,21 +517,6 @@ class UIDisplayHandlerModule extends BaseModule {
* Display a local UI message outside the server turn protocol.
* Story output must flow through structured TurnResult objects instead.
*/
- displayText(text, options = {}) {
- if (!text) return Promise.resolve(null);
-
- console.warn('UIDisplayHandler.displayText called directly; story text should come from TurnResult');
-
- const sentenceQueue = this.getModule('sentence-queue');
- if (sentenceQueue) {
- return new Promise(resolve => {
- sentenceQueue.addSentence(text, () => resolve(null));
- });
- }
-
- return Promise.resolve(null);
- }
-
/**
* Render a prepared sentence to the display
* @param {Object} sentence - Prepared sentence object from SentenceQueue
@@ -477,6 +532,8 @@ class UIDisplayHandlerModule extends BaseModule {
}
try {
+ await this.ensureLiveTailWindow();
+
// Render DOM from layout data
const paragraphElement = this.layoutRenderer.renderParagraph(
sentence.layout,
@@ -486,24 +543,17 @@ class UIDisplayHandlerModule extends BaseModule {
paragraphElement.dataset.turnId = String(sentence.turnId);
paragraphElement.classList.add('story-turn-block');
}
-
- // Append to container
- if (this.paragraphContainer) {
- this.paragraphContainer.appendChild(paragraphElement);
- if (typeof this.layoutRenderer.adjustJustification === 'function') {
- this.layoutRenderer.adjustJustification(paragraphElement);
- }
- } else {
- console.error('UIDisplayHandler: Paragraph container not found');
- return null;
+ if (sentence.blockId != null) {
+ paragraphElement.dataset.storyBlockId = String(sentence.blockId);
+ this.markBlockRendered(sentence.blockId);
}
- // Store element reference in sentence
- sentence.element = paragraphElement;
- await this.recordRenderedItem({
+ const renderedItem = {
type: sentence.kind === 'heading' ? 'heading' : 'paragraph',
id: sentence.id,
turnId: sentence.turnId ?? null,
+ blockId: sentence.blockId ?? null,
+ gameId: sentence.gameId ?? null,
text: sentence.text,
metadata: {
layoutText: sentence.layout?.sourceLayoutText || sentence.text,
@@ -514,10 +564,36 @@ class UIDisplayHandlerModule extends BaseModule {
addTopSpace: sentence.addTopSpace,
paragraphIndex: sentence.paragraphIndex
}
- });
+ };
+
+ // Append to container
+ if (this.paragraphContainer) {
+ this.paragraphContainer.appendChild(paragraphElement);
+ this.renderedItems.push(renderedItem);
+ this.historyWindowStartId = this.renderedItems.find(item => item.blockId != null)?.blockId || this.historyWindowStartId;
+ this.historyWindowEndId = [...this.renderedItems].reverse().find(item => item.blockId != null)?.blockId || this.historyWindowEndId;
+ this.updateStoryScrollbar();
+ if (typeof this.layoutRenderer.adjustJustification === 'function') {
+ this.layoutRenderer.adjustJustification(paragraphElement);
+ }
+ const updated = await this.recordRenderedMetrics(sentence.blockId, paragraphElement);
+ if (updated) {
+ renderedItem.lineStart = updated.lineStart;
+ renderedItem.lineCount = updated.lineCount;
+ renderedItem.metadata.lineStart = updated.lineStart;
+ renderedItem.metadata.lineCount = updated.lineCount;
+ this.setVirtualPadding();
+ }
+ } else {
+ console.error('UIDisplayHandler: Paragraph container not found');
+ return null;
+ }
+
+ // Store element reference in sentence
+ sentence.element = paragraphElement;
await this.trimVisibleBlocks();
- this.scrollStoryToEnd(true);
+ await this.scrollStoryToEnd(true);
// Start coordinated playback (animation + TTS), including chapter headings.
await this.playbackCoordinator.play(sentence);
@@ -542,18 +618,24 @@ class UIDisplayHandlerModule extends BaseModule {
if (!sentenceQueue || typeof sentenceQueue.prepareLayout !== 'function') return;
console.log('UIDisplayHandler: Re-typesetting story after page resize');
- const scrollTop = this.pageRight ? this.pageRight.scrollTop : 0;
+ const storyTopLine = this.storyTopLine || 0;
this.paragraphContainer.innerHTML = '';
for (const item of this.renderedItems) {
if (item.type === 'image') {
const sentenceQueue = this.getModule('sentence-queue');
+ const metadata = {
+ ...item,
+ ...(item.metadata || {}),
+ turnId: item.turnId ?? item.metadata?.turnId,
+ blockId: item.blockId ?? item.metadata?.blockId
+ };
const imageLayout = sentenceQueue && typeof sentenceQueue.prepareImageLayout === 'function'
- ? await sentenceQueue.prepareImageLayout(item.metadata || {})
+ ? await sentenceQueue.prepareImageLayout(metadata)
: null;
this.renderImageBlock({
- ...(item.metadata || {}),
- imageLayout: imageLayout || item.metadata?.imageLayout
+ ...metadata,
+ imageLayout: imageLayout || metadata.imageLayout
}, false);
continue;
}
@@ -596,70 +678,97 @@ class UIDisplayHandlerModule extends BaseModule {
this.paragraphContainer.appendChild(paragraph);
}
- if (this.pageRight) {
- this.pageRight.scrollTop = scrollTop;
- }
+ this.measureStoryLineHeight();
+ this.setStoryTopLine(storyTopLine, false, { mode: 'rerender-preserve', ensure: false });
}
scrollStoryToEnd(smooth = true) {
- if (!this.pageRight) {
- return;
- }
-
- window.requestAnimationFrame(() => {
- this.animatePageScroll(
- Math.max(0, this.pageRight.scrollHeight - this.pageRight.clientHeight),
- smooth ? 720 : 0
- );
- });
+ this.measureStoryLineHeight();
+ const targetTopLine = this.getMaxStoryTopLine();
+ const storyHeight = this.paragraphContainer?.offsetHeight || 0;
+ const viewportHeight = this.pageRight?.clientHeight || 0;
+ const targetOffset = Math.min(0, viewportHeight - storyHeight);
+ this.storyTopLine = targetTopLine;
+ return this.animateStoryOffset(targetOffset, smooth ? 720 : 0, { mode: 'auto-bottom' });
}
async restoreFromHistory(saveRecord = {}) {
if (!this.paragraphContainer || !this.storyHistory || !saveRecord?.gameId) return;
- const sentenceQueue = this.getModule('sentence-queue');
- if (!sentenceQueue || typeof sentenceQueue.prepareLayout !== 'function') return;
-
+ const latestRenderedBlockId = Math.max(0, Number(saveRecord.latestRenderedBlockId || 0));
+ if (!this.storyHistory.renderedLineCount) {
+ await this.storyHistory.getRenderedLineCount(saveRecord.gameId, latestRenderedBlockId);
+ }
const blocks = await this.storyHistory.getBlocks(
saveRecord.gameId,
this.visibleBlockLimit,
- (saveRecord.latestBlockId || Number.MAX_SAFE_INTEGER) + 1
+ latestRenderedBlockId + 1
);
- this.paragraphContainer.innerHTML = '';
- this.renderedItems = [];
-
- for (const item of blocks) {
- await this.renderStoredItem(item);
- }
-
+ await this.renderHistoryWindow(blocks, 'bottom');
this.updateStoryScrollbar({ latestBlockId: saveRecord.latestBlockId || blocks.at(-1)?.blockId || 1 });
- this.scrollStoryToEnd(false);
}
- async renderStoredItem(item) {
+ insertStoredElement(element, placement = 'append') {
+ if (!this.paragraphContainer || !element) return;
+ if (placement === 'prepend') {
+ this.paragraphContainer.insertBefore(element, this.paragraphContainer.firstChild);
+ } else {
+ this.paragraphContainer.appendChild(element);
+ }
+ }
+
+ async renderStoredItem(item, placement = 'append') {
const sentenceQueue = this.getModule('sentence-queue');
if (!sentenceQueue) return null;
- this.renderedItems.push(item);
+ if (placement === 'prepend') {
+ this.renderedItems.unshift(item);
+ } else {
+ this.renderedItems.push(item);
+ }
if (item.type === 'image') {
+ const metadata = {
+ ...item,
+ ...(item.metadata || {}),
+ turnId: item.turnId ?? item.metadata?.turnId,
+ blockId: item.blockId ?? item.metadata?.blockId
+ };
const imageLayout = typeof sentenceQueue.prepareImageLayout === 'function'
- ? await sentenceQueue.prepareImageLayout(item.metadata || {})
+ ? await sentenceQueue.prepareImageLayout(metadata)
: null;
const imageElement = this.renderImageBlock({
- ...(item.metadata || {}),
- imageLayout: imageLayout || item.metadata?.imageLayout
- }, false);
+ ...metadata,
+ imageLayout: imageLayout || metadata.imageLayout
+ }, false, placement);
if (imageElement && item.blockId != null) imageElement.dataset.storyBlockId = String(item.blockId);
+ if (imageElement && Number.isFinite(Number(item.lineStart))) imageElement.dataset.lineStart = String(item.lineStart);
+ if (imageElement && Number.isFinite(Number(item.lineCount))) imageElement.dataset.lineCount = String(item.lineCount);
return imageElement;
}
if (item.type !== 'heading' && item.type !== 'paragraph') return null;
- const layout = await sentenceQueue.prepareLayout(item.text, item.metadata || {});
+ const metadata = {
+ ...(item.metadata || {}),
+ type: item.type,
+ role: item.role || item.metadata?.role || (item.type === 'heading' ? 'chapter-heading' : 'body'),
+ layoutText: item.layoutText || item.metadata?.layoutText || item.text,
+ isFirstParagraphInChapter: Boolean(item.isFirstParagraphInChapter ?? item.metadata?.isFirstParagraphInChapter),
+ dropCap: Boolean(item.dropCap ?? item.metadata?.dropCap),
+ addTopSpace: Boolean(item.addTopSpace ?? item.metadata?.addTopSpace),
+ paragraphIndex: item.paragraphIndex ?? item.metadata?.paragraphIndex,
+ cueMarkers: item.cueMarkers || item.metadata?.cueMarkers || [],
+ turnId: item.turnId ?? item.metadata?.turnId,
+ blockId: item.blockId ?? item.metadata?.blockId,
+ gameId: item.gameId ?? item.metadata?.gameId
+ };
+ const layout = await sentenceQueue.prepareLayout(item.text, metadata);
const element = this.layoutRenderer.renderParagraph(layout, { id: item.id });
if (item.turnId != null) {
element.dataset.turnId = String(item.turnId);
element.classList.add('story-turn-block');
}
if (item.blockId != null) element.dataset.storyBlockId = String(item.blockId);
+ if (Number.isFinite(Number(item.lineStart))) element.dataset.lineStart = String(item.lineStart);
+ if (Number.isFinite(Number(item.lineCount))) element.dataset.lineCount = String(item.lineCount);
element.querySelectorAll('.word').forEach(word => {
word.style.transition = 'none';
word.style.animation = 'none';
@@ -668,114 +777,495 @@ class UIDisplayHandlerModule extends BaseModule {
word.style.transform = 'translateY(0)';
word.style.clipPath = 'inset(0 0 0 0)';
});
- this.paragraphContainer.appendChild(element);
+ this.insertStoredElement(element, placement);
return element;
}
+ async renderHistoryWindow(blocks = [], scrollTarget = 'top') {
+ if (!this.paragraphContainer) return;
+ const previousTopLine = this.storyTopLine || 0;
+ this.paragraphContainer.innerHTML = '';
+ this.renderedItems = [];
+
+ for (const item of blocks) {
+ await this.renderStoredItem(item, 'append');
+ }
+
+ this.historyWindowStartId = blocks[0]?.blockId || 1;
+ this.historyWindowEndId = blocks.at(-1)?.blockId || 0;
+ this.setVirtualPadding();
+ this.updateStoryScrollbar();
+
+ await new Promise(resolve => {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ if (scrollTarget === 'bottom') {
+ this.scrollStoryToEnd(false);
+ } else if (scrollTarget === 'preserve') {
+ this.setStoryTopLine(previousTopLine, false, { mode: 'history-preserve', ensure: false });
+ } else {
+ const firstLine = Number(blocks[0]?.lineStart || 0);
+ this.setStoryTopLine(firstLine, false, { mode: 'history-top', ensure: false });
+ }
+ resolve();
+ });
+ });
+ });
+ }
+
+ async renderHistoryWindowForTurn(turnId) {
+ if (!this.storyHistory || !this.paragraphContainer || turnId == null) return null;
+ const result = await this.storyHistory.getWindowForTurn(
+ this.storyHistory.currentGameId,
+ turnId,
+ this.visibleBlockLimit
+ );
+ if (!result?.blocks?.length) return null;
+ await this.renderHistoryWindow(result.blocks, 'top');
+ return result.targetBlockId;
+ }
+
async loadPreviousHistoryPage() {
if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage) return;
- const firstBlock = this.paragraphContainer.querySelector('[data-story-block-id]');
- const beforeBlockId = Number(firstBlock?.dataset?.storyBlockId || 0);
+ const beforeBlockId = this.historyWindowStartId || Number(this.paragraphContainer.querySelector('[data-story-block-id]')?.dataset?.storyBlockId || 0);
if (!beforeBlockId || beforeBlockId <= 1) return;
+ await this.loadAdjacentHistoryBlock(-1);
+ }
+
+ async loadNextHistoryPage() {
+ if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage || !this.hasNewerHistory()) return;
+ await this.loadAdjacentHistoryBlock(1);
+ }
+
+ async shiftHistoryWindow(deltaBlocks, scrollTarget = 'top') {
+ if (!this.storyHistory) return;
+ if (Math.abs(Number(deltaBlocks) || 0) === 1 && scrollTarget === 'preserve') {
+ await this.loadAdjacentHistoryBlock(deltaBlocks);
+ return;
+ }
+ const latest = this.getLatestHistoryBlockId();
+ const visible = Math.min(this.visibleBlockLimit, latest || this.visibleBlockLimit);
+ const maxStart = Math.max(1, latest - visible + 1);
+ const start = Math.max(1, Math.min(maxStart, (this.historyWindowStartId || 1) + deltaBlocks));
+ if (start === this.historyWindowStartId) return;
+ await this.loadHistoryWindowAt(start, scrollTarget);
+ }
+
+ handleHistoryWheel(event) {
+ if (!event.target?.closest?.('#page_right') || !this.pageRight) return;
+ event.preventDefault();
+ event.stopPropagation();
+ this.disableAutoplayForManualScroll();
+ const lineDelta = (Number(event.deltaY || 0) / Math.max(8, this.lineHeightPx || 24)) * 0.85;
+ this.scrollStoryByLines(lineDelta, true);
+ }
+
+ async loadAdjacentHistoryBlock(direction) {
+ if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage) {
+ if (direction) {
+ const startBlockId = Math.max(1, (this.historyWindowStartId || 1) + Math.sign(direction));
+ this.pendingHistoryWindowRequest = { startBlockId, scrollTarget: 'preserve' };
+ }
+ return;
+ }
+
+ const step = Math.sign(Number(direction) || 0);
+ if (!step) return;
+ const latest = this.getLatestHistoryBlockId();
+ const targetBlockId = step < 0
+ ? (this.historyWindowStartId || 1) - 1
+ : (this.historyWindowEndId || 0) + 1;
+ if (targetBlockId < 1 || targetBlockId > latest) return;
this.loadingHistoryPage = true;
try {
- const blocks = await this.storyHistory.getBlocks(
- this.storyHistory.currentGameId,
- this.visibleBlockLimit,
- beforeBlockId
- );
- if (!blocks.length) return;
- this.paragraphContainer.innerHTML = '';
- this.renderedItems = [];
- for (const item of blocks) {
- await this.renderStoredItem(item);
+ const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, targetBlockId, targetBlockId);
+ const block = blocks[0];
+ if (!block) return;
+
+ await this.renderStoredItem(block, step < 0 ? 'prepend' : 'append');
+ this.historyWindowStartId = Math.min(this.historyWindowStartId || targetBlockId, targetBlockId);
+ this.historyWindowEndId = Math.max(this.historyWindowEndId || targetBlockId, targetBlockId);
+
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+
+ await this.trimVirtualWindow(step);
+ this.setVirtualPadding();
+ this.updateStoryScrollbar();
+ } finally {
+ this.loadingHistoryPage = false;
+ const pending = this.pendingHistoryWindowRequest;
+ this.pendingHistoryWindowRequest = null;
+ if (pending && Number(pending.startBlockId) !== this.historyWindowStartId) {
+ await this.loadAdjacentHistoryBlock(Number(pending.startBlockId) < this.historyWindowStartId ? -1 : 1);
+ }
+ }
+ }
+
+ async trimVirtualWindow(direction = 1) {
+ if (!this.paragraphContainer) return;
+ const excess = this.renderedItems.length - this.visibleBlockLimit;
+ if (excess <= 0) return;
+
+ for (let index = 0; index < excess; index += 1) {
+ if (direction > 0) {
+ const removed = this.renderedItems.shift();
+ const element = removed?.blockId != null
+ ? this.paragraphContainer.querySelector(`[data-story-block-id="${CSS.escape(String(removed.blockId))}"]`)
+ : null;
+ element?.remove();
+ } else {
+ const removed = this.renderedItems.pop();
+ const element = removed?.blockId != null
+ ? this.paragraphContainer.querySelector(`[data-story-block-id="${CSS.escape(String(removed.blockId))}"]`)
+ : null;
+ element?.remove();
+ }
+ }
+
+ this.historyWindowStartId = this.renderedItems.find(item => item.blockId != null)?.blockId || 1;
+ this.historyWindowEndId = [...this.renderedItems].reverse().find(item => item.blockId != null)?.blockId || 0;
+ this.setVirtualPadding();
+ }
+
+ async loadHistoryWindowAt(startBlockId, scrollTarget = 'top') {
+ if (!this.storyHistory) return;
+ if (this.loadingHistoryPage) {
+ this.pendingHistoryWindowRequest = { startBlockId, scrollTarget };
+ return;
+ }
+ const latest = this.getLatestHistoryBlockId();
+ const maxStart = Math.max(1, latest - this.visibleBlockLimit + 1);
+ const start = Math.max(1, Math.min(maxStart, Number(startBlockId || 1)));
+ const end = Math.min(latest, start + this.visibleBlockLimit - 1);
+
+ this.loadingHistoryPage = true;
+ try {
+ const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, start, end);
+ if (blocks.length) {
+ await this.renderHistoryWindow(blocks, scrollTarget);
+ }
+ } finally {
+ this.loadingHistoryPage = false;
+ const pending = this.pendingHistoryWindowRequest;
+ this.pendingHistoryWindowRequest = null;
+ if (pending && Number(pending.startBlockId) !== this.historyWindowStartId) {
+ await this.loadHistoryWindowAt(pending.startBlockId, pending.scrollTarget);
+ }
+ }
+ }
+
+ markBlockRendered(blockId) {
+ if (this.storyHistory && typeof this.storyHistory.markRendered === 'function') {
+ const latestRenderedBlockId = this.storyHistory.markRendered(blockId);
+ document.dispatchEvent(new CustomEvent('story:history-updated', {
+ detail: {
+ gameId: this.storyHistory.currentGameId || null,
+ latestBlockId: this.getLatestHistoryBlockId(),
+ latestRenderedBlockId
+ }
+ }));
+ }
+ }
+
+ getLatestHistoryBlockId() {
+ return Math.max(0, Number(this.storyHistory?.latestRenderedBlockId || 0));
+ }
+
+ hasNewerHistory() {
+ return this.historyWindowEndId < this.getLatestHistoryBlockId();
+ }
+
+ updateStoryScrollbar(detail = {}) {
+ const track = document.getElementById('story_scrollbar');
+ const thumb = document.getElementById('story_scrollbar_thumb');
+ if (!thumb) return;
+ this.measureStoryLineHeight();
+ const totalLines = Math.max(1, Number(detail.renderedLineCount || this.storyHistory?.renderedLineCount || 0));
+ const viewportLines = Math.max(1, this.viewportLineCount || 1);
+ const visibleLines = Math.min(viewportLines, totalLines);
+ const maxTopLine = Math.max(0, totalLines - visibleLines);
+ const currentTop = Math.max(0, Math.min(maxTopLine, this.storyTopLine || 0));
+ const heightPercent = Math.max(8, Math.min(100, (visibleLines / totalLines) * 100));
+ const topPercent = maxTopLine <= 0 ? 0 : (currentTop / maxTopLine) * (100 - heightPercent);
+ if (track) {
+ track.dataset.totalLines = String(totalLines);
+ track.dataset.viewportLines = String(viewportLines);
+ track.dataset.topLine = String(currentTop);
+ track.hidden = totalLines <= viewportLines;
+ }
+ thumb.style.height = `${heightPercent}%`;
+ thumb.style.top = `${topPercent}%`;
+ }
+
+ handleStoryScrollbarPointer(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.disableAutoplayForManualScroll();
+ const track = event.currentTarget;
+ if (!track) return;
+ const moveToPointer = (pointerEvent) => {
+ const rect = track.getBoundingClientRect();
+ const ratio = Math.max(0, Math.min(1, (pointerEvent.clientY - rect.top) / Math.max(1, rect.height)));
+ this.setStoryTopLine(this.getMaxStoryTopLine() * ratio, true, { mode: 'manual' });
+ };
+
+ moveToPointer(event);
+ const onMove = (moveEvent) => moveToPointer(moveEvent);
+ const onUp = () => {
+ document.removeEventListener('pointermove', onMove);
+ document.removeEventListener('pointerup', onUp);
+ };
+ document.addEventListener('pointermove', onMove);
+ document.addEventListener('pointerup', onUp, { once: true });
+ }
+
+ disableAutoplayForManualScroll() {
+ if (!this.persistenceManager || typeof this.persistenceManager.updatePreference !== 'function') {
+ console.error('UIDisplayHandler: Cannot disable autoplay; persistence-manager dependency is unavailable.');
+ return;
+ }
+ this.persistenceManager.updatePreference('app', 'autoplay', false);
+ document.dispatchEvent(new CustomEvent('app:autoplay:change', {
+ detail: { enabled: false, autoplay: false, source: 'manual-story-scroll' }
+ }));
+ }
+
+ measureStoryLineHeight() {
+ const element = this.paragraphContainer || this.container || this.pageRight;
+ const computed = element ? window.getComputedStyle(element) : null;
+ const parsed = parseFloat(computed?.lineHeight || '');
+ const fontSize = parseFloat(computed?.fontSize || '');
+ this.lineHeightPx = Number.isFinite(parsed)
+ ? parsed
+ : (Number.isFinite(fontSize) ? fontSize * 1.45 : 24);
+ this.viewportLineCount = Math.max(1, Math.floor((this.pageRight?.clientHeight || this.lineHeightPx) / this.lineHeightPx));
+ return this.lineHeightPx;
+ }
+
+ measureBlockLines(element, fallbackLineCount = 1) {
+ if (!element) return { lineCount: Math.max(1, fallbackLineCount), heightPx: 0, lineHeightPx: this.measureStoryLineHeight() };
+ const lineHeight = this.measureStoryLineHeight();
+ const style = window.getComputedStyle(element);
+ const marginTop = parseFloat(style.marginTop || '0') || 0;
+ const marginBottom = parseFloat(style.marginBottom || '0') || 0;
+ const heightPx = Math.max(0, element.offsetHeight + marginTop + marginBottom);
+ const lineCount = Math.max(1, Math.ceil((heightPx || lineHeight * fallbackLineCount) / Math.max(1, lineHeight)));
+ return { lineCount, heightPx, lineHeightPx: lineHeight };
+ }
+
+ async recordRenderedMetrics(blockId, element, fallbackLineCount = 1) {
+ if (!this.storyHistory || typeof this.storyHistory.updateBlockMetrics !== 'function' || blockId == null) return null;
+ await new Promise(resolve => window.requestAnimationFrame(resolve));
+ const metrics = this.measureBlockLines(element, fallbackLineCount);
+ const updated = await this.storyHistory.updateBlockMetrics(blockId, metrics);
+ if (updated && element) {
+ element.dataset.lineStart = String(updated.lineStart || 0);
+ element.dataset.lineCount = String(updated.lineCount || metrics.lineCount);
+ }
+ this.setVirtualPadding();
+ this.updateStoryScrollbar();
+ return updated;
+ }
+
+ setVirtualPadding() {
+ if (!this.paragraphContainer) return;
+ const first = this.renderedItems.find(item => item.blockId != null);
+ const last = [...this.renderedItems].reverse().find(item => item.blockId != null);
+ const topLines = Math.max(0, Number(first?.lineStart || first?.metadata?.lineStart || 0));
+ const lastStart = Number(last?.lineStart ?? last?.metadata?.lineStart ?? 0);
+ const lastCount = Number(last?.lineCount ?? last?.metadata?.lineCount ?? 0);
+ const totalLines = Math.max(0, Number(this.storyHistory?.renderedLineCount || 0));
+ const bottomLines = Math.max(0, totalLines - (Number.isFinite(lastStart) ? lastStart + Math.max(1, lastCount || 1) : totalLines));
+ const lineHeight = this.measureStoryLineHeight();
+ this.paragraphContainer.style.paddingTop = `${topLines * lineHeight}px`;
+ this.paragraphContainer.style.paddingBottom = `${bottomLines * lineHeight}px`;
+ }
+
+ setStoryOffset(offsetPx) {
+ this.storyOffsetPx = Number(offsetPx) || 0;
+ if (this.container) {
+ this.container.style.transform = `translateY(${this.storyOffsetPx}px)`;
+ }
+ this.handleStoryScroll();
+ this.updateStoryScrollbar();
+ }
+
+ getMaxStoryTopLine() {
+ this.measureStoryLineHeight();
+ const totalLines = Math.max(0, Number(this.storyHistory?.renderedLineCount || 0));
+ return Math.max(0, totalLines - Math.max(1, this.viewportLineCount || 1));
+ }
+
+ scrollStoryByLines(deltaLines, smooth = true) {
+ const nextLine = (this.storyTopLine || 0) + Number(deltaLines || 0);
+ return this.setStoryTopLine(nextLine, smooth, { mode: 'manual' });
+ }
+
+ setStoryTopLine(targetLine, smooth = true, options = {}) {
+ this.measureStoryLineHeight();
+ const maxTopLine = this.getMaxStoryTopLine();
+ const target = Math.round(Math.max(0, Math.min(maxTopLine, Number(targetLine || 0))));
+ const start = this.storyTopLine || 0;
+ const delta = target - start;
+ const animationId = ++this.storyScrollAnimationId;
+
+ if (!smooth || Math.abs(delta) < 0.02) {
+ this.storyTopLine = target;
+ this.setStoryOffset(-(target * this.lineHeightPx));
+ if (options.ensure !== false) {
+ this.ensureHistoryWindowForLine(target + (this.viewportLineCount / 2));
+ }
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ const startedAt = performance.now();
+ const duration = Math.min(1100, Math.max(360, Math.abs(delta) * 36));
+ const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+ const shouldEnsureDuringAnimation = options.ensure !== false;
+ const step = (now) => {
+ if (animationId !== this.storyScrollAnimationId) {
+ resolve();
+ return;
+ }
+ const progress = Math.min(1, (now - startedAt) / duration);
+ this.storyTopLine = start + (delta * ease(progress));
+ this.setStoryOffset(-(this.storyTopLine * this.lineHeightPx));
+ if (shouldEnsureDuringAnimation) {
+ this.ensureHistoryWindowForLine(this.storyTopLine + (this.viewportLineCount / 2));
+ }
+ if (progress < 1) {
+ requestAnimationFrame(step);
+ } else {
+ this.storyTopLine = target;
+ this.setStoryOffset(-(target * this.lineHeightPx));
+ if (shouldEnsureDuringAnimation) {
+ this.ensureHistoryWindowForLine(target + (this.viewportLineCount / 2));
+ }
+ resolve();
+ }
+ };
+ requestAnimationFrame(step);
+ });
+ }
+
+ async ensureLiveTailWindow() {
+ if (!this.storyHistory || !this.paragraphContainer || this.loadingHistoryPage) return;
+ const latestRendered = Math.max(0, Number(this.storyHistory.latestRenderedBlockId || 0));
+ if (latestRendered <= 0) return;
+ if ((this.historyWindowEndId || 0) >= latestRendered) return;
+
+ const count = Math.max(1, this.visibleBlockLimit - 1);
+ const start = Math.max(1, latestRendered - count + 1);
+ const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, start, latestRendered);
+ if (!blocks.length) return;
+
+ await this.renderHistoryWindow(blocks, 'bottom');
+ this.activeCenterBlockId = latestRendered;
+ this.lastEnsuredCenterBucket = null;
+ }
+
+ async ensureHistoryWindowForLine(centerLine) {
+ if (!this.storyHistory || this.loadingHistoryPage) return;
+ const bucket = Math.floor(Math.max(0, Number(centerLine || 0)) / Math.max(1, this.viewportLineCount || 1));
+ if (this.historyEnsurePending || bucket === this.lastEnsuredCenterBucket) return;
+ this.historyEnsurePending = true;
+ this.lastEnsuredCenterBucket = bucket;
+ try {
+ const block = await this.storyHistory.findBlockForLine(
+ this.storyHistory.currentGameId,
+ centerLine,
+ this.storyHistory.latestRenderedBlockId
+ );
+ const centerBlockId = block?.blockId;
+ if (!centerBlockId || centerBlockId === this.activeCenterBlockId) return;
+ this.activeCenterBlockId = centerBlockId;
+ const hasEnoughBefore = centerBlockId - (this.historyWindowStartId || 1) >= this.historyBufferBlocks;
+ const hasEnoughAfter = (this.historyWindowEndId || 0) - centerBlockId >= this.historyBufferBlocks;
+ if (!hasEnoughBefore || !hasEnoughAfter) {
+ await this.loadHistoryWindowAround(centerBlockId, 'preserve');
+ }
+ } finally {
+ this.historyEnsurePending = false;
+ }
+ }
+
+ async loadHistoryWindowAround(centerBlockId, scrollTarget = 'preserve') {
+ if (!this.storyHistory || this.loadingHistoryPage) return;
+ const latest = Math.max(0, Number(this.storyHistory.latestRenderedBlockId || 0));
+ const center = Math.max(1, Math.min(latest, Number(centerBlockId || 1)));
+ const start = Math.max(1, center - this.historyBufferBlocks);
+ const end = Math.min(latest, center + this.historyBufferBlocks);
+ if (start === this.historyWindowStartId && end === this.historyWindowEndId) return;
+ this.loadingHistoryPage = true;
+ try {
+ const blocks = await this.storyHistory.getBlocksRange(this.storyHistory.currentGameId, start, end);
+ if (blocks.length) {
+ await this.renderHistoryWindow(blocks, scrollTarget);
}
- this.pageRight.scrollTop = 0;
- this.updateStoryScrollbar({ latestBlockId: this.storyHistory.nextBlockId - 1 });
} finally {
this.loadingHistoryPage = false;
}
}
- async recordRenderedItem(item) {
- this.renderedItems.push(item);
- if (this.storyHistory && typeof this.storyHistory.recordBlock === 'function') {
- try {
- const record = await this.storyHistory.recordBlock(item);
- if (record && item.id) {
- item.blockId = record.blockId;
- item.gameId = record.gameId;
- const element = document.getElementById(item.id);
- if (element) element.dataset.storyBlockId = String(record.blockId);
- }
- document.dispatchEvent(new CustomEvent('story:history-updated', {
- detail: {
- gameId: record?.gameId || null,
- latestBlockId: record?.blockId || null
- }
- }));
- } catch (error) {
- console.warn('UIDisplayHandler: Failed to store story history item:', error);
- }
- }
- }
-
- updateStoryScrollbar(detail = {}) {
- const thumb = document.getElementById('story_scrollbar_thumb');
- if (!thumb) return;
- const latest = Math.max(1, Number(detail.latestBlockId || this.storyHistory?.nextBlockId || 1));
- const visible = Math.min(this.visibleBlockLimit, latest);
- const heightPercent = Math.max(8, Math.min(100, (visible / latest) * 100));
- const topPercent = latest <= visible ? 0 : 100 - heightPercent;
- thumb.style.height = `${heightPercent}%`;
- thumb.style.top = `${topPercent}%`;
- }
-
async trimVisibleBlocks() {
if (!this.paragraphContainer) return;
- const blocks = Array.from(this.paragraphContainer.querySelectorAll('.story-turn-block'));
- const excess = blocks.length - this.visibleBlockLimit;
- if (excess <= 0) return;
-
- blocks.slice(0, excess).forEach(block => {
- block.classList.add('story-block-archiving');
- window.setTimeout(() => block.remove(), 360);
- });
- this.renderedItems = this.renderedItems.slice(Math.max(0, this.renderedItems.length - this.visibleBlockLimit));
+ await this.trimVirtualWindow(1);
+ this.updateStoryScrollbar();
}
- animatePageScroll(targetTop, duration = 720) {
- if (!this.pageRight) return;
- if (!duration) {
- this.pageRight.scrollTop = targetTop;
- return;
+ animateStoryOffset(targetOffset, duration = 720, options = {}) {
+ const target = Number(targetOffset || 0);
+ const start = Number(this.storyOffsetPx || 0);
+ const delta = target - start;
+ const animationId = ++this.storyScrollAnimationId;
+
+ if (!duration || Math.abs(delta) < 1) {
+ this.setStoryOffset(target);
+ this.updateStoryScrollbar({ mode: options.mode });
+ return Promise.resolve();
}
- const startTop = this.pageRight.scrollTop;
- const delta = targetTop - startTop;
- if (Math.abs(delta) < 1) return;
-
- const startedAt = performance.now();
- const ease = (t) => 1 - Math.pow(1 - t, 3);
- const step = (now) => {
- const progress = Math.min(1, (now - startedAt) / duration);
- this.pageRight.scrollTop = startTop + (delta * ease(progress));
- if (progress < 1) {
- requestAnimationFrame(step);
- }
- };
- requestAnimationFrame(step);
+ return new Promise(resolve => {
+ const startedAt = performance.now();
+ const ease = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+ const step = (now) => {
+ if (animationId !== this.storyScrollAnimationId) {
+ resolve();
+ return;
+ }
+ const progress = Math.min(1, (now - startedAt) / duration);
+ this.setStoryOffset(start + (delta * ease(progress)));
+ if (progress < 1) {
+ requestAnimationFrame(step);
+ return;
+ }
+ this.setStoryOffset(target);
+ resolve();
+ };
+ requestAnimationFrame(step);
+ });
}
scrollToTurn(turnId) {
if (!this.pageRight || turnId == null) return;
const escapedTurnId = CSS.escape(String(turnId));
- const target = this.paragraphContainer?.querySelector(`[data-turn-id="${escapedTurnId}"]`);
- if (!target) return;
- this.pageRight.scrollTo({
- top: Math.max(0, target.offsetTop - 12),
- behavior: 'smooth'
+ const scrollToLiveTarget = () => {
+ const target = this.paragraphContainer?.querySelector(`[data-turn-id="${escapedTurnId}"]`);
+ if (!target) return false;
+ const targetLine = Number(target.dataset.lineStart);
+ if (Number.isFinite(targetLine)) {
+ this.setStoryTopLine(targetLine, true, { mode: 'jump-to-turn' });
+ } else {
+ this.setStoryTopLine((target.offsetTop || 0) / Math.max(1, this.measureStoryLineHeight()), true, { mode: 'jump-to-turn' });
+ }
+ return true;
+ };
+
+ if (scrollToLiveTarget()) return;
+ this.renderHistoryWindowForTurn(turnId).then(() => {
+ requestAnimationFrame(() => scrollToLiveTarget());
});
}
@@ -785,12 +1275,16 @@ class UIDisplayHandlerModule extends BaseModule {
const blocks = Array.from(this.paragraphContainer.querySelectorAll('[data-turn-id]'));
if (blocks.length === 0) return;
- const viewportMiddle = this.pageRight.scrollTop + (this.pageRight.clientHeight / 2);
+ const viewportMiddle = (this.storyTopLine * this.measureStoryLineHeight()) + (this.pageRight.clientHeight / 2);
let best = null;
let bestDistance = Infinity;
blocks.forEach((block) => {
- const blockMiddle = block.offsetTop + (block.offsetHeight / 2);
+ const lineStart = Number(block.dataset.lineStart);
+ const lineCount = Number(block.dataset.lineCount);
+ const blockMiddle = Number.isFinite(lineStart) && Number.isFinite(lineCount)
+ ? ((lineStart + (lineCount / 2)) * this.lineHeightPx)
+ : block.offsetTop + (block.offsetHeight / 2);
const distance = Math.abs(blockMiddle - viewportMiddle);
if (distance < bestDistance) {
bestDistance = distance;
@@ -816,17 +1310,37 @@ class UIDisplayHandlerModule extends BaseModule {
}));
if (sentence.kind === 'image') {
- const element = this.renderImageBlock({ ...(sentence.metadata || {}), id: sentence.id }, true);
- await this.recordRenderedItem({
+ await this.ensureLiveTailWindow();
+ const element = this.renderImageBlock({ ...(sentence.metadata || {}), id: sentence.id, revealImmediately: false }, true);
+ const renderedItem = {
type: 'image',
id: sentence.id,
turnId: sentence.turnId ?? null,
+ blockId: sentence.blockId ?? null,
+ gameId: sentence.gameId ?? null,
text: '',
metadata: { ...(sentence.metadata || {}), id: sentence.id }
- });
+ };
+ if (element && sentence.blockId != null) {
+ element.dataset.storyBlockId = String(sentence.blockId);
+ this.markBlockRendered(sentence.blockId);
+ const updated = await this.recordRenderedMetrics(sentence.blockId, element, sentence.metadata?.imageLayout?.lineCount || 1);
+ if (updated) {
+ renderedItem.lineStart = updated.lineStart;
+ renderedItem.lineCount = updated.lineCount;
+ renderedItem.metadata.lineStart = updated.lineStart;
+ renderedItem.metadata.lineCount = updated.lineCount;
+ }
+ }
+ this.renderedItems.push(renderedItem);
+ this.historyWindowStartId = this.renderedItems.find(item => item.blockId != null)?.blockId || this.historyWindowStartId;
+ this.historyWindowEndId = [...this.renderedItems].reverse().find(item => item.blockId != null)?.blockId || this.historyWindowEndId;
+ this.setVirtualPadding();
+ this.updateStoryScrollbar();
await this.trimVisibleBlocks();
- this.scrollStoryToEnd(true);
+ await this.scrollStoryToEnd(true);
+ this.revealImageBlock(element);
if (sentence.onComplete) {
sentence.onComplete();
@@ -837,6 +1351,9 @@ class UIDisplayHandlerModule extends BaseModule {
if (sentence.kind === 'music') {
console.log('UIDisplayHandler: Music block started', sentence.metadata || {});
+ if (sentence.blockId != null) {
+ this.markBlockRendered(sentence.blockId);
+ }
}
if (sentence.onComplete) {
@@ -893,7 +1410,7 @@ class UIDisplayHandlerModule extends BaseModule {
});
}
- renderImageBlock(metadata = {}, animate = true) {
+ renderImageBlock(metadata = {}, animate = true, placement = 'append') {
if (!this.paragraphContainer) return null;
const metrics = metadata.imageLayout || this.calculateImageMetrics(metadata.size);
@@ -912,34 +1429,58 @@ class UIDisplayHandlerModule extends BaseModule {
figure.style.height = `${metrics.height}px`;
figure.style.marginTop = `${metrics.verticalMargin || 0}px`;
figure.style.marginBottom = `${metrics.verticalMargin || 0}px`;
- figure.dataset.animationMs = '900';
+ if ((metrics.size || metadata.size) === 'portrait') {
+ const gap = metrics.gap || 0;
+ figure.style.shapeMargin = `${gap}px`;
+ if (metrics.floatSide === 'right') {
+ figure.style.marginLeft = `${gap}px`;
+ } else {
+ figure.style.marginRight = `${gap}px`;
+ }
+ }
+ figure.dataset.animationMs = '2000';
if (metadata.turnId != null) {
figure.dataset.turnId = String(metadata.turnId);
figure.classList.add('story-turn-block');
}
const img = document.createElement('img');
- img.src = metadata.url || metadata.filename || '';
+ img.src = this.resolveImageUrl(metadata);
img.alt = metadata.alt || '';
img.decoding = 'async';
img.loading = 'eager';
figure.appendChild(img);
- this.paragraphContainer.appendChild(figure);
+ this.insertStoredElement(figure, placement);
- if (animate) {
- window.requestAnimationFrame(() => {
+ if (animate && metadata.revealImmediately !== false) {
+ window.requestAnimationFrame(() => this.revealImageBlock(figure));
+ } else {
+ if (!animate) {
figure.classList.remove('story-image-pending');
figure.classList.add('story-image-visible');
- });
- } else {
- figure.classList.remove('story-image-pending');
- figure.classList.add('story-image-visible');
+ }
}
return figure;
}
+ revealImageBlock(figure) {
+ if (!figure) return;
+ figure.classList.remove('story-image-pending');
+ figure.classList.add('story-image-visible');
+ }
+
+ resolveImageUrl(metadata = {}) {
+ const explicit = String(metadata.url || '').trim();
+ if (explicit) return explicit;
+
+ const filename = String(metadata.filename || '').trim();
+ if (!filename) return '';
+ if (/^(https?:|data:|blob:|\/)/i.test(filename)) return filename;
+ return `/images/${filename.replace(/^images[\\/]/i, '').replace(/\\/g, '/')}`;
+ }
+
calculateImageMetrics(size = 'landscape') {
const storyElement = document.getElementById('story');
const pageWidth = storyElement?.clientWidth || 600;
@@ -956,14 +1497,20 @@ class UIDisplayHandlerModule extends BaseModule {
? 'landscape'
: String(size || 'landscape').toLowerCase();
const aspect = normalizedSize === 'portrait' ? (9 / 16) : normalizedSize === 'square' ? 1 : (16 / 9);
- const imageGap = lineHeight * 0.9;
- const maxWidth = normalizedSize === 'portrait' ? pageWidth * 0.5 : pageWidth;
- const naturalHeight = maxWidth / aspect;
+ const isPortrait = normalizedSize === 'portrait';
+ const imageGap = lineHeight;
+ const maxOuterWidth = isPortrait ? pageWidth * 0.5 : pageWidth;
+ const maxImageWidth = isPortrait
+ ? Math.max(lineHeight * 4, maxOuterWidth - imageGap)
+ : maxOuterWidth;
+ const naturalHeight = maxImageWidth / aspect;
const imageLineCount = Math.max(1, Math.floor(naturalHeight / lineHeight));
- const height = imageLineCount * lineHeight;
- const width = Math.min(maxWidth, height * aspect);
- const verticalMargin = lineHeight / 2;
- const lineCount = imageLineCount + 1;
+ const verticalMargin = isPortrait ? lineHeight / 2 : 0;
+ const lineCount = isPortrait ? imageLineCount + 1 : imageLineCount;
+ const height = isPortrait
+ ? Math.max(lineHeight, (lineCount * lineHeight) - (verticalMargin * 2))
+ : imageLineCount * lineHeight;
+ const width = Math.min(maxImageWidth, height * aspect);
return {
size: normalizedSize,
@@ -995,9 +1542,13 @@ class UIDisplayHandlerModule extends BaseModule {
this.container.appendChild(this.paragraphContainer);
}
this.renderedItems = [];
- if (this.pageRight) {
- this.pageRight.scrollTop = 0;
- }
+ this.historyWindowStartId = 1;
+ this.historyWindowEndId = 0;
+ this.storyTopLine = 0;
+ this.activeCenterBlockId = null;
+ this.setVirtualPadding();
+ this.setStoryOffset(0);
+ this.updateStoryScrollbar({ latestBlockId: this.getLatestHistoryBlockId() });
}
/**
@@ -1056,16 +1607,10 @@ class UIDisplayHandlerModule extends BaseModule {
choicesGroup.appendChild(choiceButton);
});
- // Schedule transition for choices to appear
- setTimeout(() => {
+ window.requestAnimationFrame(() => {
choicesGroup.classList.add('visible');
-
- // Scroll to the choices
- choicesGroup.scrollIntoView({ behavior: 'smooth', block: 'end' });
-
- // Resolve the promise with the choices container
resolve(choicesGroup);
- }, 100);
+ });
});
}
}
diff --git a/public/js/ui-effects-module.js b/public/js/ui-effects-module.js
index d77ac8f..2a74327 100644
--- a/public/js/ui-effects-module.js
+++ b/public/js/ui-effects-module.js
@@ -40,7 +40,6 @@ class UIEffectsModule extends BaseModule {
'applyEffect',
'applyShakeEffect',
'applyFlashEffect',
- 'applyTextEmphasis',
'processCommand',
'handleLightingAnimationEnd'
]);
@@ -209,8 +208,6 @@ class UIEffectsModule extends BaseModule {
return this.applyShakeEffect(options);
case 'flash':
return this.applyFlashEffect(options);
- case 'emphasis':
- return this.applyTextEmphasis(options.text, options);
default:
console.warn(`Unknown effect: ${effectName}`);
return null;
@@ -288,20 +285,6 @@ class UIEffectsModule extends BaseModule {
return effectId;
}
- applyTextEmphasis(text, options = {}) {
- // Use existing display handler to show emphasized text
- const displayHandler = this.getModule('ui-display-handler');
- if (!displayHandler) return null;
-
- const style = {
- fontWeight: 'bold',
- color: options.color || '#990000',
- fontSize: options.size || '1.2em'
- };
-
- return displayHandler.displayText(text, { style, speak: true });
- }
-
processCommand(command) {
switch (command.action) {
case 'apply':
diff --git a/src/server.ts b/src/server.ts
index bcead76..f02a000 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -186,19 +186,8 @@ io.on('connection', (socket) => {
}
});
- // Start a new game
- socket.on('startGame', async () => {
- try {
- await handleGameApi(socket, 'newGame', []);
-
- } catch (error) {
- console.error('Error starting game:', error);
- socket.emit('error', { message: 'Failed to start game. Please try again.' });
- }
- });
-
- // Process player command
- socket.on('playerCommand', async (data) => {
+ // Process player command
+ socket.on('playerCommand', async (data) => {
try {
const gameRunner = gameSessions.get(socket.id);
@@ -221,51 +210,7 @@ io.on('connection', (socket) => {
}
});
- // Save game state
- socket.on('saveGame', () => {
- try {
- const gameRunner = gameSessions.get(socket.id);
-
- if (!gameRunner) {
- socket.emit('error', { message: 'Game session not found. Please start a new game.' });
- return;
- }
-
- socket.data.saveGames.set(1, gameRunner.getGameState());
-
- socket.emit('gameSaved');
-
- } catch (error) {
- console.error('Error saving game:', error);
- socket.emit('error', { message: 'Failed to save game. Please try again.' });
- }
- });
-
- // Load game state
- socket.on('loadGame', async () => {
- try {
- const gameRunner = gameSessions.get(socket.id);
-
- if (!gameRunner) {
- socket.emit('error', { message: 'Game session not found. Please start a new game.' });
- return;
- }
-
- // Check if there's a saved game
- if (!socket.data.saveGames?.has(1)) {
- socket.emit('error', { message: 'No saved game found.' });
- return;
- }
-
- await handleGameApi(socket, 'loadGame', [1]);
-
- } catch (error) {
- console.error('Error loading game:', error);
- socket.emit('error', { message: 'Failed to load game. Please try again.' });
- }
- });
-
- // Handle disconnection
+ // Handle disconnection
socket.on('disconnect', () => {
console.log(`Client disconnected: ${socket.id}`);
diff --git a/src/test-server.ts b/src/test-server.ts
index b92b938..38a01e0 100644
--- a/src/test-server.ts
+++ b/src/test-server.ts
@@ -136,18 +136,6 @@ io.on('connection', (socket) => {
}
});
- // Start a new game
- socket.on('startGame', async () => {
- try {
- console.log('Starting test game session');
- startDemoGame();
-
- } catch (error) {
- console.error('Error starting game:', error);
- socket.emit('error', { message: 'Failed to start game. Please try again.' });
- }
- });
-
// Process player command
socket.on('playerCommand', async (data) => {
try {