diff --git a/README.md b/README.md index 963ab9c..3c202e1 100644 --- a/README.md +++ b/README.md @@ -647,9 +647,16 @@ Règles modélisées : résout `count` balayages indépendants dans la même action ; les balayages restants sont perdus si la cible tombe (inconsciente ou morte) en cours de rafale. Attaque à outrance (action à round complet) : à BAB +6/+11/+16, des - attaques supplémentaires à -5/-10/-15 ; les armes naturelles (`count` > 1) - ne gagnent pas d'itératifs. Plusieurs armes naturelles en une seule attaque - à outrance ne sont pas modélisées. + attaques supplémentaires à -5/-10/-15 pour les armes manufacturées. Les + armes naturelles (`natural: true` dans le JSON — morsures, griffes, sabots, + talons…) ne gagnent **jamais** d'itératifs : une attaque à outrance avec une + arme naturelle résout **un seul** balayage quel que soit le BAB (CRB + « Natural Attacks » : « You do not receive additional natural attacks for a + high base attack bonus »). Une attaque naturelle marquée `secondary: true` + (les attaques secondaires du bestiaire) frappe à **BAB −5** ; le don + Multiattack réduit cette pénalité à **−2** (CRB « Natural Attacks » / + « Multiattack »). Plusieurs armes naturelles en une seule attaque à outrance + ne sont pas modélisées. Choix de l'arme : `Action(kind="attack", weapon_id=...)` et `Action(kind="full_attack", weapon_id=...)` frappent avec l'arme désignée (l'arc plutôt que l'épée, l'arme principale plutôt que la secondaire) au diff --git a/data/monsters/celestial_wolf.json b/data/monsters/celestial_wolf.json index cdc75d4..799991b 100644 --- a/data/monsters/celestial_wolf.json +++ b/data/monsters/celestial_wolf.json @@ -1,53 +1,56 @@ { - "name": "Celestial Wolf", - "level": 2, - "size": "Medium", - "abilities": { - "str_score": 13, - "dex_score": 15, - "con_score": 12, - "int_score": 2, - "wis_score": 12, - "cha_score": 6 - }, - "hp_max": 15, - "ac": { - "total": 14, - "touch": 13, - "flat_footed": 12 - }, - "bab": 1, - "initiative_mod": 2, - "speed_land_ft": 50, - "saves": { - "fort": 4, - "ref": 5, - "will": 2 - }, - "attacks": [ + "name": "Celestial Wolf", + "level": 2, + "size": "Medium", + "abilities": { + "str_score": 13, + "dex_score": 15, + "con_score": 12, + "int_score": 2, + "wis_score": 12, + "cha_score": 6 + }, + "hp_max": 15, + "ac": { + "total": 14, + "touch": 13, + "flat_footed": 12 + }, + "bab": 1, + "initiative_mod": 2, + "speed_land_ft": 50, + "saves": { + "fort": 4, + "ref": 5, + "will": 2 + }, + "attacks": [ + { + "id": "celestial_wolf:bite", + "name": "Bite", + "kind": "melee", + "attack_bonus": 3, + "damage": [ { - "id": "celestial_wolf:bite", - "name": "Bite", - "kind": "melee", - "attack_bonus": 3, - "damage": [ - { - "formula": "1d6+1", - "types": ["piercing"] - } - ], - "damage_bonus": 1, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 + "formula": "1d6+1", + "types": [ + "piercing" + ] } - ], - "source": "summon monster II", - "features": [], - "spells": [], - "casting_ability": "wis", - "resources": {}, - "metamagic_feats": [], - "skills": {} + ], + "damage_bonus": 1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + } + ], + "source": "summon monster II", + "features": [], + "spells": [], + "casting_ability": "wis", + "resources": {}, + "metamagic_feats": [], + "skills": {} } diff --git a/data/monsters/giant_belostomatid.json b/data/monsters/giant_belostomatid.json index 7977505..74aee93 100644 --- a/data/monsters/giant_belostomatid.json +++ b/data/monsters/giant_belostomatid.json @@ -1,46 +1,70 @@ { - "name": "Giant Belostomatid", - "level": 3, - "size": "Large", - "cr": "3", - "xp": 800, - "source": "Bestiary > Giant Belostomatid", - "notes": "Digestive enzyme (DC 15 Fort or paralyzed 1d4 rounds + 1d2 Con damage/round) and grab on claws not modeled in Phase 0; Int - (mindless) modeled as 0; immune to mind-affecting not modeled; swim 40 ft / fly 20 ft (poor) not modeled, land speed only.", - "abilities": { - "str_score": 15, - "dex_score": 13, - "con_score": 16, - "int_score": 0, - "wis_score": 11, - "cha_score": 2 - }, - "hp_max": 30, - "ac": { "total": 15, "touch": 10, "flat_footed": 14 }, - "bab": 3, - "initiative_mod": 1, - "speed_land_ft": 20, - "saves": { "fort": 7, "ref": 2, "will": 1 }, - "attacks": [ + "name": "Giant Belostomatid", + "level": 3, + "size": "Large", + "cr": "3", + "xp": 800, + "source": "Bestiary > Giant Belostomatid", + "notes": "Digestive enzyme (DC 15 Fort or paralyzed 1d4 rounds + 1d2 Con damage/round) and grab on claws not modeled in Phase 0; Int - (mindless) modeled as 0; immune to mind-affecting not modeled; swim 40 ft / fly 20 ft (poor) not modeled, land speed only.", + "abilities": { + "str_score": 15, + "dex_score": 13, + "con_score": 16, + "int_score": 0, + "wis_score": 11, + "cha_score": 2 + }, + "hp_max": 30, + "ac": { + "total": 15, + "touch": 10, + "flat_footed": 14 + }, + "bab": 3, + "initiative_mod": 1, + "speed_land_ft": 20, + "saves": { + "fort": 7, + "ref": 2, + "will": 1 + }, + "attacks": [ + { + "name": "bite", + "kind": "melee", + "attack_bonus": 4, + "damage": [ { - "name": "bite", - "kind": "melee", - "attack_bonus": 4, - "damage": [{ "formula": "1d6", "types": ["piercing"] }], - "damage_bonus": 2, - "crit_mult": 2, - "reach_ft": 10 - }, - { - "name": "claw", - "kind": "melee", - "attack_bonus": 4, - "damage": [{ "formula": "1d4", "types": ["slashing"] }], - "damage_bonus": 2, - "crit_mult": 2, - "count": 2, - "reach_ft": 10 + "formula": "1d6", + "types": [ + "piercing" + ] } - ], - "cmb": 6, - "cmd": 17 -} \ No newline at end of file + ], + "damage_bonus": 2, + "crit_mult": 2, + "reach_ft": 10, + "natural": true + }, + { + "name": "claw", + "kind": "melee", + "attack_bonus": 4, + "damage": [ + { + "formula": "1d4", + "types": [ + "slashing" + ] + } + ], + "damage_bonus": 2, + "crit_mult": 2, + "count": 2, + "reach_ft": 10, + "natural": true + } + ], + "cmb": 6, + "cmd": 17 +} diff --git a/data/pcs/harvie.json b/data/pcs/harvie.json index d7b1eff..be329f0 100644 --- a/data/pcs/harvie.json +++ b/data/pcs/harvie.json @@ -1,76 +1,82 @@ { - "name": "Harvie", - "level": 2, - "size": "Small", - "abilities": { - "str_score": 10, - "dex_score": 15, - "con_score": 12, - "int_score": 2, - "wis_score": 12, - "cha_score": 6 - }, - "hp_max": 15, - "ac": { - "total": 14, - "touch": 13, - "flat_footed": 12 - }, - "bab": 1, - "initiative_mod": 2, - "speed_land_ft": 10, - "speed_fly_ft": 80, - "saves": { - "fort": 4, - "ref": 5, - "will": 2 - }, - "attacks": [ + "name": "Harvie", + "level": 2, + "size": "Small", + "abilities": { + "str_score": 10, + "dex_score": 15, + "con_score": 12, + "int_score": 2, + "wis_score": 12, + "cha_score": 6 + }, + "hp_max": 15, + "ac": { + "total": 14, + "touch": 13, + "flat_footed": 12 + }, + "bab": 1, + "initiative_mod": 2, + "speed_land_ft": 10, + "speed_fly_ft": 80, + "saves": { + "fort": 4, + "ref": 5, + "will": 2 + }, + "attacks": [ + { + "id": "harvie:bite", + "name": "Bite", + "kind": "melee", + "attack_bonus": 3, + "damage": [ { - "id": "harvie:bite", - "name": "Bite", - "kind": "melee", - "attack_bonus": 3, - "damage": [ - { - "formula": "1d4", - "types": ["piercing"] - } - ], - "damage_bonus": 0, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 - }, - { - "id": "harvie:claw", - "name": "Claw", - "kind": "melee", - "attack_bonus": 3, - "damage": [ - { - "formula": "1d3", - "types": ["slashing"] - } - ], - "damage_bonus": 0, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 + "formula": "1d4", + "types": [ + "piercing" + ] } - ], - "source": "fiches_personnages/harvie_sheet.json", - "features": [ - "Friendly Face", - "Fey-touched", - "Animal (race)" - ], - "spells": [], - "casting_ability": "wis", - "spell_slots": {}, - "resources": {}, - "metamagic_feats": [], - "skills": {} + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + }, + { + "id": "harvie:claw", + "name": "Claw", + "kind": "melee", + "attack_bonus": 3, + "damage": [ + { + "formula": "1d3", + "types": [ + "slashing" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + } + ], + "source": "fiches_personnages/harvie_sheet.json", + "features": [ + "Friendly Face", + "Fey-touched", + "Animal (race)" + ], + "spells": [], + "casting_ability": "wis", + "spell_slots": {}, + "resources": {}, + "metamagic_feats": [], + "skills": {} } diff --git a/data/pcs/ierlieth.json b/data/pcs/ierlieth.json index 5ad6e13..f0cbff2 100644 --- a/data/pcs/ierlieth.json +++ b/data/pcs/ierlieth.json @@ -1,59 +1,62 @@ { - "name": "Ierlieth", - "level": 4, - "size": "Tiny", - "abilities": { - "str_score": 8, - "dex_score": 17, - "con_score": 11, - "int_score": 10, - "wis_score": 10, - "cha_score": 9 - }, - "hp_max": 29, - "ac": { - "total": 17, - "touch": 15, - "flat_footed": 14 - }, - "bab": 3, - "initiative_mod": 3, - "speed_land_ft": 15, - "speed_fly_ft": 40, - "saves": { - "fort": 3, - "ref": 6, - "will": 3 - }, - "attacks": [ + "name": "Ierlieth", + "level": 4, + "size": "Tiny", + "abilities": { + "str_score": 8, + "dex_score": 17, + "con_score": 11, + "int_score": 10, + "wis_score": 10, + "cha_score": 9 + }, + "hp_max": 29, + "ac": { + "total": 17, + "touch": 15, + "flat_footed": 14 + }, + "bab": 3, + "initiative_mod": 3, + "speed_land_ft": 15, + "speed_fly_ft": 40, + "saves": { + "fort": 3, + "ref": 6, + "will": 3 + }, + "attacks": [ + { + "id": "ierlieth:bite", + "name": "Bite", + "kind": "melee", + "attack_bonus": 4, + "damage": [ { - "id": "ierlieth:bite", - "name": "Bite", - "kind": "melee", - "attack_bonus": 4, - "damage": [ - { - "formula": "1d4", - "types": ["piercing"] - } - ], - "damage_bonus": 1, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 + "formula": "1d4", + "types": [ + "piercing" + ] } - ], - "source": "fiches_personnages/ierlieth_sheet.json", - "features": [ - "Drake (race)", - "Toughness", - "Power Attack" - ], - "spells": [], - "casting_ability": "wis", - "spell_slots": {}, - "resources": {}, - "metamagic_feats": [], - "skills": {} + ], + "damage_bonus": 1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + } + ], + "source": "fiches_personnages/ierlieth_sheet.json", + "features": [ + "Drake (race)", + "Toughness", + "Power Attack" + ], + "spells": [], + "casting_ability": "wis", + "spell_slots": {}, + "resources": {}, + "metamagic_feats": [], + "skills": {} } diff --git a/data/pcs/misty.json b/data/pcs/misty.json index d8fdf01..3ba6a34 100644 --- a/data/pcs/misty.json +++ b/data/pcs/misty.json @@ -1,166 +1,168 @@ { - "name": "Misty", - "level": 4, - "size": "Medium", - "abilities": { - "str_score": 12, - "dex_score": 19, - "con_score": 14, - "int_score": 16, - "wis_score": 7, - "cha_score": 18 - }, - "hp_max": 31, - "ac": { - "total": 20, - "touch": 14, - "flat_footed": 16 - }, - "bab": 3, - "initiative_mod": 4, - "speed_land_ft": 30, - "saves": { - "fort": 7, - "ref": 9, - "will": 3 - }, - "casting_ability": "cha", - "spell_slots": { - "1": 4, - "2": 3 - }, - "resources": { - "bardic_rounds": 12 - }, - "attacks": [ + "name": "Misty", + "level": 4, + "size": "Medium", + "abilities": { + "str_score": 12, + "dex_score": 19, + "con_score": 14, + "int_score": 16, + "wis_score": 7, + "cha_score": 18 + }, + "hp_max": 31, + "ac": { + "total": 20, + "touch": 14, + "flat_footed": 16 + }, + "bab": 3, + "initiative_mod": 4, + "speed_land_ft": 30, + "saves": { + "fort": 7, + "ref": 9, + "will": 3 + }, + "casting_ability": "cha", + "spell_slots": { + "1": 4, + "2": 3 + }, + "resources": { + "bardic_rounds": 12 + }, + "attacks": [ + { + "name": "MWK Rapier", + "kind": "melee", + "attack_bonus": 8, + "damage": [ { - "name": "MWK Rapier", - "kind": "melee", - "attack_bonus": 8, - "damage": [ - { - "formula": "1d6", - "types": [ - "piercing" - ] - } - ], - "damage_bonus": 1, - "crit_range": 18, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 - }, - { - "name": "Dagger", - "kind": "melee", - "attack_bonus": 7, - "damage": [ - { - "formula": "1d4", - "types": [ - "piercing" - ] - } - ], - "damage_bonus": 1, - "crit_range": 19, - "crit_mult": 2, - "reach_ft": 5, - "count": 1, - "range_increment_ft": 10 - }, - { - "name": "Talons (attaque naturelle)", - "kind": "melee", - "attack_bonus": 4, - "damage": [ - { - "formula": "1d4", - "types": [ - "slashing" - ] - } - ], - "damage_bonus": 1, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 - }, - { - "name": "Bite (attaque naturelle)", - "kind": "melee", - "attack_bonus": 4, - "damage": [ - { - "formula": "1d4", - "types": [ - "piercing" - ] - } - ], - "damage_bonus": 1, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 - }, - { - "name": "Unarmed Strike", - "kind": "melee", - "attack_bonus": 4, - "damage": [ - { - "formula": "1d3", - "types": [ - "bludgeoning" - ] - } - ], - "damage_bonus": 1, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 + "formula": "1d6", + "types": [ + "piercing" + ] } - ], - "source": "fiches_personnages/misty_sheet.json", - "features": [ - "Harmonic Spell", - "Weapon Finesse", - "Axe to Grind (combat)", - "Spell Vulnerability (divination; drawback)", - "Born Damned (race: tiefling)", - "Gifted Adept (dispel magic; magic)", - "Prehensile Tail (tiefling daemon-spawn)", - "Scaled Skin (electricity) (tiefling daemon-spawn)", - "Maw or Claw (Bite) (tiefling daemon-spawn)" - ], - "spells": [ - "Prestidigitation", - "Oath of Anonymity", - "Detect Magic", - "Read Magic", - "Disrupt Undead", - "Ray of Frost", - "Summon Instrument", - "Mage Hand", - "Open/Close", - "Arcane Mark", - "Acid Splash", - "Grasp", - "Shield", - "Cure Light Wounds", - "Color Spray", - "Saving Finale", - "Vanish", - "Shocking Grasp", - "Burning Hands", - "Alarm", - "Scorching Ray", - "Bladed Dash", - "Mirror Image", - "Heroism" - ] + ], + "damage_bonus": 1, + "crit_range": 18, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + }, + { + "name": "Dagger", + "kind": "melee", + "attack_bonus": 7, + "damage": [ + { + "formula": "1d4", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 1, + "crit_range": 19, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 10 + }, + { + "name": "Talons (attaque naturelle)", + "kind": "melee", + "attack_bonus": 4, + "damage": [ + { + "formula": "1d4", + "types": [ + "slashing" + ] + } + ], + "damage_bonus": 1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + }, + { + "name": "Bite (attaque naturelle)", + "kind": "melee", + "attack_bonus": 4, + "damage": [ + { + "formula": "1d4", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + }, + { + "name": "Unarmed Strike", + "kind": "melee", + "attack_bonus": 4, + "damage": [ + { + "formula": "1d3", + "types": [ + "bludgeoning" + ] + } + ], + "damage_bonus": 1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + } + ], + "source": "fiches_personnages/misty_sheet.json", + "features": [ + "Harmonic Spell", + "Weapon Finesse", + "Axe to Grind (combat)", + "Spell Vulnerability (divination; drawback)", + "Born Damned (race: tiefling)", + "Gifted Adept (dispel magic; magic)", + "Prehensile Tail (tiefling daemon-spawn)", + "Scaled Skin (electricity) (tiefling daemon-spawn)", + "Maw or Claw (Bite) (tiefling daemon-spawn)" + ], + "spells": [ + "Prestidigitation", + "Oath of Anonymity", + "Detect Magic", + "Read Magic", + "Disrupt Undead", + "Ray of Frost", + "Summon Instrument", + "Mage Hand", + "Open/Close", + "Arcane Mark", + "Acid Splash", + "Grasp", + "Shield", + "Cure Light Wounds", + "Color Spray", + "Saving Finale", + "Vanish", + "Shocking Grasp", + "Burning Hands", + "Alarm", + "Scorching Ray", + "Bladed Dash", + "Mirror Image", + "Heroism" + ] } diff --git a/data/pcs/oni.json b/data/pcs/oni.json index dd3c4fe..e6b18c6 100644 --- a/data/pcs/oni.json +++ b/data/pcs/oni.json @@ -1,126 +1,127 @@ { - "name": "Oni Triumvir", - "level": 4, - "size": "Medium", - "abilities": { - "str_score": 12, - "dex_score": 14, - "con_score": 14, - "int_score": 12, - "wis_score": 18, - "cha_score": 16 - }, - "hp_max": 36, - "ac": { - "total": 19, - "touch": 18, - "flat_footed": 17 - }, - "bab": 4, - "initiative_mod": 2, - "speed_land_ft": 30, - "saves": { - "fort": 7, - "ref": 7, - "will": 9 - }, - "casting_ability": "wis", - "spell_slots": { - "1": 4, - "2": 3 - }, - "resources": { - "channel_energy": 6 - }, - "attacks": [ + "name": "Oni Triumvir", + "level": 4, + "size": "Medium", + "abilities": { + "str_score": 12, + "dex_score": 14, + "con_score": 14, + "int_score": 12, + "wis_score": 18, + "cha_score": 16 + }, + "hp_max": 36, + "ac": { + "total": 19, + "touch": 18, + "flat_footed": 17 + }, + "bab": 4, + "initiative_mod": 2, + "speed_land_ft": 30, + "saves": { + "fort": 7, + "ref": 7, + "will": 9 + }, + "casting_ability": "wis", + "spell_slots": { + "1": 4, + "2": 3 + }, + "resources": { + "channel_energy": 6 + }, + "attacks": [ + { + "name": "Longbow", + "kind": "ranged", + "attack_bonus": 6, + "damage": [ { - "name": "Longbow", - "kind": "ranged", - "attack_bonus": 6, - "damage": [ - { - "formula": "1d8", - "types": [ - "piercing" - ] - } - ], - "damage_bonus": 0, - "crit_range": 20, - "crit_mult": 3, - "reach_ft": 5, - "count": 1, - "range_increment_ft": 100 - }, - { - "name": "Alchemist's Fire", - "kind": "touch", - "attack_bonus": 6, - "damage": [ - { - "formula": "1d6", - "types": [ - "fire" - ] - } - ], - "damage_bonus": 0, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1, - "range_increment_ft": 10 - }, - { - "name": "Attaques naturelles (Shifter Claws / formes fées)", - "kind": "melee", - "attack_bonus": 5, - "damage": [ - { - "formula": "1d6", - "types": [ - "slashing" - ] - } - ], - "damage_bonus": 1, - "crit_range": 20, - "crit_mult": 2, - "reach_ft": 5, - "count": 1 + "formula": "1d8", + "types": [ + "piercing" + ] } - ], - "source": "fiches_personnages/oni_sheet.json", - "notes": "Armes placeholder Foundry sans dégâts encodés omises : 'une Pioche' (non maîtrisée) et 'Weapon' (à remplacer par la vraie arme).", - "features": [ - "Summon Good Monster", - "Sacred Summons", - "Selective Channeling", - "Extremely Fashionable", - "Exalted of the Society (Cleric, Pathfinder Society)", - "Bitter", - "Ambitious" - ], - "spells": [ - "Guidance", - "Detect Magic", - "Stabilize", - "Read Magic", - "Detect Poison", - "Purify Food and Drink", - "Summon Monster I", - "Shield of Faith", - "Forbid Action", - "Cure Light Wounds", - "Hidden Diplomacy", - "Protection from Evil", - "Bless", - "Summon Monster II", - "Cure Moderate Wounds", - "Sound Burst", - "Suppress Charms and Compulsions", - "Delay Poison", - "Ironskin", - "Restoration, Lesser" - ] + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 3, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 100 + }, + { + "name": "Alchemist's Fire", + "kind": "touch", + "attack_bonus": 6, + "damage": [ + { + "formula": "1d6", + "types": [ + "fire" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 10 + }, + { + "name": "Attaques naturelles (Shifter Claws / formes fées)", + "kind": "melee", + "attack_bonus": 5, + "damage": [ + { + "formula": "1d6", + "types": [ + "slashing" + ] + } + ], + "damage_bonus": 1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "natural": true + } + ], + "source": "fiches_personnages/oni_sheet.json", + "notes": "Armes placeholder Foundry sans dégâts encodés omises : 'une Pioche' (non maîtrisée) et 'Weapon' (à remplacer par la vraie arme).", + "features": [ + "Summon Good Monster", + "Sacred Summons", + "Selective Channeling", + "Extremely Fashionable", + "Exalted of the Society (Cleric, Pathfinder Society)", + "Bitter", + "Ambitious" + ], + "spells": [ + "Guidance", + "Detect Magic", + "Stabilize", + "Read Magic", + "Detect Poison", + "Purify Food and Drink", + "Summon Monster I", + "Shield of Faith", + "Forbid Action", + "Cure Light Wounds", + "Hidden Diplomacy", + "Protection from Evil", + "Bless", + "Summon Monster II", + "Cure Moderate Wounds", + "Sound Burst", + "Suppress Charms and Compulsions", + "Delay Poison", + "Ironskin", + "Restoration, Lesser" + ] } diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 49501d6..97b01f6 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -6699,6 +6699,12 @@ class CombatEngine: target_live = self._stats[target.combatant.id] hp_before = target.hp override = bonus_override + if weapon.secondary: + # PF1e (CRB "Natural Attacks"): secondary natural attacks are at + # base attack bonus -5; the Multiattack feat reduces this to -2. + penalty = 2 if self._has_feat(attacker, "Multiattack") else 5 + base = weapon.attack_bonus if override is None else override + override = base - penalty if attack_penalty: base = weapon.attack_bonus if override is None else override override = base + attack_penalty @@ -6829,6 +6835,12 @@ class CombatEngine: if weapon.count > 1: self._attack(attacker, target, weapon, attack_penalty=attack_penalty) return + if weapon.natural: + # PF1e (CRB "Natural Attacks"): "You do not receive additional + # natural attacks for a high base attack bonus." One swing per + # natural weapon, at its own attack bonus. + self._attack(attacker, target, weapon, attack_penalty=attack_penalty) + return n = min( _MAX_ITERATIVES, 1 + max(0, (attacker.combatant.bab - 1) // _ITERATIVE_PENALTY), diff --git a/src/pf1e_simulator/models.py b/src/pf1e_simulator/models.py index 9333846..109e931 100644 --- a/src/pf1e_simulator/models.py +++ b/src/pf1e_simulator/models.py @@ -242,14 +242,18 @@ class AttackSpec(_FrozenModel): increment and against full AC beyond it. `two_handed` marks a weapon swung in two hands (two-handed weapon, one-handed weapon used two hands, or a primary natural with 1.5x Strength) — PF1e Power Attack rolls +50% damage - with such a weapon (CRB "Power Attack"). `reach_weapon` marks a weapon + with such a weapon (CRB "Power Attack"). `reach_weapon` marks a weapon with the reach weapon quality (CRB "Big and Little Creatures in Combat"): it doubles the wielder's NATURAL reach but cannot strike at the wielder's natural reach or less — the usable zone is a RING (dead zone = natural reach). Without the flag a melee attack follows the creature's natural reach, which is a DISK: a Large creature's natural attack (its JSON `reach_ft` IS its natural reach, 10 ft) still strikes and threatens - adjacent squares. + adjacent squares. `natural` marks a natural weapon (bite, claw, etc.): + PF1e (CRB "Natural Attacks") grants no BAB iteratives for natural + weapons. `secondary` marks a secondary natural attack (tail, wing, etc.): + the engine applies -5 to the attack bonus (reduced to -2 with the + Multiattack feat). """ id: str @@ -266,6 +270,8 @@ class AttackSpec(_FrozenModel): count: int = Field(default=1, ge=1) firearm: bool = False two_handed: bool = False + natural: bool = False + secondary: bool = False class DamageReduction(_FrozenModel): diff --git a/tests/test_natural_attacks.py b/tests/test_natural_attacks.py new file mode 100644 index 0000000..20e33c2 --- /dev/null +++ b/tests/test_natural_attacks.py @@ -0,0 +1,167 @@ +"""Natural weapon attack rules (CRB "Natural Attacks"). + +Given an attacker with natural weapons and high BAB +When a full attack is resolved +Then natural weapons make exactly one swing each (no BAB iteratives) + +Given a secondary natural attack +When a swing is resolved +Then the attack bonus is reduced by 5 + +Given a secondary natural attack and the attacker has the Multiattack feat +When a swing is resolved +Then the attack bonus is reduced by 2 (instead of 5) +""" + +from __future__ import annotations + +from pf1e_simulator.combat import CombatantState, CombatEngine +from pf1e_simulator.dice import parse_dice +from pf1e_simulator.grid import Grid +from pf1e_simulator.map import MapSpec, TerrainType +from pf1e_simulator.models import ( + AbilityScores, + AbilitySpec, + ACProfile, + AttackSpec, + Combatant, + DamageComponent, + Saves, +) +from pf1e_simulator.rng import ScriptedRng + + +def _make_combatant( + cid: str, + *, + side: str, + bab: int = 1, + attack_bonus: int = 0, + natural: bool = False, + secondary: bool = False, + features: tuple[str, ...] = (), +) -> Combatant: + """Build a minimal Combatant with one attack.""" + attack = AttackSpec( + id=f"{cid}-w", + name="weapon", + kind="melee", + attack_bonus=attack_bonus, + damage=[DamageComponent(formula=parse_dice("1d6"), types=["slashing"])], + natural=natural, + secondary=secondary, + ) + return Combatant( + id=cid, + name=cid, + level=1, + size="Medium", + bab=bab, + abilities=AbilityScores( + str_score=10, dex_score=10, con_score=10, + int_score=10, wis_score=10, cha_score=10, + ), + hp_max=20, + ac=ACProfile(total=15, touch=10, flat_footed=10), + initiative_mod=0, + speed_land_ft=30, + saves=Saves(fort=0, ref=0, will=0), + attacks=[attack], + features=[AbilitySpec(name=f, category="feat") for f in features], + ) + + +def _make_grid() -> Grid: + """8x8 floor grid.""" + legend = {".": TerrainType(type="floor", move_cost=1)} + spec = MapSpec(name="test", terrain=tuple(["." * 8] * 8), legend=legend) + return Grid.from_spec(spec) + + +def _make_engine( + queue: list[int], + attacker: Combatant, + target: Combatant, + attacker_pos: tuple[int, int] = (1, 1), + target_pos: tuple[int, int] = (1, 2), +) -> tuple[CombatEngine, CombatantState, CombatantState]: + """Create engine with two adjacent combatants and return (engine, atk_state, tgt_state).""" + a = CombatantState(combatant=attacker, side="players", pos=attacker_pos, + hp=attacker.hp_max) + t = CombatantState(combatant=target, side="monsters", pos=target_pos, + hp=target.hp_max) + engine = CombatEngine(ScriptedRng(queue), _make_grid(), [a, t]) + return engine, a, t + + +def _swing_lines(engine: CombatEngine, target_id: str) -> list[str]: + """Transcript lines that resolved a swing against the target.""" + return [line for line in engine._transcript if f"vs {target_id}" in line] + + +# BAB iteratives + + +def test_natural_weapon_no_bab_iteratives() -> None: + """Natural weapon with BAB 16 produces exactly 1 swing, not 4. + + PF1e (CRB "Natural Attacks"): you do not receive additional natural + attacks for a high base attack bonus. + """ + atk = _make_combatant("a1", side="players", bab=16, attack_bonus=18, natural=True) + tgt = _make_combatant("t1", side="monsters", bab=1) + # RNG: [d20=15] attack roll; on hit -> damage dice (1d6=4) + engine, a, t = _make_engine([15, 4], atk, tgt) + engine._full_attack(a, t) + swings = _swing_lines(engine, "t1") + assert len(swings) == 1, f"natural weapon should swing once, got {len(swings)}: {swings}" + + +def test_manufactured_weapon_gets_iteratives() -> None: + """Non-natural weapon with BAB 16 produces 4 iterative swings (16/11/6/1).""" + atk = _make_combatant("a2", side="players", bab=16, attack_bonus=18) + tgt = _make_combatant("t2", side="monsters", bab=1) + # RNG: 4 swings, each = 1 attack d20 (15) + 1 damage roll (3); target + # survives 4 x 3 = 12 damage (hp_max 20) so all iteratives resolve. + queue = [15, 3] * 4 + engine, a, t = _make_engine(queue, atk, tgt) + engine._full_attack(a, t) + swings = _swing_lines(engine, "t2") + assert len(swings) == 4, f"manufactured weapon should swing 4x, got {len(swings)}: {swings}" + + +def test_secondary_natural_attack_penalty() -> None: + """Secondary natural attack has -5 penalty on attack bonus. + + PF1e (CRB "Natural Attacks"): secondary natural attacks are at + base attack bonus -5. + """ + atk = _make_combatant("a3", side="players", bab=1, attack_bonus=10, + natural=True, secondary=True) + tgt = _make_combatant("t3", side="monsters", bab=1) + # RNG: [d20=15] attack -> 10 - 5 = 5; total = 15 + 5 = 20 >= AC 15 -> hit; damage 1d6=4 + engine, a, t = _make_engine([15, 4], atk, tgt) + engine._full_attack(a, t) + swings = _swing_lines(engine, "t3") + assert len(swings) == 1 + # base_bonus is 5 (10 - 5 secondary penalty) + assert "d20=15+5=20" in swings[0] + + +def test_multiattack_feat_reduces_secondary_penalty() -> None: + """Multiattack feat reduces secondary natural penalty from -5 to -2. + + PF1e (CRB "Multiattack"): secondary natural attacks are at + base attack bonus -2 (instead of -5). + """ + atk = _make_combatant("a4", side="players", bab=1, attack_bonus=10, + natural=True, secondary=True, + features=("Multiattack",)) + tgt = _make_combatant("t4", side="monsters", bab=1) + # RNG: [d20=15] attack -> 10 - 2 = 8; total = 15 + 8 = 23 >= AC 15 -> hit; damage 1d6=4 + engine, a, t = _make_engine([15, 4], atk, tgt) + engine._full_attack(a, t) + swings = _swing_lines(engine, "t4") + assert len(swings) == 1 + # base_bonus is 8 (10 - 2 Multiattack) + assert "d20=15+8=23" in swings[0]