fix(combat): summoned creatures act and expire
This commit is contained in:
@@ -339,6 +339,8 @@ class CombatEngine:
|
||||
self._stats: dict[str, _LiveStats] = {s.combatant.id: _LiveStats() for s in states}
|
||||
self._current_round = 0
|
||||
self._aoo_used: set[str] = set()
|
||||
self._initiative_keys: dict[str, tuple[int, int, int]] = {}
|
||||
self._pending_summons: list[CombatantState] = []
|
||||
|
||||
@property
|
||||
def states(self) -> list[CombatantState]:
|
||||
@@ -410,17 +412,13 @@ class CombatEngine:
|
||||
|
||||
def run(self) -> CombatResult:
|
||||
"""Run the battle and return the final result and transcript."""
|
||||
order = self._roll_initiative()
|
||||
self._roll_initiative()
|
||||
round_no = 1
|
||||
while round_no <= self._round_cap:
|
||||
self._current_round = round_no
|
||||
self._aoo_used.clear()
|
||||
for state in order:
|
||||
if not state.active:
|
||||
continue
|
||||
if self._take_turn(state):
|
||||
break
|
||||
if self._winner_side() is not None:
|
||||
self._expire_summons()
|
||||
if self._run_round():
|
||||
break
|
||||
round_no += 1
|
||||
winner = self._winner_side()
|
||||
@@ -436,6 +434,31 @@ class CombatEngine:
|
||||
stats=stats,
|
||||
)
|
||||
|
||||
def _run_round(self) -> bool:
|
||||
"""Execute one round; return True if the battle ended."""
|
||||
order = self._build_round_order()
|
||||
for state in order:
|
||||
if not state.active:
|
||||
continue
|
||||
if self._take_turn(state):
|
||||
return True
|
||||
if self._drive_pending_summons():
|
||||
return True
|
||||
self._pending_summons.clear()
|
||||
return self._winner_side() is not None
|
||||
|
||||
def _drive_pending_summons(self) -> bool:
|
||||
"""Give summoned creatures their turn immediately; return True if battle ended."""
|
||||
while self._pending_summons:
|
||||
summoned = self._pending_summons.pop(0)
|
||||
if (
|
||||
summoned in self._states
|
||||
and summoned.active
|
||||
and self._take_turn(summoned)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def nearest_enemy(self, state: CombatantState) -> CombatantState | None:
|
||||
"""Closest active enemy by grid distance; ties keep list order."""
|
||||
enemies = [s for s in self._states if s.side != state.side and s.active]
|
||||
@@ -658,13 +681,17 @@ class CombatEngine:
|
||||
for state in self._states:
|
||||
roll = self._rng.d20()
|
||||
rolls[state.combatant.id] = (roll, roll + state.combatant.initiative_mod)
|
||||
order = sorted(
|
||||
self._states,
|
||||
key=lambda s: (
|
||||
self._initiative_keys = {
|
||||
s.combatant.id: (
|
||||
rolls[s.combatant.id][1],
|
||||
s.combatant.initiative_mod,
|
||||
self._states.index(s),
|
||||
),
|
||||
)
|
||||
for s in self._states
|
||||
}
|
||||
order = sorted(
|
||||
self._states,
|
||||
key=lambda s: self._initiative_keys[s.combatant.id],
|
||||
reverse=True,
|
||||
)
|
||||
for state in order:
|
||||
@@ -675,6 +702,42 @@ class CombatEngine:
|
||||
)
|
||||
return order
|
||||
|
||||
def _build_round_order(self) -> list[CombatantState]:
|
||||
"""Build initiative order for this round from current ``_states``.
|
||||
|
||||
Summoned creatures (not present at the start of combat) inherit their
|
||||
caster's initiative key so they act immediately after the caster via
|
||||
stable sort.
|
||||
"""
|
||||
|
||||
def sort_key(s: CombatantState) -> tuple[int, int, int]:
|
||||
key = self._initiative_keys.get(s.combatant.id)
|
||||
if key is not None:
|
||||
return key
|
||||
caster_id = s.summon_caster_id
|
||||
if caster_id and caster_id in self._initiative_keys:
|
||||
ck = self._initiative_keys[caster_id]
|
||||
return (ck[0], ck[1], ck[2])
|
||||
return (0, 0, -999)
|
||||
|
||||
return sorted(self._states, key=sort_key, reverse=True)
|
||||
|
||||
def _expire_summons(self) -> None:
|
||||
"""Decrement summon durations and remove expired summoned creatures."""
|
||||
expired: list[CombatantState] = []
|
||||
for state in list(self._states):
|
||||
if state.summon_duration > 0:
|
||||
state.summon_duration -= 1
|
||||
if state.summon_duration <= 0:
|
||||
expired.append(state)
|
||||
for state in expired:
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: summon expired"
|
||||
)
|
||||
self._states.remove(state)
|
||||
if state in self._pending_summons:
|
||||
self._pending_summons.remove(state)
|
||||
|
||||
def _winner_side(self) -> str | None:
|
||||
active_sides = {s.side for s in self._states if s.active}
|
||||
if len(active_sides) == 1:
|
||||
@@ -1860,6 +1923,9 @@ class CombatEngine:
|
||||
summoned_state.summon_duration = effect.duration_rounds
|
||||
summoned_state.summon_caster_id = state.combatant.id
|
||||
|
||||
self._stats[summoned_state.combatant.id] = _LiveStats()
|
||||
self._pending_summons.append(summoned_state)
|
||||
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: "
|
||||
f"summon {summoned_combatant.id} at {spawn_pos} ({effect.duration_rounds}r)"
|
||||
|
||||
Reference in New Issue
Block a user