mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Phase 2b (lever): wounded-entrant surprise-nick clamp
The post-2a tier-lethality trace showed the remaining 0% completion was driven by chained surprise-round nicks on already-wounded fighters, not by the elites themselves. Pattern from the trace: fight day=5 Hobgoblin Warchief: hp_pre=24 → hp_post=14 WON fight day=5 Goblin Archer: nick=6, hp_pre=8 → hp_post=0 LOST The Warchief left the fighter at HP 14; the Goblin Archer's surprise nick (6 HP) dropped them to 8 before combat resolved — and a standard goblin then finished a fighter who should have survived. Same shape at T2 (Dire Wolf nick 4 on hp=3), T3 (Fire Elemental nick 4 on hp=5), T4-T5. The nick was acting as a hidden cascade multiplier, pre-empting the combat engine on wounded entries. clampSurpriseNick caps the nick at max(1, hpCurrent/5) when the fighter enters wounded (HPCurrent < HPMax); at full HP the raw nick stands. The existing 'nick < HPCurrent' KO-guard is preserved as a backstop. /5 is the wounded-fighter lethality knob; tighter (/10) is gentler, looser (/3) re-opens the cascade. Live caller (runHarvestInterrupt) and harness (runHarnessFight) both route through the new helper so the sim measures the same lever the live caller applies. Matrix delta is mild (encs +0.1-0.2 per cell, completion% still 0%) but the tier-lethality trace stretches substantively: T1 trial 0 ran 5→8 encs / 5→7 days, T3 trial 1 saw a fighter survive multiple chained interrupts at low HP that pre-2b would have ended on nick alone. The remaining deaths are now legible as elite-one-shot fights on fresh entries (Warchief, Green Hag, Roper, Young Red Dragon) — that's the Phase 2c roster-gate signal. Push-back on the original lever-order: the Phase 2a recap put roster gate first, but the trace fingerprint named the wounded-entry nick as the dominant cause-of-death in 4 of 5 tier traces. Doing nick-cap first keeps Phase 2c's diagnostic clean and avoids re-tuning rosters after another lever changes the shape under us. Pre-existing failures unrelated to this change: - TestAdv2Scenario_ZoneRunGoblinWarrens (advance regression, prior) - TestMageSpellbookLineInRender (render assertion, prior) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,16 +121,14 @@ func (p *AdventurePlugin) runHarvestInterrupt(
|
||||
}
|
||||
|
||||
// Surprise round (§4.1): a single free enemy swing nicks HP. We cap at
|
||||
// HP-1 so the nick alone can't KO; real combat resolves below.
|
||||
// HP-1 so the nick alone can't KO; real combat resolves below. The
|
||||
// wounded-entrant clamp (clampSurpriseNick) softens the nick further
|
||||
// when the fighter is already below full HP — see that helper for
|
||||
// the death-spiral motivation.
|
||||
preDmg := surpriseRoundNick(monster, int(zone.Tier))
|
||||
dndChar, _ := LoadDnDCharacter(userID)
|
||||
if dndChar != nil && preDmg > 0 {
|
||||
if preDmg >= dndChar.HPCurrent {
|
||||
preDmg = dndChar.HPCurrent - 1
|
||||
if preDmg < 0 {
|
||||
preDmg = 0
|
||||
}
|
||||
}
|
||||
preDmg = clampSurpriseNick(preDmg, dndChar.HPCurrent, dndChar.HPMax)
|
||||
dndChar.HPCurrent -= preDmg
|
||||
_ = SaveDnDCharacter(dndChar)
|
||||
}
|
||||
@@ -216,6 +214,49 @@ func surpriseRoundNick(m DnDMonsterTemplate, tier int) int {
|
||||
return dmg
|
||||
}
|
||||
|
||||
// clampSurpriseNick scales the surprise-round nick down for fighters who
|
||||
// enter combat already wounded. The raw nick is fine on a fresh entry
|
||||
// (full HP), but chained interrupts create a death-spiral: an over-tier
|
||||
// elite drops the fighter to ~25% HP and a retreat, then the next
|
||||
// standard fight's surprise nick — landing on already-low HP — pre-empts
|
||||
// the combat engine entirely. The Phase 2 tier-lethality trace
|
||||
// (TestExpeditionBalance_Phase2_TierLethality) showed this cascade was
|
||||
// the cause of death in 4 of 5 tier traces post-2a, not the elites
|
||||
// themselves.
|
||||
//
|
||||
// Policy: at full HP, raw nick stands. When wounded (HPCurrent < HPMax),
|
||||
// cap the nick at max(1, HPCurrent/5) so a wounded fighter loses at most
|
||||
// ~20% of remaining HP to the free swing — they enter the fight bruised
|
||||
// but with margin to fight back. The existing KO-guard (nick < HP) is
|
||||
// preserved as a hard backstop.
|
||||
//
|
||||
// Tuning surface: the /5 divisor is the wounded-fighter lethality knob.
|
||||
// Tighter (e.g. /10) is gentler; looser (/3) re-opens the cascade. See
|
||||
// gogobee_expedition_difficulty.md Phase 2b.
|
||||
func clampSurpriseNick(rawNick, hpCurrent, hpMax int) int {
|
||||
if rawNick <= 0 || hpCurrent <= 0 {
|
||||
return 0
|
||||
}
|
||||
nick := rawNick
|
||||
if hpCurrent < hpMax {
|
||||
cap := hpCurrent / 5
|
||||
if cap < 1 {
|
||||
cap = 1
|
||||
}
|
||||
if nick > cap {
|
||||
nick = cap
|
||||
}
|
||||
}
|
||||
// KO-guard: surprise alone never finishes the fighter.
|
||||
if nick >= hpCurrent {
|
||||
nick = hpCurrent - 1
|
||||
}
|
||||
if nick < 0 {
|
||||
nick = 0
|
||||
}
|
||||
return nick
|
||||
}
|
||||
|
||||
// ── Kill-log writer ─────────────────────────────────────────────────────────
|
||||
|
||||
// monsterKillTags maps a bestiary ID to the kill-log tags it satisfies.
|
||||
|
||||
@@ -372,14 +372,11 @@ func (h *expeditionHarness) runHarnessFight(zone ZoneDefinition, elite bool) Com
|
||||
}
|
||||
|
||||
// Surprise-round nick — same shape as runHarvestInterrupt's
|
||||
// pre-combat HP shave. Cap at HP-1 so the nick alone can't KO.
|
||||
nick := surpriseRoundNick(monster, int(zone.Tier))
|
||||
if nick >= h.char.HPCurrent {
|
||||
nick = h.char.HPCurrent - 1
|
||||
if nick < 0 {
|
||||
nick = 0
|
||||
}
|
||||
}
|
||||
// pre-combat HP shave, including the wounded-entrant clamp
|
||||
// (clampSurpriseNick) that breaks the chained-interrupt death
|
||||
// spiral. Mirror live exactly so the harness measures the same
|
||||
// lever the live caller applies.
|
||||
nick := clampSurpriseNick(surpriseRoundNick(monster, int(zone.Tier)), h.char.HPCurrent, h.char.HPMax)
|
||||
h.char.HPCurrent -= nick
|
||||
|
||||
player := buildHarnessPlayer(h.char)
|
||||
|
||||
Reference in New Issue
Block a user