diff --git a/README.md b/README.md index f1cd9b6..d97d885 100644 --- a/README.md +++ b/README.md @@ -1149,10 +1149,10 @@ Règles modélisées : cible en porte une, après la touche et la confirmation de critique, le moteur tire un d100 : si `d100 ≤ miss_chance`, l'attaque rate complètement (aucun dégât, pas de critique, deed non consommé — journalisé « concealment »). Une - cible avec concealment **total** (`miss_chance ≥ 50 %`) ne peut pas être la - victime d'une attaque sournoise ; une dissimulation partielle (flou 20 %) ne - bloque plus la sournoise (conformance A7 — CRB rogue « Sneak Attack » : - « cannot sneak attack while striking a creature with total concealment »). + cible avec **n'importe quelle dissimulation** (`miss_chance > 0` — flou 20 % + comme dissimulation totale 50 %) ne peut pas être la victime d'une attaque + sournoise (conformance A7 — SRD rogue « Sneak Attack » : + « A rogue cannot sneak attack while striking a creature with concealment »). Attaquant aveugle (V5a) : un attaquant portant `blinded` considère tous ses adversaires comme ayant un concealment total — le même jet unique de d100 est tiré avec `miss_chance = max(concealment de la cible, 50)` et journalisé @@ -1189,8 +1189,9 @@ Règles modélisées : attaque à distance à ≤ 30 ft). Les dés sont jetés avec le RNG du moteur et ajoutés APRÈS le multiplicateur de critique (la précision n'est jamais multipliée) et AVANT la RD et les résistances énergétiques ; jamais contre une - cible à dissimulation **totale** (`miss_chance ≥ 50 %` ; conformance A7 — une - dissimulation partielle comme le flou 20 % n'empêche plus la sournoise). + cible qui a une dissimulation, même partielle (`miss_chance > 0` ; + conformance A7 — SRD rogue : « A rogue cannot sneak attack while striking + a creature with concealment »). Conformance P2 : les morts-vivants (`undead`) et les constructeurs (`construct`) sont immunisés aux dégâts de précision — la cible ne peut pas être la victime d'une attaque sournoise ; les autres immunités RAW diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 9d1eff0..69afa2f 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -2999,14 +2999,15 @@ class CombatEngine: PF1e: precision damage when the target is flanked (melee only, melee touch included) or denied its Dex bonus to AC (melee, or ranged within - 30 ft); dice = (level + 1) // 2. Only TOTAL concealment (miss chance - >= 50%) denies the sneak attack; partial concealment (blurred 20%) - does not (CRB rogue "Sneak Attack": "cannot sneak attack while - striking a creature with total concealment"). A BLINDED attacker faces - total concealment against ALL its opponents (CRB "Conditions — - Blinded": "All opponents are considered to have total concealment - (50% miss chance) against the blinded character"), so it cannot sneak - attack either — same max(target, 50) convention as _resolve_hit_damage. + 30 ft); dice = (level + 1) // 2. ANY concealment (miss chance > 0) + denies the sneak attack (SRD rogue "Sneak Attack": "A rogue cannot + sneak attack while striking a creature with concealment" — the rogue + must see the target well enough to pick a vital spot). A BLINDED + attacker faces total concealment against ALL its opponents (CRB + "Conditions — Blinded": "All opponents are considered to have total + concealment (50% miss chance) against the blinded character"), so it + cannot sneak attack either — same max(target, 50) convention as + _resolve_hit_damage. PF1e (CRB): undead and constructs are immune to precision damage — ooze/plant immunity is not modeled (no such creature types exist in the data model). @@ -3015,7 +3016,7 @@ class CombatEngine: if self._has_condition_named(attacker, "blinded"): effective_concealment = max(concealment, _TOTAL_CONCEALMENT_PCT) if ( - effective_concealment >= _TOTAL_CONCEALMENT_PCT + effective_concealment > 0 or not self._has_feat(attacker, _SNEAK_ATTACK_FEATURE) ): return 0 diff --git a/tests/test_accuracy_rules.py b/tests/test_accuracy_rules.py index c13fe53..b67d51b 100644 --- a/tests/test_accuracy_rules.py +++ b/tests/test_accuracy_rules.py @@ -158,26 +158,27 @@ def test_total_concealment_denies_sneak_attack() -> None: assert not any("sneak" in line for line in eng._transcript) -def test_partial_concealment_still_allows_sneak_attack() -> None: - """(A7) a blurred (20%) target CAN be sneak attacked — CRB rogue text is - "cannot sneak attack while striking a creature with total concealment", - so partial concealment no longer denies the precision damage.""" +def test_partial_concealment_denies_sneak_attack() -> None: + """(A7) a blurred (20%) target CANNOT be sneak attacked — SRD rogue text + is "A rogue cannot sneak attack while striking a creature with + concealment": ANY concealment denies the precision damage (the rogue + must see the target well enough to pick out a vital spot).""" sneak = _combatant("atk", level=3, features=("Sneak Attack",)) eng = _engine( [sneak, _combatant("tgt")], [(0, 0), (0, 1)], ["players", "monsters"], - [15, 90, 6, 6, 4], + [15, 90, 6], ) a, t = eng._states _add_condition(t, "flat_footed") _add_condition(t, "blurred") result = eng.resolve_attack(a, t, a.combatant.attacks[0]) - # d20=15 hits, d100=90 > 20% lands; 2d6 sneak (6+6=12) + 1d6 weapon (4) - # = 16 precision-inclusive damage. - assert result.damage == 16 - assert t.hp == 84 - assert any("sneak attack" in line and "2d6" in line for line in eng._transcript) + # d20=15 hits, d100=90 > 20% lands; sneak denied by the 20% concealment, + # so only the 1d6 weapon die (6) lands. + assert result.damage == 6 + assert t.hp == 94 + assert not any("sneak" in line for line in eng._transcript) def test_concealment_miss_does_not_consume_deed() -> None: