adventure: T6 Phylactery Verses + stackable-rebirth engine primitive (P8)

The second Layer-2 postgame boss mechanic, and the first stateful in-combat one.
Valdris ("boss_valdris_ascendant", the Ossuary Ascendant) is bound to three
Verses hidden on the zone's secret nodes: every Verse the player finds and walks
before the fight unbinds one rebirth, every Verse they skip leaves it armed. A
full-clear explorer strips all three and fights a mortal lich; a speedrunner who
blows past the secrets fights a god who will not stay down.

New engine primitive: stackable rebirth. The stock survive_at_1 is a one-shot
1-HP stay; Valdris needs several rebirths that each restore a real pool.
combatState/CombatStatuses gain EnemyReviveCharges/EnemyReviveHP (round-tripped
through the turn engine so a suspend/resume keeps the live count), and enemyDown
consumes a charge after the survive_at_1 check, reviving to 25% of the
party-scaled max and emitting a phylactery_rebirth event. Zero for every
non-Valdris fight.

Unlike Greed Tax (a pure per-round recompute in applyBossRunModifiers), rebirths
are spent mid-fight, so the charge count is seeded ONCE at session creation
(seedBossRunStatuses, from unvisited secret Verses) and never re-derived on the
per-round enemy rebuild. Seeded from handleFightCmd after startPartyCombatSession.

Sim A/B (millenia, n=120 L20 party+Pete+pets, same binary, control neuters the
dispatch): mortal end (verses found) fighter 42.5% -- reproduces the deployed P7
ossuary baseline, confirming the mechanic doesn't touch the validated full-clear
path -- and the god end (0 verses) fighter 12.5%, a deadly-but-beatable flex arm.
The sim walks 0 verses (autopilot takes the first unlocked fork; Verses are
behind Perception locks), so the A/B brackets the whole player-agency gradient.

Claude-Session: https://claude.ai/code/session_0156WqjgsbmSY2U8eQ3Kkb1s
This commit is contained in:
prosolis
2026-07-16 08:04:19 -07:00
parent fc9e055083
commit 686434f8e3
7 changed files with 239 additions and 0 deletions

View File

@@ -331,6 +331,8 @@ func renderEvent(e CombatEvent, playerName, enemyName string, result CombatResul
return pickRand(narrativeSurviveArmed)
case "survive_at_1":
return pickRand(narrativeSurvive)
case "phylactery_rebirth":
return pickRand(narrativePhylacteryRebirth)
case "stat_drain":
return fmt.Sprintf(pickRand(narrativeStatDrain), e.Damage)
case "debuff":
@@ -761,6 +763,15 @@ var narrativeSurvive = []string{
"🕯️ The enemy by all rights should be down. It is, instead, very barely up.",
}
// narrativePhylacteryRebirth fires when Valdris burns a Verse the player left
// un-found: a bound rebirth spends and the lich reassembles. Each line reads as
// "you skipped one of these" so the mechanic teaches itself over a wipe.
var narrativePhylacteryRebirth = []string{
"💀 The lich comes apart — and a Verse you never found sings him back together. He rises, unhurried.",
"💀 Bone-dust swirls up off the floor and re-seats itself. A rebirth you didn't unbind just spent itself. He stands.",
"💀 That should have been the end of him. A Verse still hums somewhere in the cathedral, and Valdris simply *begins again*.",
}
var narrativeStatDrain = []string{
"🩸 The enemy saps your strength — your swings feel heavier, weaker. (-%d hit damage)",
"🩸 Something drains out of your limbs. Your hits won't bite as deep now. (-%d damage)",