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,
"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)",
+5 -1
View File
@@ -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",
+37 -2
View File
@@ -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).