mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
N3/P2: give the combat engine an N-player roster
Splits combatState into a fight-scoped half and a per-character half. Everything that belongs to one PC -- HP, ward/spore/reflect charges, heal charges, poison ticks, the death save, Lucky/Rage, the first-attack one-shots, the arcane ward, concentration, and the debuffs an enemy stacks onto a specific character -- moves to a new `actor`. What belongs to the fight stays: the enemy pool, the enemy's stance (evade/block/advantage/retaliate/regen/survive), the round counter, the event log, and the RNG stream. combatState embeds *actor, so the promoted fields keep their names and all ~230 existing reads (st.playerHP, st.wardCharges, ...) compile untouched. The embedded pointer is a cursor: seat(i) points it at a roster member. Solo seats one actor and never moves the cursor, so the draw order off the single RNG stream is unchanged. That is the whole point. TestCombatCharacterization -- 57 scenarios x 5 seeds, 7468 pinned golden lines -- is byte-identical before and after. Solo combat provably did not move, so the d8prereq balance corpus survives the parties work and only party bands need new baselines in P7. Hold-person is fight-scoped (holding the enemy holds it for everyone) while stat_drain/debuff/max_hp_drain are per-character, which is why they landed on opposite sides of the split. No multi-actor *semantics* here: nothing yet decides who the enemy swings at or how initiative interleaves N players. That is P3. This commit only lands the data model, and the roster tests cover what the solo golden structurally cannot see -- cursor isolation, shared-state visibility across seats, and the pointer embed (a value embed would silently copy on seat() and fail the round-trip assertion).
This commit is contained in:
@@ -111,17 +111,15 @@ func phaseOrdinal(phase string) uint64 {
|
||||
// resumeTurnEngine rebuilds the in-memory combatState from a persisted session.
|
||||
// rng is the deterministic source for this step (see combatSessionRNG).
|
||||
func resumeTurnEngine(sess *CombatSession, player, enemy *Combatant, rng *rand.Rand) *turnEngine {
|
||||
st := &combatState{
|
||||
playerHP: sess.PlayerHP,
|
||||
enemyHP: sess.EnemyHP,
|
||||
round: sess.Round,
|
||||
poisonTicks: sess.Statuses.PoisonTicks,
|
||||
poisonDmg: sess.Statuses.PoisonDmg,
|
||||
stunPlayer: sess.Statuses.StunPlayer,
|
||||
enraged: sess.Statuses.Enraged,
|
||||
armorBroken: sess.Statuses.ArmorBroken,
|
||||
armorBreakAmt: sess.Statuses.ArmorBreakAmt,
|
||||
enemySkipFirst: sess.Statuses.EnemySkipNext,
|
||||
// The seated character's half of the persisted statuses. A single-seat
|
||||
// roster today; P4 gives combat_session per-participant rows and this
|
||||
// becomes one actor per member.
|
||||
seat0 := &actor{
|
||||
c: player,
|
||||
playerHP: sess.PlayerHP,
|
||||
poisonTicks: sess.Statuses.PoisonTicks,
|
||||
poisonDmg: sess.Statuses.PoisonDmg,
|
||||
stunPlayer: sess.Statuses.StunPlayer,
|
||||
// Fight-scoped depleting resources + once-per-fight one-shots: restored
|
||||
// from the persisted statuses so a charge or "already used" flag can't
|
||||
// reset across a suspend/resume. commit writes the updated values back.
|
||||
@@ -139,6 +137,20 @@ func resumeTurnEngine(sess *CombatSession, player, enemy *Combatant, rng *rand.R
|
||||
firstAttackBonusUsed: sess.Statuses.FirstAtkBonusUsed,
|
||||
assassinateRerollUsed: sess.Statuses.AssassinateReroll,
|
||||
assassinateBonusUsed: sess.Statuses.AssassinateBonus,
|
||||
// Enemy debuffs stacked onto this character specifically.
|
||||
playerAtkDrain: sess.Statuses.PlayerAtkDrain,
|
||||
playerACDebuff: sess.Statuses.PlayerACDebuff,
|
||||
maxHPDrain: sess.Statuses.MaxHPDrain,
|
||||
}
|
||||
st := &combatState{
|
||||
actor: seat0,
|
||||
actors: []*actor{seat0},
|
||||
enemyHP: sess.EnemyHP,
|
||||
round: sess.Round,
|
||||
enraged: sess.Statuses.Enraged,
|
||||
armorBroken: sess.Statuses.ArmorBroken,
|
||||
armorBreakAmt: sess.Statuses.ArmorBreakAmt,
|
||||
enemySkipFirst: sess.Statuses.EnemySkipNext,
|
||||
// Slice-3 stateful monster-ability effects — armed by applyAbility,
|
||||
// round-tripped here so they survive a suspend/resume or reaper auto-play.
|
||||
enemyEvadeNext: sess.Statuses.EnemyEvadeNext,
|
||||
@@ -147,9 +159,6 @@ func resumeTurnEngine(sess *CombatSession, player, enemy *Combatant, rng *rand.R
|
||||
enemyRetaliateFrac: sess.Statuses.EnemyRetaliateFrac,
|
||||
enemyRegen: sess.Statuses.EnemyRegen,
|
||||
enemySurviveArmed: sess.Statuses.EnemySurviveArmed,
|
||||
playerAtkDrain: sess.Statuses.PlayerAtkDrain,
|
||||
playerACDebuff: sess.Statuses.PlayerACDebuff,
|
||||
maxHPDrain: sess.Statuses.MaxHPDrain,
|
||||
// Slice-4 monster-ability effects — the former flavor-only placeholders.
|
||||
enemySpellResist: sess.Statuses.EnemySpellResist,
|
||||
enemyRevealNext: sess.Statuses.EnemyRevealNext,
|
||||
|
||||
Reference in New Issue
Block a user