mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Review follow-up N: Misty's round-end procs fire in turn-based combat
DerivePlayerStats builds MistyHealProc / CrowdRevengeProc onto every turn-based combatant, but stepRoundEnd had no counterpart to endOfRoundForSeat, so neither was ever read. The buff (Misty's heal) was silently lost. The debuff (her crowd's revenge) was an exploit: a player who declined Misty escaped it entirely by fighting with !attack instead of letting the room auto-resolve -- no discovery required, just press the button. Hoisted both procs out of endOfRoundForSeat into shared helpers (mistyCrowdRevenge, mistyHeal) and a seatEndOfRound hook that runs the pair in the auto-resolve order. endOfRoundForSeat now calls the helpers; stepRoundEnd calls seatEndOfRound per seat, after the poison tick so a heal can answer the round's damage. A one-sided debuff-only hook would have been a second parallel sibling of the kind deferred item D warns about, so the heal ships with it -- a player-favourable discovery mechanic, consistent with the lift-trailers stance. Both helpers short-circuit before st.randFloat() when their proc is unarmed, so a character with no Misty history draws no dice: the sim corpus and combat_characterization.golden do not move. seatCombatResult now reads MistyHealed back off the misty_heal event (as combat_pet_save always has), so combat_misty_clutch is reachable turn-based. combat_sniper_kill stays unreachable -- Arina's proc is a pre-combat one-shot with no round-end seam. renderAllySeatEvent renders crowd_revenge as unattributed damage: an ally sees the hit but not Misty's name, keeping the grudge the owner's own discovery. Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
@@ -208,21 +208,38 @@ func seatFightStartMods(sess *CombatSession, seat int, c *DnDCharacter) CombatMo
|
||||
// engine never builds a CombatResult — it persists a session and a shared event
|
||||
// log — so the close-out has to assemble one.
|
||||
//
|
||||
// SniperKilled and MistyHealed stay false because the turn engine has no Arina
|
||||
// or Misty proc to set them: those two live only in the auto-resolve engine.
|
||||
// MistyHealed is read back off the seat's event log rather than off a flag: the
|
||||
// turn engine's CombatResult is a scratch value it discards between steps, and
|
||||
// the log is the only thing that survives to the close-out. This is how
|
||||
// combat_pet_save has always been detected.
|
||||
//
|
||||
// SniperKilled stays false. Arina's proc is a pre-combat one-shot, not a
|
||||
// round-end effect, so seatEndOfRound does not carry it and the turn engine
|
||||
// still has no place to fire it — combat_sniper_kill remains unreachable from a
|
||||
// manual kill.
|
||||
//
|
||||
// NearDeath mirrors the auto-resolve engine's win threshold (below 15% of max);
|
||||
// its loss-side meaning is unused here, since the only hook that reads it gates
|
||||
// on PlayerWon.
|
||||
func seatCombatResult(sess *CombatSession, seat int) CombatResult {
|
||||
hp, hpMax := sess.seatHP(seat), sess.seatHPMax(seat)
|
||||
won := sess.Status == CombatStatusWon
|
||||
events := eventsForSeat(sess.TurnLog, seat)
|
||||
misty := false
|
||||
for _, ev := range events {
|
||||
if ev.Action == "misty_heal" {
|
||||
misty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return CombatResult{
|
||||
PlayerWon: won,
|
||||
Events: eventsForSeat(sess.TurnLog, seat),
|
||||
Events: events,
|
||||
PlayerEndHP: hp,
|
||||
EnemyEndHP: sess.EnemyHP,
|
||||
TotalRounds: sess.Round,
|
||||
NearDeath: won && hp > 0 && float64(hp) < float64(max(1, hpMax))*0.15,
|
||||
MistyHealed: misty,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user