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:
2026-09-20 19:11:39 +02:00
parent 83c6e541ac
commit 9cfcd67f70
3 changed files with 47 additions and 4 deletions
+5 -1
View File
@@ -72,7 +72,11 @@
"crit_mult": 2, "crit_mult": 2,
"reach_ft": 5, "reach_ft": 5,
"count": 1, "count": 1,
"range_increment_ft": 10 "range_increment_ft": 10,
"splash": {
"damage": 1,
"type": "fire"
}
}, },
{ {
"name": "Attaques naturelles (Shifter Claws / formes fées)", "name": "Attaques naturelles (Shifter Claws / formes fées)",
+5 -1
View File
@@ -111,7 +111,11 @@
"crit_mult": 2, "crit_mult": 2,
"reach_ft": 5, "reach_ft": 5,
"count": 1, "count": 1,
"range_increment_ft": 10 "range_increment_ft": 10,
"splash": {
"damage": 1,
"type": "fire"
}
}, },
{ {
"name": "Unarmed Strike", "name": "Unarmed Strike",
+37 -2
View File
@@ -7,9 +7,9 @@ never sees raw JSON.
from __future__ import annotations 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.abilities import AbilitySpec # noqa: TC001
from pf1e_simulator.dice import DiceExpr, DiceParseError, parse_dice from pf1e_simulator.dice import DiceExpr, DiceParseError, parse_dice
@@ -232,6 +232,24 @@ class DamageComponent(_FrozenModel):
return value 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, ...] = ( _HAFTED_WEAPON_TOKENS: tuple[str, ...] = (
"axe", "spear", "hammer", "mace", "flail", "staff", "pick", "javelin", "axe", "spear", "hammer", "mace", "flail", "staff", "pick", "javelin",
) )
@@ -291,6 +309,23 @@ class AttackSpec(_FrozenModel):
hardness: int | None = Field(default=None, ge=0) hardness: int | None = Field(default=None, ge=0)
hit_points: int | None = Field(default=None, ge=1) hit_points: int | None = Field(default=None, ge=1)
enhancement: int = Field(default=0, ge=0, le=20) 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]: def object_stats(self) -> tuple[int, int]:
"""Hardness and hit points of the weapon as an OBJECT (Medium item). """Hardness and hit points of the weapon as an OBJECT (Medium item).