chore(lint): restore green gate
This commit is contained in:
+588
-446
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ StatTarget = Literal[
|
|||||||
"linguistics", "perception", "perform", "profession", "ride",
|
"linguistics", "perception", "perform", "profession", "ride",
|
||||||
"sense_motive", "sleight_of_hand", "spellcraft", "stealth",
|
"sense_motive", "sleight_of_hand", "spellcraft", "stealth",
|
||||||
"survival", "swim", "use_magic_device",
|
"survival", "swim", "use_magic_device",
|
||||||
|
"dex_skills",
|
||||||
]
|
]
|
||||||
|
|
||||||
_STACKING_TYPES: frozenset[str] = frozenset({"", "dodge", "racial", "trait"})
|
_STACKING_TYPES: frozenset[str] = frozenset({"", "dodge", "racial", "trait"})
|
||||||
|
|||||||
@@ -26,56 +26,14 @@ if TYPE_CHECKING:
|
|||||||
from pf1e_simulator.loaders.foundry import Json
|
from pf1e_simulator.loaders.foundry import Json
|
||||||
|
|
||||||
from pf1e_simulator.abilities import (
|
from pf1e_simulator.abilities import (
|
||||||
ADOPTED,
|
|
||||||
ALERTNESS,
|
ALERTNESS,
|
||||||
ARMOR_PROFICIENCY_LIGHT,
|
|
||||||
ARMOR_PROFICIENCY_MEDIUM,
|
|
||||||
AXE_TO_GRIND,
|
|
||||||
BITTER,
|
|
||||||
BORN_DAMNED,
|
|
||||||
BREEZE_KISSED,
|
|
||||||
BREW_POTION,
|
|
||||||
COMBAT_REFLEXES,
|
|
||||||
CRAFT_WONDROUS_ITEM,
|
|
||||||
DAZZLING_DISPLAY,
|
|
||||||
DODGE,
|
DODGE,
|
||||||
EXTRA_HEX,
|
|
||||||
ESCHEW_MATERIALS,
|
|
||||||
EXALTED_SOCIETY,
|
|
||||||
EXTREMELY_FASHIONABLE,
|
|
||||||
FATES_FAVORED,
|
|
||||||
FEY_TOUCHED,
|
|
||||||
FOCUSED_STUDY,
|
|
||||||
FORTIFIED_DRINKER,
|
|
||||||
FRIENDLY_FACE,
|
|
||||||
GIFTED_ADEPT,
|
|
||||||
GREAT_FORTITUDE,
|
GREAT_FORTITUDE,
|
||||||
GUNSMITHING,
|
|
||||||
HARMONIC_SPELL,
|
|
||||||
IRON_WILL,
|
IRON_WILL,
|
||||||
LIKE_THE_WIND,
|
|
||||||
LIGHTNING_REFLEXES,
|
LIGHTNING_REFLEXES,
|
||||||
LONELY,
|
|
||||||
LARGER_THAN_LIFE,
|
|
||||||
MAW_OR_CLAW,
|
|
||||||
MOUNTED_COMBAT,
|
|
||||||
POINT_BLANK_SHOT,
|
POINT_BLANK_SHOT,
|
||||||
POWER_ATTACK,
|
POWER_ATTACK,
|
||||||
PRECISE_SHOT,
|
PRECISE_SHOT,
|
||||||
PREHENSILE_TAIL,
|
|
||||||
RAPID_RELOAD,
|
|
||||||
SACRED_SUMS,
|
|
||||||
SELECTIVE_CHANNELING,
|
|
||||||
SKILL_FOCUS_PERCEPTION,
|
|
||||||
SPELL_VULNERABILITY_DIVINATION,
|
|
||||||
STEALTH_SYNERGY,
|
|
||||||
SUMMON_GOOD_MONSTER,
|
|
||||||
THOROUGHBRED,
|
|
||||||
VAGABOND_CHILD,
|
|
||||||
WHISPERING_WIND,
|
|
||||||
VAGABOND_CHILD,
|
|
||||||
WHISPERING_WIND,
|
|
||||||
WEAPON_FINESSE,
|
|
||||||
AbilitySpec,
|
AbilitySpec,
|
||||||
toughness,
|
toughness,
|
||||||
weapon_focus,
|
weapon_focus,
|
||||||
@@ -175,16 +133,17 @@ def load_combatant(path: Path) -> Combatant:
|
|||||||
_auto_attack_ids(data, monster_id)
|
_auto_attack_ids(data, monster_id)
|
||||||
|
|
||||||
# Resolve features (list of strings → list of AbilitySpec)
|
# Resolve features (list of strings → list of AbilitySpec)
|
||||||
level = data.get("level", 1) if isinstance(data.get("level"), int) else 1
|
raw_level = data.get("level")
|
||||||
|
level = raw_level if isinstance(raw_level, int) and not isinstance(raw_level, bool) else 1
|
||||||
raw_features = data.pop("features", None)
|
raw_features = data.pop("features", None)
|
||||||
|
validate_payload: dict[str, object] = dict(data)
|
||||||
if isinstance(raw_features, list):
|
if isinstance(raw_features, list):
|
||||||
# Type ignore: we assign AbilitySpec list into a Json dict before Pydantic validation
|
validate_payload["features"] = _resolve_features(raw_features, level)
|
||||||
data["features"] = _resolve_features(raw_features, level) # type: ignore[assignment]
|
|
||||||
|
|
||||||
# spells stays as list[str] — Pydantic validates it directly
|
# spells stays as list[str] — Pydantic validates it directly
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return Combatant.model_validate(data)
|
return Combatant.model_validate(validate_payload)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
raise CombatantLoadError(path, str(exc)) from exc
|
raise CombatantLoadError(path, str(exc)) from exc
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ class AbilityScores(_FrozenModel):
|
|||||||
wis_score: int
|
wis_score: int
|
||||||
cha_score: int
|
cha_score: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def str_mod(self) -> int:
|
||||||
|
"""Strength ability modifier."""
|
||||||
|
return (self.str_score - 10) // 2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dex_mod(self) -> int:
|
||||||
|
"""Dexterity ability modifier."""
|
||||||
|
return (self.dex_score - 10) // 2
|
||||||
|
|
||||||
|
|
||||||
class ACProfile(_FrozenModel):
|
class ACProfile(_FrozenModel):
|
||||||
total: int
|
total: int
|
||||||
|
|||||||
+113
-39
@@ -17,10 +17,10 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Literal, cast
|
from typing import TYPE_CHECKING, Literal, TypeGuard, get_args
|
||||||
|
|
||||||
from pf1e_simulator.dice import DiceExpr, parse_dice
|
from pf1e_simulator.dice import DiceExpr, parse_dice
|
||||||
from pf1e_simulator.effects import BonusType, StatModifier
|
from pf1e_simulator.effects import BonusType, StatModifier, StatTarget
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pf1e_simulator.loaders.foundry import Json
|
from pf1e_simulator.loaders.foundry import Json
|
||||||
@@ -57,7 +57,7 @@ class BuffEffect:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AreaEffect:
|
class AreaEffect:
|
||||||
"""Area of effect spell (burst only for Phase 0).
|
"""Area of effect spell (burst only for Phase 0).
|
||||||
|
|
||||||
- shape: only "burst" supported (circle around target point)
|
- shape: only "burst" supported (circle around target point)
|
||||||
- radius_ft: radius in feet (e.g., 20 for fireball)
|
- radius_ft: radius in feet (e.g., 20 for fireball)
|
||||||
- effects: the effects applied to each creature in the area
|
- effects: the effects applied to each creature in the area
|
||||||
@@ -71,7 +71,7 @@ class AreaEffect:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SummonEffect:
|
class SummonEffect:
|
||||||
"""Summoning spell effect.
|
"""Summoning spell effect.
|
||||||
|
|
||||||
- creature: path to the creature JSON file (e.g., "data/monsters/celestial_wolf.json")
|
- creature: path to the creature JSON file (e.g., "data/monsters/celestial_wolf.json")
|
||||||
- duration_rounds: how long the summoned creature stays
|
- duration_rounds: how long the summoned creature stays
|
||||||
"""
|
"""
|
||||||
@@ -106,64 +106,138 @@ def spell_range_ft(spell: SpellSpec, caster_level: int) -> int:
|
|||||||
return 400 + 40 * caster_level
|
return 400 + 40 * caster_level
|
||||||
|
|
||||||
|
|
||||||
def _parse_effect(data: dict[str, Json]) -> SpellEffect:
|
def _as_dict(value: Json) -> dict[str, Json]:
|
||||||
effect_type = str(data["type"])
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
msg = f"expected object, got {value!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_list(value: Json) -> list[Json]:
|
||||||
|
if isinstance(value, list):
|
||||||
|
return value
|
||||||
|
msg = f"expected list, got {value!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_str(value: Json) -> str:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
msg = f"expected string, got {value!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_int(value: Json) -> int:
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
if isinstance(value, float):
|
||||||
|
return int(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
return int(value)
|
||||||
|
msg = f"expected integer, got {value!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_optional_str(value: Json) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return _as_str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_spell_range(value: str) -> TypeGuard[SpellRange]:
|
||||||
|
return value in ("personal", "touch", "close", "medium", "long")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_save_type(value: str) -> TypeGuard[SaveType]:
|
||||||
|
return value in ("fort", "ref", "will", "none")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_stat_target(value: str) -> TypeGuard[StatTarget]:
|
||||||
|
return value in get_args(StatTarget)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_bonus_type(value: str) -> TypeGuard[BonusType]:
|
||||||
|
return value in get_args(BonusType)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_modifier(value: Json) -> StatModifier:
|
||||||
|
m = _as_dict(value)
|
||||||
|
target = _as_str(m["target"])
|
||||||
|
if not _is_stat_target(target):
|
||||||
|
msg = f"unknown stat target: {target!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
bonus = _as_str(m.get("bonus_type", ""))
|
||||||
|
if not _is_bonus_type(bonus):
|
||||||
|
msg = f"unknown bonus type: {bonus!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
return StatModifier(
|
||||||
|
target=target,
|
||||||
|
value=_as_int(m["value"]),
|
||||||
|
bonus_type=bonus,
|
||||||
|
condition=_as_optional_str(m.get("condition")),
|
||||||
|
weapon_filter=_as_optional_str(m.get("weapon_filter")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_effect(data: Json) -> SpellEffect:
|
||||||
|
effect = _as_dict(data)
|
||||||
|
effect_type = _as_str(effect["type"])
|
||||||
if effect_type == "damage":
|
if effect_type == "damage":
|
||||||
return DamageEffect(
|
return DamageEffect(
|
||||||
formula=parse_dice(str(data["formula"])),
|
formula=parse_dice(_as_str(effect["formula"])),
|
||||||
types=tuple(str(t) for t in data["types"]), # type: ignore[index]
|
types=tuple(_as_str(t) for t in _as_list(effect["types"])),
|
||||||
half_on_save=bool(data.get("half_on_save", True)),
|
half_on_save=bool(effect.get("half_on_save", True)),
|
||||||
)
|
)
|
||||||
if effect_type == "condition":
|
if effect_type == "condition":
|
||||||
return ConditionEffect(
|
return ConditionEffect(
|
||||||
condition_name=str(data["condition_name"]),
|
condition_name=_as_str(effect["condition_name"]),
|
||||||
duration_rounds=int(data["duration_rounds"]), # type: ignore[arg-type]
|
duration_rounds=_as_int(effect["duration_rounds"]),
|
||||||
save_negates=bool(data.get("save_negates", True)),
|
save_negates=bool(effect.get("save_negates", True)),
|
||||||
)
|
)
|
||||||
if effect_type == "heal":
|
if effect_type == "heal":
|
||||||
return HealEffect(
|
return HealEffect(
|
||||||
formula=parse_dice(str(data["formula"])),
|
formula=parse_dice(_as_str(effect["formula"])),
|
||||||
)
|
)
|
||||||
if effect_type == "buff":
|
if effect_type == "buff":
|
||||||
return BuffEffect(
|
return BuffEffect(
|
||||||
modifiers=tuple(
|
modifiers=tuple(_parse_modifier(m) for m in _as_list(effect["modifiers"])),
|
||||||
StatModifier(
|
duration_rounds=_as_int(effect.get("duration_rounds", 0)),
|
||||||
target=str(m["target"]), # type: ignore[index]
|
|
||||||
value=int(m["value"]), # type: ignore[arg-type]
|
|
||||||
bonus_type=cast("BonusType", str(m.get("bonus_type", ""))), # type: ignore[union-attr]
|
|
||||||
condition=m.get("condition"), # type: ignore[assignment]
|
|
||||||
weapon_filter=m.get("weapon_filter"), # type: ignore[assignment]
|
|
||||||
)
|
|
||||||
for m in cast("list[dict[str, Json]]", data["modifiers"])
|
|
||||||
),
|
|
||||||
duration_rounds=int(data.get("duration_rounds", 0)), # type: ignore[arg-type]
|
|
||||||
)
|
)
|
||||||
if effect_type == "area":
|
if effect_type == "area":
|
||||||
return AreaEffect(
|
return AreaEffect(
|
||||||
radius_ft=int(data.get("radius_ft", 20)), # type: ignore[arg-type]
|
radius_ft=_as_int(effect.get("radius_ft", 20)),
|
||||||
effects=tuple(_parse_effect(e) for e in data["effects"]), # type: ignore[index]
|
effects=tuple(_parse_effect(e) for e in _as_list(effect["effects"])),
|
||||||
save_for_half=bool(data.get("save_for_half", True)),
|
save_for_half=bool(effect.get("save_for_half", True)),
|
||||||
)
|
)
|
||||||
if effect_type == "summon":
|
if effect_type == "summon":
|
||||||
return SummonEffect(
|
return SummonEffect(
|
||||||
creature=str(data["creature"]), # type: ignore[index]
|
creature=_as_str(effect["creature"]),
|
||||||
duration_rounds=int(data["duration_rounds"]), # type: ignore[arg-type]
|
duration_rounds=_as_int(effect["duration_rounds"]),
|
||||||
)
|
)
|
||||||
raise ValueError(f"Unknown effect type: {effect_type}")
|
msg = f"Unknown effect type: {effect_type}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
def load_spell(path: str | Path) -> SpellSpec:
|
def load_spell(path: str | Path) -> SpellSpec:
|
||||||
data: dict[str, Json] = json.loads(Path(path).read_text())
|
data = _as_dict(json.loads(Path(path).read_text()))
|
||||||
effects = tuple(_parse_effect(e) for e in data.get("effects", [])) # type: ignore[arg-type]
|
effects = tuple(_parse_effect(e) for e in _as_list(data.get("effects", [])))
|
||||||
|
range_raw = _as_str(data.get("range", "medium"))
|
||||||
|
if not _is_spell_range(range_raw):
|
||||||
|
msg = f"unknown spell range: {range_raw!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
save_raw = _as_str(data.get("save", "none"))
|
||||||
|
if not _is_save_type(save_raw):
|
||||||
|
msg = f"unknown save type: {save_raw!r}"
|
||||||
|
raise ValueError(msg)
|
||||||
return SpellSpec(
|
return SpellSpec(
|
||||||
name=str(data["name"]),
|
name=_as_str(data["name"]),
|
||||||
level=int(data["level"]), # type: ignore[arg-type]
|
level=_as_int(data["level"]),
|
||||||
school=str(data["school"]),
|
school=_as_str(data["school"]),
|
||||||
range=str(data.get("range", "medium")), # type: ignore[assignment]
|
range=range_raw,
|
||||||
save=str(data.get("save", "none")), # type: ignore[assignment]
|
save=save_raw,
|
||||||
base_dc=int(data.get("base_dc", 10)), # type: ignore[arg-type]
|
base_dc=_as_int(data.get("base_dc", 10)),
|
||||||
effects=effects,
|
effects=effects,
|
||||||
description=str(data.get("description", "")),
|
description=_as_str(data.get("description", "")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pf1e_simulator.abilities import (
|
from pf1e_simulator.abilities import (
|
||||||
ALERTNESS,
|
ALERTNESS,
|
||||||
DODGE,
|
DODGE,
|
||||||
@@ -9,6 +11,7 @@ from pf1e_simulator.abilities import (
|
|||||||
IRON_WILL,
|
IRON_WILL,
|
||||||
LIGHTNING_REFLEXES,
|
LIGHTNING_REFLEXES,
|
||||||
POINT_BLANK_SHOT,
|
POINT_BLANK_SHOT,
|
||||||
|
AbilitySpec,
|
||||||
weapon_focus,
|
weapon_focus,
|
||||||
)
|
)
|
||||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||||
@@ -30,16 +33,16 @@ from pf1e_simulator.rng import ScriptedRng
|
|||||||
def _make_hero(
|
def _make_hero(
|
||||||
cid: str = "hero",
|
cid: str = "hero",
|
||||||
*,
|
*,
|
||||||
features: list[object] | None = None,
|
features: list[AbilitySpec] | None = None,
|
||||||
weapon_name: str = "sword",
|
weapon_name: str = "sword",
|
||||||
weapon_kind: str = "melee",
|
weapon_kind: Literal["melee", "ranged", "touch"] = "melee",
|
||||||
attack_bonus: int = 5,
|
attack_bonus: int = 5,
|
||||||
damage_bonus: int = 2,
|
damage_bonus: int = 2,
|
||||||
) -> Combatant:
|
) -> Combatant:
|
||||||
attack = AttackSpec(
|
attack = AttackSpec(
|
||||||
id=f"{cid}-w",
|
id=f"{cid}-w",
|
||||||
name=weapon_name,
|
name=weapon_name,
|
||||||
kind=weapon_kind, # type: ignore[arg-type]
|
kind=weapon_kind,
|
||||||
attack_bonus=attack_bonus,
|
attack_bonus=attack_bonus,
|
||||||
damage=[DamageComponent(formula=parse_dice("1d8"), types=["slashing"])],
|
damage=[DamageComponent(formula=parse_dice("1d8"), types=["slashing"])],
|
||||||
damage_bonus=damage_bonus,
|
damage_bonus=damage_bonus,
|
||||||
@@ -61,7 +64,7 @@ def _make_hero(
|
|||||||
speed_land_ft=30,
|
speed_land_ft=30,
|
||||||
saves=Saves(fort=5, ref=4, will=3),
|
saves=Saves(fort=5, ref=4, will=3),
|
||||||
attacks=[attack],
|
attacks=[attack],
|
||||||
features=features or [], # type: ignore[arg-type]
|
features=features or [],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from typing import TYPE_CHECKING
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pf1e_simulator.loaders.foundry import Json
|
||||||
|
|
||||||
from pf1e_simulator.abilities import (
|
from pf1e_simulator.abilities import (
|
||||||
DODGE,
|
DODGE,
|
||||||
IRON_WILL,
|
IRON_WILL,
|
||||||
@@ -79,7 +81,8 @@ class TestResolveFeatures:
|
|||||||
assert result[1] is DODGE
|
assert result[1] is DODGE
|
||||||
|
|
||||||
def test_filters_non_strings(self) -> None:
|
def test_filters_non_strings(self) -> None:
|
||||||
result = _resolve_features(["Iron Will", 42, None, True], 1) # type: ignore[list-item]
|
raw: list[Json] = ["Iron Will", 42, None, True]
|
||||||
|
result = _resolve_features(raw, 1)
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
|
|
||||||
def test_empty_list(self) -> None:
|
def test_empty_list(self) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pf1e_simulator.abilities import POWER_ATTACK
|
from pf1e_simulator.abilities import POWER_ATTACK, AbilitySpec
|
||||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||||
from pf1e_simulator.dice import parse_dice
|
from pf1e_simulator.dice import parse_dice
|
||||||
from pf1e_simulator.grid import Grid
|
from pf1e_simulator.grid import Grid
|
||||||
@@ -24,7 +24,7 @@ def _make_fighter(
|
|||||||
bab: int = 4,
|
bab: int = 4,
|
||||||
attack_bonus: int = 5,
|
attack_bonus: int = 5,
|
||||||
damage_bonus: int = 2,
|
damage_bonus: int = 2,
|
||||||
features: list[object] | None = None,
|
features: list[AbilitySpec] | None = None,
|
||||||
) -> Combatant:
|
) -> Combatant:
|
||||||
attack = AttackSpec(
|
attack = AttackSpec(
|
||||||
id=f"{cid}-w",
|
id=f"{cid}-w",
|
||||||
@@ -50,7 +50,7 @@ def _make_fighter(
|
|||||||
speed_land_ft=30,
|
speed_land_ft=30,
|
||||||
saves=Saves(fort=6, ref=2, will=2),
|
saves=Saves(fort=6, ref=2, will=2),
|
||||||
attacks=[attack],
|
attacks=[attack],
|
||||||
features=features or [], # type: ignore[arg-type]
|
features=features or [],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -407,7 +407,8 @@ class TestSpellDamageWithDr:
|
|||||||
"dr": DamageReduction(amount=5, bypass=frozenset()),
|
"dr": DamageReduction(amount=5, bypass=frozenset()),
|
||||||
})
|
})
|
||||||
# 2 save rolls (2 targets in area) + 6 damage dice
|
# 2 save rolls (2 targets in area) + 6 damage dice
|
||||||
engine, c, t = _make_engine(caster, target_with_dr, [2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])
|
queue = [2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
|
||||||
|
engine, c, t = _make_engine(caster, target_with_dr, queue)
|
||||||
hp_before = t.hp
|
hp_before = t.hp
|
||||||
engine._cast_spell(c, t, "Fireball")
|
engine._cast_spell(c, t, "Fireball")
|
||||||
# Save fails (2), 6d6 rolls all 3 = 18 damage, DR 5/- -> 13 damage
|
# Save fails (2), 6d6 rolls all 3 = 18 damage, DR 5/- -> 13 damage
|
||||||
|
|||||||
Reference in New Issue
Block a user