feat(rules): SplashSpec data model - data-driven splash marker on thrown weapons, Alchemist's Fire splash 1 fire on oni/tammara (SRD Throw Splash Weapon, Alchemist's fire)
This commit is contained in:
+5
-1
@@ -72,7 +72,11 @@
|
||||
"crit_mult": 2,
|
||||
"reach_ft": 5,
|
||||
"count": 1,
|
||||
"range_increment_ft": 10
|
||||
"range_increment_ft": 10,
|
||||
"splash": {
|
||||
"damage": 1,
|
||||
"type": "fire"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attaques naturelles (Shifter Claws / formes fées)",
|
||||
|
||||
@@ -111,7 +111,11 @@
|
||||
"crit_mult": 2,
|
||||
"reach_ft": 5,
|
||||
"count": 1,
|
||||
"range_increment_ft": 10
|
||||
"range_increment_ft": 10,
|
||||
"splash": {
|
||||
"damage": 1,
|
||||
"type": "fire"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Unarmed Strike",
|
||||
|
||||
@@ -7,9 +7,9 @@ never sees raw JSON.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import Literal, Self
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from pf1e_simulator.abilities import AbilitySpec # noqa: TC001
|
||||
from pf1e_simulator.dice import DiceExpr, DiceParseError, parse_dice
|
||||
@@ -232,6 +232,24 @@ class DamageComponent(_FrozenModel):
|
||||
return value
|
||||
|
||||
|
||||
class SplashSpec(_FrozenModel):
|
||||
"""Splash payload of a thrown splash weapon (SRD "Throw Splash Weapon").
|
||||
|
||||
``damage`` is the FLAT splash damage every creature within 5 feet of the
|
||||
point of impact takes on a direct hit — no attack roll, no saving throw
|
||||
(SRD "Alchemist's fire": "Every creature within 5 feet of the point where
|
||||
the flask hits takes 1 point of fire damage from the splash"). ``type`` is
|
||||
the energy type ("fire", "acid", ...): splash is energy damage, so the
|
||||
component-based energy resistance/immunity path applies and damage
|
||||
reduction does not; the flat amount is never multiplied on a critical hit.
|
||||
On a miss the weapon scatters (1d8 direction, one range increment) and
|
||||
deals only this splash at where it landed.
|
||||
"""
|
||||
|
||||
damage: int = Field(ge=0)
|
||||
type: str = Field(min_length=1)
|
||||
|
||||
|
||||
_HAFTED_WEAPON_TOKENS: tuple[str, ...] = (
|
||||
"axe", "spear", "hammer", "mace", "flail", "staff", "pick", "javelin",
|
||||
)
|
||||
@@ -291,6 +309,23 @@ class AttackSpec(_FrozenModel):
|
||||
hardness: int | None = Field(default=None, ge=0)
|
||||
hit_points: int | None = Field(default=None, ge=1)
|
||||
enhancement: int = Field(default=0, ge=0, le=20)
|
||||
# SRD "Throw Splash Weapon": a thrown splash weapon (alchemist's fire,
|
||||
# acid, holy water) is a ranged attack with a splash payload; it must be
|
||||
# thrown (ranged kind or ranged touch with a range increment), so the
|
||||
# validator below rejects splash on melee or increment-less attacks.
|
||||
splash: SplashSpec | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _splash_needs_thrown_range(self) -> Self:
|
||||
if self.splash is not None and (
|
||||
self.kind == "melee" or self.range_increment_ft is None
|
||||
):
|
||||
msg = (
|
||||
f"attack {self.id!r}: splash requires a thrown weapon "
|
||||
"(kind ranged/touch with range_increment_ft)"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return self
|
||||
|
||||
def object_stats(self) -> tuple[int, int]:
|
||||
"""Hardness and hit points of the weapon as an OBJECT (Medium item).
|
||||
|
||||
Reference in New Issue
Block a user