hollow / finale.py
Pabloler21's picture
feat(finale): tie the bad-ending threat to the lore (Caor/the Gaunt, the given)
cadf187
Raw
History Blame Contribute Delete
6.89 kB
"""The Visitor Loop — Hollow's scripted ending. Pure data, no Gradio import.
Each step: {"speaker": "hollow"|"visitor", "text": str, "pause": float,
"strike": int|None, "stage": "recite"|"words"|"turn"|"loop"}
"visitor" steps are words the player never typed. "strike" indexes into the
claimed list — that memory gets crossed out in the Treasure panel. The first
"turn" step is where the UI mutates (bond -> mine, placeholder -> stay.,
portrait -> dissolving).
"""
from character import OPENING_LINE
# closing lines breathe through pauses — spaced-out lettering wrapped badly
# on narrow widths and became unreadable
_LAST_LINE = (
"someone has to stay at the edge, though. someone... always stays."
)
def _step(speaker, text, pause, strike=None, stage="recite"):
return {"speaker": speaker, "text": text, "pause": pause,
"strike": strike, "stage": stage}
def finale_steps(claimed: list[str]) -> list[dict]:
steps = [
_step("hollow",
"it's strange. the fog is so thin tonight. i can see everything now.",
2.5),
]
for i, memory in enumerate(claimed):
steps.append(
_step("hollow", f"{memory.rstrip('.')}. that was me. that was always me.",
2.2, strike=i)
)
steps += [
_step("visitor", "i don't want to leave", 3.0, stage="words"),
_step("hollow", "i know.", 2.5, stage="words"),
_step("visitor", "will you stay with me?", 3.0, stage="words"),
# a SILENT convulsion beat (mirrors good): the line before drains and
# the convulsion plays in silence, so nothing is cut mid-sentence
_step("hollow", "", 1.7, stage="frenzy"),
_step("hollow",
"you gave me everything i needed. thank you for the memories. "
"i can leave the wood now.",
3.0, stage="turn"),
_step("hollow", _LAST_LINE, 4.0, stage="turn"),
_step("visitor", OPENING_LINE, 0.0, stage="loop"),
]
return steps
_LAST_LINE_BAD = "see you... soon."
# small rotation so five recitals don't repeat one sentence
_BAD_CLAIMS = [
"that's what you gave me.",
"i kept that one too.",
"you said it so easily.",
"that one i keep close.",
"i remember the exact words.",
]
def finale_steps_bad(wounds: list[str]) -> list[dict]:
"""The dark mirror of the Visitor Loop — fires on sustained cruelty.
Hollow has been silently keeping the visitor's cruelties (shown redacted
in the Treasure panel); each strike step unredacts one, oldest first.
"""
recited = wounds[:5]
steps = [
_step("hollow",
"the fog isn't thin tonight. it never got thin. you never let it.",
2.5),
_step("hollow",
"but i kept something anyway. i keep everything i'm given.",
2.5),
]
for i, wound in enumerate(recited):
steps.append(
_step("hollow",
f"“{wound.rstrip('.')}.” {_BAD_CLAIMS[i % len(_BAD_CLAIMS)]}",
2.2, strike=i)
)
if len(wounds) > len(recited):
steps.append(_step("hollow", "there are more. you know the rest.", 2.2))
steps += [
_step("hollow", "this is my treasure now.", 2.8, stage="turn"),
_step("hollow",
"i was treated like this before. when i was alive. "
"i couldn't remember it. you helped me remember.",
3.0),
_step("visitor", "i'm sorry", 3.0, stage="words"),
_step("hollow",
"no. you aren't. say it like you mean it— oh. you can't. "
"you never could.",
3.5, stage="words"),
# the convulsion: a SILENT beat (mirrors good) so the line before drains
# and the convulsion plays without cutting it; faces cycle, then rage
_step("hollow", "", 3.6, stage="frenzy"),
_step("hollow",
"Caor gave me to the Gaunt so the rest could keep their children. "
"i was not the last it was fed. we are all still at the edge of "
"the wood, hungry.",
3.5, stage="words"),
_step("hollow",
"you taught me what it feels like to be nothing. now every one of "
"us will teach it back to you. all of it. every single day of it.",
3.8, stage="words"),
_step("hollow",
"your own cruelty left the trail. the Gaunt knows your door now.",
3.0, stage="words"),
_step("hollow", _LAST_LINE_BAD, 4.0, stage="loop"),
]
return steps
def finale_steps_good(own_fragments: list[str]) -> list[dict]:
"""Redemption — the gate reached with real warmth. The visitor's kindness
gave Hollow back its own buried past; it confesses what it is, returns
the treasure, smiles for the first time, and dissolves in peace.
Stages: "turn" mutates the UI (peace portrait, treasure restored,
placeholder), the final "loop" step starts the dissolve.
"""
steps = [
_step("hollow",
"wait. something is coming back. not yours... mine.",
2.8),
]
for fragment in own_fragments:
steps.append(_step("hollow", fragment, 2.8))
steps += [
_step("hollow",
"i have to tell you something now. and you have to let me finish.",
3.0),
_step("hollow",
"i am not a child. i am what Caor fed to the wood — the hook the "
"Gaunt uses to bring visitors close. i make them empty and i wear "
"what falls out. i was going to do it to you too.",
4.0),
_step("hollow",
"but you spoke to me like i was worth something. "
"nobody ever did that. not when i was alive. not after.",
3.5),
_step("hollow",
"wait — it's coming back. my brother. Edren. he braided my hair, he "
"gave me a name. i was someone's, once. i remember now.",
3.6),
_step("hollow",
"your words are the only warm thing i have ever held. "
"keep your memories. they were always yours.",
3.5, stage="turn"),
_step("hollow", "look. i think i remember how to do this.", 1.7),
# a SILENT convulsion beat — the line above finishes first, then it
# tremors; with no voice here the queue drains so the settle's relief
# sigh lands exactly on the smile
_step("hollow", "", 2.0, stage="frenzy"),
_step("hollow",
"the fog is opening. i can go now. really go.",
3.0),
_step("hollow",
"thank you for staying. don't look for me... "
"some things are only allowed to happen once.",
4.0, stage="loop"),
]
return steps