mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
J1: Extra Attack now fires in turn-based combat
The class-identity audit (98ba416) wired Extra Attack via the new
resolvePlayerSwings helper, but only SimulateCombat (auto-resolve)
called it. The turn-based engine — every !fight/!attack and every
elite/boss gate the sim drives via autoResolveCombat — still called
single-swing resolvePlayerAttack, so Fighter L11+ got 1 swing/turn at
the gates instead of 3. The audit close-out was correct in spirit but
half-applied.
J1 baseline matrix surfaced it: Fighter L12 cleared 100% of T2 forest
but 2% of T3 manor and 7% of T4 underdark, with %boss_reached at 100%
across the board. The wall was the boss-room damage exchange, not
mid-zone attrition. Trace dump on a sample fight: Fighter dealt 79
dmg in 14 rounds (7 hits / 9 swings) — exactly one swing per round —
versus 167 enemy dmg. With multi-swing wired in, the same fight ends
in 7 rounds with the boss dead, Fighter at 87/168 HP, 16 hits in 19
swings.
n=100 matrix after the fix:
Fighter L12 manor: 2% → 100% clr
Fighter L12 underdark: 7% → 98% clr
Fighter L12 forest: 94% → 100% (no leader regression)
Mage cells unchanged (J2 territory). Rogue cells within noise.
Sim infra changes that landed alongside (needed to read the J1
signal):
* expedition_sim auto-arms class-default defensive abilities
(Second Wind / Healing Word) via the new simAutoArmEnabled toggle
+ trySimAutoArm helper, hooked before applyArmedAbility in both
combat builders. Production code paths untouched (toggle stays
off). Without this the sim simulated a player who never types
!arm, which under-counts class survival.
* SimResult.Combats captures per-fight turn-log summaries (rounds,
hits/misses, damage by side, AC values inferred from RollAgainst)
so future J-phase questions can dig into the engine without
re-running the matrix.
* sim_results/run_matrix.sh fans the matrix across (class,level,zone)
cells via xargs -P (one process per cell — each owns its global
sqlite handle). ~6× wall-clock speedup on a 14-core box; n=100
matrix runs in ~3min.
* sim_results/summarize.sh gains p50_yld_clr + %boss_reached columns
so future sweeps don't conflate "reaches boss" with "clears zone".
Baselines:
sim_results/baseline_j0_n100.jsonl — pre-fix (1350 rows)
sim_results/baseline_j1_extra_attack.jsonl — post-fix (4500 rows)
Phase J state: J0 baseline locked, J1 done. T5 dragons_lair still
0% clear universally (J3). Mage T2+ wall still real (J2).
This commit is contained in:
@@ -357,6 +357,59 @@ func displayAbility(id string) string {
|
||||
|
||||
// ── Combat hook ──────────────────────────────────────────────────────────────
|
||||
|
||||
// simAutoArmEnabled, when true, lets the combat builders pre-arm a
|
||||
// class-default ability for any character entering combat with an empty
|
||||
// ArmedAbility slot. The expedition-sim flips this on so synthetic
|
||||
// players model a competent real player (who would `!arm` Second Wind
|
||||
// before each fight) instead of a player who never touches the
|
||||
// !arm command. Untouched in production code paths.
|
||||
var simAutoArmEnabled = false
|
||||
|
||||
// simAutoArmDefaultFor returns the class-default ability id the sim
|
||||
// should pre-arm. Defensive heals (Second Wind, Healing Word) only —
|
||||
// burning a Mage's spell slot every fight would mask J2, not help it.
|
||||
func simAutoArmDefaultFor(class DnDClass) string {
|
||||
switch class {
|
||||
case ClassFighter:
|
||||
return "second_wind"
|
||||
case ClassCleric:
|
||||
return "healing_word"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// trySimAutoArm pre-arms the class-default ability if the global toggle
|
||||
// is on, no ability is currently armed, and the resource pool has at
|
||||
// least one charge. Mirrors handleDnDArmCmd's spend-and-save flow.
|
||||
// Returns the ability name (or "" when nothing was armed).
|
||||
func trySimAutoArm(c *DnDCharacter) string {
|
||||
if !simAutoArmEnabled || c == nil || c.ArmedAbility != "" {
|
||||
return ""
|
||||
}
|
||||
id := simAutoArmDefaultFor(c.Class)
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
ab, ok := dndActiveAbilities[id]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
cur, _, err := getResource(c.UserID, ab.Resource)
|
||||
if err != nil || cur < 1 {
|
||||
return ""
|
||||
}
|
||||
c.ArmedAbility = ab.ID
|
||||
if err := SaveDnDCharacter(c); err != nil {
|
||||
return ""
|
||||
}
|
||||
if ok, _ := spendResource(c.UserID, ab.Resource, 1); !ok {
|
||||
c.ArmedAbility = ""
|
||||
_ = SaveDnDCharacter(c)
|
||||
return ""
|
||||
}
|
||||
return ab.Name
|
||||
}
|
||||
|
||||
// applyArmedAbility checks for a pre-armed ability on the character and
|
||||
// applies its effect to the player's CombatModifiers, then clears the armed
|
||||
// flag. Called from combat_bridge.go before SimulateCombat.
|
||||
|
||||
Reference in New Issue
Block a user