feat(combat): honor AttackSpec.count for multi-swing attacks
A weapon with count>1 ('2x Talons') now resolves one independent attack
roll per swing in the same attack action (label '#N'), aggregating stats
per swing. Remaining swings are lost if the target drops mid-routine.
BAB iterative attacks remain out of Phase 0 scope.
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
Phase 0 documented deviations from PF1e (conventions):
|
||||
- One action per turn: move OR attack (no move+attack, no AoO, no flanking).
|
||||
- A weapon with `count` > 1 ("2x Talons") resolves `count` independent attacks
|
||||
in one attack action; remaining swings are lost once the target drops
|
||||
(down or dead) mid-routine. BAB iterative attacks are not modeled.
|
||||
- Natural 1 always misses; natural 20 always hits and threatens a crit.
|
||||
- A confirmed crit multiplies the total damage (dice + flat bonus) by crit_mult.
|
||||
- DR applies once, after crit multiplication; any bypassing type defeats DR.
|
||||
@@ -277,25 +280,31 @@ class CombatEngine:
|
||||
def _attack(
|
||||
self, attacker: CombatantState, target: CombatantState, weapon: AttackSpec, round_no: int
|
||||
) -> None:
|
||||
hp_before = target.hp
|
||||
result = self.resolve_attack(attacker, target, weapon)
|
||||
live = self._stats[attacker.combatant.id]
|
||||
live.hits += int(result.hit)
|
||||
live.crits += int(result.crit)
|
||||
live.damage_dealt += result.damage
|
||||
self._stats[target.combatant.id].damage_taken += result.damage
|
||||
outcome = "CRIT" if result.crit else "HIT" if result.hit else "MISS"
|
||||
line = (
|
||||
f"round {round_no} {attacker.combatant.id}: {weapon.name} vs {target.combatant.id} "
|
||||
f"d20={result.roll}+{weapon.attack_bonus}={result.total} AC {result.ac} -> {outcome}"
|
||||
)
|
||||
if result.hit:
|
||||
line += f" {result.damage} damage ({hp_before}->{target.hp})"
|
||||
self._log(line)
|
||||
if target.dead:
|
||||
self._log(f"round {round_no} {attacker.combatant.id}: {target.combatant.id} dead")
|
||||
elif not target.active:
|
||||
self._log(f"round {round_no} {attacker.combatant.id}: {target.combatant.id} down")
|
||||
target_live = self._stats[target.combatant.id]
|
||||
for swing in range(1, weapon.count + 1):
|
||||
hp_before = target.hp
|
||||
result = self.resolve_attack(attacker, target, weapon)
|
||||
live.hits += int(result.hit)
|
||||
live.crits += int(result.crit)
|
||||
live.damage_dealt += result.damage
|
||||
target_live.damage_taken += result.damage
|
||||
outcome = "CRIT" if result.crit else "HIT" if result.hit else "MISS"
|
||||
label = weapon.name if weapon.count == 1 else f"{weapon.name} #{swing}"
|
||||
line = (
|
||||
f"round {round_no} {attacker.combatant.id}: {label} vs {target.combatant.id} "
|
||||
f"d20={result.roll}+{weapon.attack_bonus}={result.total} "
|
||||
f"AC {result.ac} -> {outcome}"
|
||||
)
|
||||
if result.hit:
|
||||
line += f" {result.damage} damage ({hp_before}->{target.hp})"
|
||||
self._log(line)
|
||||
if target.dead:
|
||||
self._log(f"round {round_no} {attacker.combatant.id}: {target.combatant.id} dead")
|
||||
return
|
||||
if not target.active:
|
||||
self._log(f"round {round_no} {attacker.combatant.id}: {target.combatant.id} down")
|
||||
return
|
||||
|
||||
def _move(self, state: CombatantState, target: CombatantState, round_no: int) -> None:
|
||||
step = self._step_toward(state, target)
|
||||
|
||||
Reference in New Issue
Block a user