feat(grid): apply elevation to movement cost and high ground
This commit is contained in:
@@ -121,6 +121,7 @@ _RANGE_INCREMENT_PENALTY = 2 # PF1e: -2 per full range increment beyond the fir
|
||||
_MAX_RANGE_INCREMENTS = 10 # PF1e: projectile weapons shoot up to 10 increments
|
||||
_MAX_TOUCH_RANGE_INCREMENTS = 5 # PF1e: thrown/firearm touch weapons shoot up to 5 increments
|
||||
_FLANK_BONUS = 2 # PF1e: +2 melee attack when ally threatens opposite side
|
||||
_HIGH_GROUND_BONUS = 1 # PF1e: +1 melee attack when attacker stands higher than target
|
||||
_ITERATIVE_PENALTY = 5 # PF1e: each BAB iterative is at -5 from the previous
|
||||
_MAX_ITERATIVES = 4 # PF1e: BAB +16 gives 4 attacks (at +16, +11, +6, +1)
|
||||
_WEAPON_DURABILITY = 10 # Accumulated sunder damage that destroys a weapon.
|
||||
@@ -318,6 +319,7 @@ class AttackResult:
|
||||
ac: int
|
||||
penalty: int = 0
|
||||
flank_bonus: int = 0
|
||||
high_ground_bonus: int = 0
|
||||
base_bonus: int = 0
|
||||
|
||||
|
||||
@@ -591,6 +593,10 @@ class CombatEngine:
|
||||
for cell_b in cells_b
|
||||
)
|
||||
|
||||
def _elevation(self, state: CombatantState) -> int:
|
||||
"""Height of the creature's anchor cell (0 when no elevation layer)."""
|
||||
return self._grid.elevation_at(state.pos)
|
||||
|
||||
def _cells_free(self, cells: Iterable[Pos], mover: CombatantState) -> bool:
|
||||
"""True if every cell is in bounds, passable, and unoccupied by others."""
|
||||
blocked = self._blocked(mover)
|
||||
@@ -842,6 +848,12 @@ class CombatEngine:
|
||||
dist_ft = self.creature_distance(attacker, target) * _SQUARE_FT
|
||||
penalty = self.range_penalty(weapon, dist_ft)
|
||||
flank = self._flanking_bonus(attacker, target) if weapon.kind == "melee" else 0
|
||||
high_ground = (
|
||||
_HIGH_GROUND_BONUS
|
||||
if weapon.kind == "melee"
|
||||
and self._elevation(attacker) > self._elevation(target)
|
||||
else 0
|
||||
)
|
||||
base_bonus = bonus_override if bonus_override is not None else weapon.attack_bonus
|
||||
atk_mods = self._stat_modifiers(
|
||||
attacker, "attack", weapon_name=weapon.name, weapon=weapon, dist_ft=dist_ft
|
||||
@@ -850,7 +862,7 @@ class CombatEngine:
|
||||
atk_mods.append(StatModifier(target="attack", value=-4))
|
||||
attack_mod = resolve_modifiers(atk_mods)
|
||||
pa_amt = self._power_attack_amt(attacker) if weapon.kind == "melee" else 0
|
||||
total = roll + base_bonus + penalty + flank + attack_mod - pa_amt
|
||||
total = roll + base_bonus + penalty + flank + high_ground + attack_mod - pa_amt
|
||||
# Check if target denies DEX to AC (flat-footed)
|
||||
denies_dex = self.has_condition_flag(target, "denies_dex_to_ac")
|
||||
full_ac_base = (
|
||||
@@ -895,7 +907,9 @@ class CombatEngine:
|
||||
if hit:
|
||||
if roll != _NATURAL_ONE and roll >= weapon.crit_range:
|
||||
confirm = self._rng.d20()
|
||||
confirm_total = confirm + base_bonus + penalty + flank + attack_mod - pa_amt
|
||||
confirm_total = (
|
||||
confirm + base_bonus + penalty + flank + high_ground + attack_mod - pa_amt
|
||||
)
|
||||
crit = confirm != _NATURAL_ONE and confirm_total >= ac
|
||||
dmg_mods = self._stat_modifiers(
|
||||
attacker, "damage", weapon_name=weapon.name, weapon=weapon, dist_ft=dist_ft
|
||||
@@ -909,7 +923,8 @@ class CombatEngine:
|
||||
target.hp -= damage
|
||||
return AttackResult(
|
||||
hit=hit, crit=crit, damage=damage, roll=roll, total=total, ac=ac,
|
||||
penalty=penalty, flank_bonus=flank, base_bonus=base_bonus,
|
||||
penalty=penalty, flank_bonus=flank, high_ground_bonus=high_ground,
|
||||
base_bonus=base_bonus,
|
||||
)
|
||||
|
||||
def _apply_dr(self, damage: int, weapon: AttackSpec, target: CombatantState) -> int:
|
||||
@@ -2521,10 +2536,13 @@ class CombatEngine:
|
||||
target_live.damage_taken += result.damage
|
||||
outcome = "CRIT" if result.crit else "HIT" if result.hit else "MISS"
|
||||
flank_str = f"+{result.flank_bonus}(flank)" if result.flank_bonus else ""
|
||||
high_ground_str = (
|
||||
f"+{result.high_ground_bonus}(high)" if result.high_ground_bonus else ""
|
||||
)
|
||||
bonus_str = (
|
||||
f"{result.base_bonus}{result.penalty:+d}{flank_str}"
|
||||
f"{result.base_bonus}{result.penalty:+d}{flank_str}{high_ground_str}"
|
||||
if result.penalty
|
||||
else f"{result.base_bonus}{flank_str}"
|
||||
else f"{result.base_bonus}{flank_str}{high_ground_str}"
|
||||
)
|
||||
line = (
|
||||
f"round {self._current_round} {attacker.combatant.id}: {label} "
|
||||
|
||||
Reference in New Issue
Block a user