mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Review fixes: align party enemy-HP scaling in the !fight entry path
The P8 diff scaled the enemy's max HP ×1.15 for parties at persist and per-turn rebuild, but the !fight command's own template stayed unscaled: the entry banner reported the pre-scale HP, and the opening-round settle resolved the enemy against the wrong MaxHP ceiling (regen clamp, bloodied threshold). Mirror the scalar for the banner and align the in-memory template before the settle. Solo scales by 1.0, so it is untouched. Also extract enemyActionPlan() so both combat engines share the one load-bearing action-count computation instead of duplicating it. Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
@@ -101,7 +101,11 @@ func (p *AdventurePlugin) handleFightCmd(ctx MessageContext) error {
|
||||
if refusal != "" {
|
||||
return p.replyDM(ctx, refusal)
|
||||
}
|
||||
enemyHP := enemy.Stats.MaxHP
|
||||
// The persisted session scales enemy HP for a party (solo scales by 1.0, so
|
||||
// this is a no-op there); mirror it here so the entry banner and the opening
|
||||
// round resolve against the same ceiling startPartyCombatSession persisted and
|
||||
// the rebuilt rounds use.
|
||||
enemyHP := scaledEnemyMaxHP(enemy.Stats.MaxHP, len(seats))
|
||||
|
||||
// Fight-start one-shot resources (Abjuration Arcane Ward, etc.) are seeded
|
||||
// per seat onto the session and its participant rows, so they survive the
|
||||
@@ -130,6 +134,11 @@ func (p *AdventurePlugin) handleFightCmd(ctx MessageContext) error {
|
||||
|
||||
if sess.IsParty() {
|
||||
players := seatCombatants(seats)
|
||||
// Align the in-memory template with the scaled HP persisted above, so the
|
||||
// opening-round settle resolves the enemy against the same MaxHP the
|
||||
// rebuilt rounds do (regen clamp, bloodied-ability threshold). The persist
|
||||
// already happened off the unscaled value, so this does not double-scale.
|
||||
enemy.Stats.MaxHP = enemyHP
|
||||
// The enemy may have won initiative. Resolve everything the round owes
|
||||
// before anyone is asked to act, so the opening block narrates the hit
|
||||
// rather than showing its damage with no explanation.
|
||||
|
||||
@@ -369,6 +369,22 @@ func scaledEnemyMaxHP(baseMaxHP, rosterSize int) int {
|
||||
return int(float64(baseMaxHP) * partyEnemyHPScale(rosterSize))
|
||||
}
|
||||
|
||||
// enemyActionPlan is the shared action budget both combat engines resolve an
|
||||
// enemy turn against: how many attack-actions the enemy takes, and whether the
|
||||
// first one reuses the round's already-picked target (and its already-rolled
|
||||
// procs). A cleave/lifesteal ability already spent the first action, so one fewer
|
||||
// follows and none of them reuse the initial target. Both engines call this so
|
||||
// the load-bearing count stays in lockstep even though their per-action bodies
|
||||
// differ.
|
||||
func enemyActionPlan(st *combatState, abilityDealtDamage bool) (count int, reuseFirst bool) {
|
||||
count = enemyActionsThisRound(st)
|
||||
reuseFirst = !abilityDealtDamage
|
||||
if abilityDealtDamage {
|
||||
count--
|
||||
}
|
||||
return count, reuseFirst
|
||||
}
|
||||
|
||||
// enemyRoundSwings resolves the enemy's attacks for one round of the auto-resolve
|
||||
// engine. A solo roster takes exactly one swing at its single seat, reusing the
|
||||
// round's already-rolled target and pet procs, so its RNG stream and event log are
|
||||
@@ -383,11 +399,7 @@ func scaledEnemyMaxHP(baseMaxHP, rosterSize int) int {
|
||||
func enemyRoundSwings(st *combatState, enemy *Combatant, phase *CombatPhase, seats []CombatResult,
|
||||
target int, abilityDealtDamage, petWhiff, petDeflect, sporeMiss bool) bool {
|
||||
|
||||
swings := enemyActionsThisRound(st)
|
||||
reuseFirst := !abilityDealtDamage
|
||||
if abilityDealtDamage {
|
||||
swings--
|
||||
}
|
||||
swings, reuseFirst := enemyActionPlan(st, abilityDealtDamage)
|
||||
for k := 0; k < swings; k++ {
|
||||
tgt := target
|
||||
sw, sd, sp := petWhiff, petDeflect, sporeMiss
|
||||
|
||||
@@ -673,11 +673,7 @@ func (te *turnEngine) stepEnemyTurn() {
|
||||
// drawn — so its event stream and RNG draws are unchanged. When the ability
|
||||
// already dealt damage it was the first action, so one fewer follows; for solo
|
||||
// that collapses to the old "the ability stood in for the attack" skip.
|
||||
actions := enemyActionsThisRound(te.st)
|
||||
reuseFirstTarget := !abilityDealtDamage
|
||||
if abilityDealtDamage {
|
||||
actions--
|
||||
}
|
||||
actions, reuseFirstTarget := enemyActionPlan(te.st, abilityDealtDamage)
|
||||
for a := 0; a < actions; a++ {
|
||||
if !(reuseFirstTarget && a == 0) {
|
||||
tgt, alive := te.enemyTarget()
|
||||
|
||||
Reference in New Issue
Block a user