fix(rules): natural weapons get no BAB iteratives, secondary attacks at -5
PF1e (CRB "Natural Attacks"): - Natural weapons (natural=true) get exactly one swing per full attack, regardless of BAB (no BAB iteratives for natural attacks). - Secondary natural attacks (secondary=true) attack at BAB -5; the Multiattack feat reduces this to -2. - Manifested in combat.py (_full_attack_weapon, _resolve_swing), models.py (AttackSpec.natural/secondary), 6 data JSONs, 4 new tests, and README documentation. 1461 passed, ruff clean, basedpyright 0 errors.
This commit is contained in:
@@ -647,9 +647,16 @@ Règles modélisées :
|
|||||||
résout `count` balayages indépendants dans la même action ; les balayages
|
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
|
restants sont perdus si la cible tombe (inconsciente ou morte) en cours de
|
||||||
rafale. Attaque à outrance (action à round complet) : à BAB +6/+11/+16, des
|
rafale. Attaque à outrance (action à round complet) : à BAB +6/+11/+16, des
|
||||||
attaques supplémentaires à -5/-10/-15 ; les armes naturelles (`count` > 1)
|
attaques supplémentaires à -5/-10/-15 pour les armes manufacturées. Les
|
||||||
ne gagnent pas d'itératifs. Plusieurs armes naturelles en une seule attaque
|
armes naturelles (`natural: true` dans le JSON — morsures, griffes, sabots,
|
||||||
à outrance ne sont pas modélisées.
|
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
|
Choix de l'arme : `Action(kind="attack", weapon_id=...)` et
|
||||||
`Action(kind="full_attack", weapon_id=...)` frappent avec l'arme désignée
|
`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
|
(l'arc plutôt que l'épée, l'arme principale plutôt que la secondaire) au
|
||||||
|
|||||||
@@ -1,53 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "Celestial Wolf",
|
"name": "Celestial Wolf",
|
||||||
"level": 2,
|
"level": 2,
|
||||||
"size": "Medium",
|
"size": "Medium",
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"str_score": 13,
|
"str_score": 13,
|
||||||
"dex_score": 15,
|
"dex_score": 15,
|
||||||
"con_score": 12,
|
"con_score": 12,
|
||||||
"int_score": 2,
|
"int_score": 2,
|
||||||
"wis_score": 12,
|
"wis_score": 12,
|
||||||
"cha_score": 6
|
"cha_score": 6
|
||||||
},
|
},
|
||||||
"hp_max": 15,
|
"hp_max": 15,
|
||||||
"ac": {
|
"ac": {
|
||||||
"total": 14,
|
"total": 14,
|
||||||
"touch": 13,
|
"touch": 13,
|
||||||
"flat_footed": 12
|
"flat_footed": 12
|
||||||
},
|
},
|
||||||
"bab": 1,
|
"bab": 1,
|
||||||
"initiative_mod": 2,
|
"initiative_mod": 2,
|
||||||
"speed_land_ft": 50,
|
"speed_land_ft": 50,
|
||||||
"saves": {
|
"saves": {
|
||||||
"fort": 4,
|
"fort": 4,
|
||||||
"ref": 5,
|
"ref": 5,
|
||||||
"will": 2
|
"will": 2
|
||||||
},
|
},
|
||||||
"attacks": [
|
"attacks": [
|
||||||
|
{
|
||||||
|
"id": "celestial_wolf:bite",
|
||||||
|
"name": "Bite",
|
||||||
|
"kind": "melee",
|
||||||
|
"attack_bonus": 3,
|
||||||
|
"damage": [
|
||||||
{
|
{
|
||||||
"id": "celestial_wolf:bite",
|
"formula": "1d6+1",
|
||||||
"name": "Bite",
|
"types": [
|
||||||
"kind": "melee",
|
"piercing"
|
||||||
"attack_bonus": 3,
|
]
|
||||||
"damage": [
|
|
||||||
{
|
|
||||||
"formula": "1d6+1",
|
|
||||||
"types": ["piercing"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"damage_bonus": 1,
|
|
||||||
"crit_range": 20,
|
|
||||||
"crit_mult": 2,
|
|
||||||
"reach_ft": 5,
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": "summon monster II",
|
"damage_bonus": 1,
|
||||||
"features": [],
|
"crit_range": 20,
|
||||||
"spells": [],
|
"crit_mult": 2,
|
||||||
"casting_ability": "wis",
|
"reach_ft": 5,
|
||||||
"resources": {},
|
"count": 1,
|
||||||
"metamagic_feats": [],
|
"natural": true
|
||||||
"skills": {}
|
}
|
||||||
|
],
|
||||||
|
"source": "summon monster II",
|
||||||
|
"features": [],
|
||||||
|
"spells": [],
|
||||||
|
"casting_ability": "wis",
|
||||||
|
"resources": {},
|
||||||
|
"metamagic_feats": [],
|
||||||
|
"skills": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,70 @@
|
|||||||
{
|
{
|
||||||
"name": "Giant Belostomatid",
|
"name": "Giant Belostomatid",
|
||||||
"level": 3,
|
"level": 3,
|
||||||
"size": "Large",
|
"size": "Large",
|
||||||
"cr": "3",
|
"cr": "3",
|
||||||
"xp": 800,
|
"xp": 800,
|
||||||
"source": "Bestiary > Giant Belostomatid",
|
"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.",
|
"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": {
|
"abilities": {
|
||||||
"str_score": 15,
|
"str_score": 15,
|
||||||
"dex_score": 13,
|
"dex_score": 13,
|
||||||
"con_score": 16,
|
"con_score": 16,
|
||||||
"int_score": 0,
|
"int_score": 0,
|
||||||
"wis_score": 11,
|
"wis_score": 11,
|
||||||
"cha_score": 2
|
"cha_score": 2
|
||||||
},
|
},
|
||||||
"hp_max": 30,
|
"hp_max": 30,
|
||||||
"ac": { "total": 15, "touch": 10, "flat_footed": 14 },
|
"ac": {
|
||||||
"bab": 3,
|
"total": 15,
|
||||||
"initiative_mod": 1,
|
"touch": 10,
|
||||||
"speed_land_ft": 20,
|
"flat_footed": 14
|
||||||
"saves": { "fort": 7, "ref": 2, "will": 1 },
|
},
|
||||||
"attacks": [
|
"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",
|
"formula": "1d6",
|
||||||
"kind": "melee",
|
"types": [
|
||||||
"attack_bonus": 4,
|
"piercing"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"cmb": 6,
|
"damage_bonus": 2,
|
||||||
"cmd": 17
|
"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
|
||||||
|
}
|
||||||
|
|||||||
+78
-72
@@ -1,76 +1,82 @@
|
|||||||
{
|
{
|
||||||
"name": "Harvie",
|
"name": "Harvie",
|
||||||
"level": 2,
|
"level": 2,
|
||||||
"size": "Small",
|
"size": "Small",
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"str_score": 10,
|
"str_score": 10,
|
||||||
"dex_score": 15,
|
"dex_score": 15,
|
||||||
"con_score": 12,
|
"con_score": 12,
|
||||||
"int_score": 2,
|
"int_score": 2,
|
||||||
"wis_score": 12,
|
"wis_score": 12,
|
||||||
"cha_score": 6
|
"cha_score": 6
|
||||||
},
|
},
|
||||||
"hp_max": 15,
|
"hp_max": 15,
|
||||||
"ac": {
|
"ac": {
|
||||||
"total": 14,
|
"total": 14,
|
||||||
"touch": 13,
|
"touch": 13,
|
||||||
"flat_footed": 12
|
"flat_footed": 12
|
||||||
},
|
},
|
||||||
"bab": 1,
|
"bab": 1,
|
||||||
"initiative_mod": 2,
|
"initiative_mod": 2,
|
||||||
"speed_land_ft": 10,
|
"speed_land_ft": 10,
|
||||||
"speed_fly_ft": 80,
|
"speed_fly_ft": 80,
|
||||||
"saves": {
|
"saves": {
|
||||||
"fort": 4,
|
"fort": 4,
|
||||||
"ref": 5,
|
"ref": 5,
|
||||||
"will": 2
|
"will": 2
|
||||||
},
|
},
|
||||||
"attacks": [
|
"attacks": [
|
||||||
|
{
|
||||||
|
"id": "harvie:bite",
|
||||||
|
"name": "Bite",
|
||||||
|
"kind": "melee",
|
||||||
|
"attack_bonus": 3,
|
||||||
|
"damage": [
|
||||||
{
|
{
|
||||||
"id": "harvie:bite",
|
"formula": "1d4",
|
||||||
"name": "Bite",
|
"types": [
|
||||||
"kind": "melee",
|
"piercing"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": "fiches_personnages/harvie_sheet.json",
|
"damage_bonus": 0,
|
||||||
"features": [
|
"crit_range": 20,
|
||||||
"Friendly Face",
|
"crit_mult": 2,
|
||||||
"Fey-touched",
|
"reach_ft": 5,
|
||||||
"Animal (race)"
|
"count": 1,
|
||||||
],
|
"natural": true
|
||||||
"spells": [],
|
},
|
||||||
"casting_ability": "wis",
|
{
|
||||||
"spell_slots": {},
|
"id": "harvie:claw",
|
||||||
"resources": {},
|
"name": "Claw",
|
||||||
"metamagic_feats": [],
|
"kind": "melee",
|
||||||
"skills": {}
|
"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": {}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-55
@@ -1,59 +1,62 @@
|
|||||||
{
|
{
|
||||||
"name": "Ierlieth",
|
"name": "Ierlieth",
|
||||||
"level": 4,
|
"level": 4,
|
||||||
"size": "Tiny",
|
"size": "Tiny",
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"str_score": 8,
|
"str_score": 8,
|
||||||
"dex_score": 17,
|
"dex_score": 17,
|
||||||
"con_score": 11,
|
"con_score": 11,
|
||||||
"int_score": 10,
|
"int_score": 10,
|
||||||
"wis_score": 10,
|
"wis_score": 10,
|
||||||
"cha_score": 9
|
"cha_score": 9
|
||||||
},
|
},
|
||||||
"hp_max": 29,
|
"hp_max": 29,
|
||||||
"ac": {
|
"ac": {
|
||||||
"total": 17,
|
"total": 17,
|
||||||
"touch": 15,
|
"touch": 15,
|
||||||
"flat_footed": 14
|
"flat_footed": 14
|
||||||
},
|
},
|
||||||
"bab": 3,
|
"bab": 3,
|
||||||
"initiative_mod": 3,
|
"initiative_mod": 3,
|
||||||
"speed_land_ft": 15,
|
"speed_land_ft": 15,
|
||||||
"speed_fly_ft": 40,
|
"speed_fly_ft": 40,
|
||||||
"saves": {
|
"saves": {
|
||||||
"fort": 3,
|
"fort": 3,
|
||||||
"ref": 6,
|
"ref": 6,
|
||||||
"will": 3
|
"will": 3
|
||||||
},
|
},
|
||||||
"attacks": [
|
"attacks": [
|
||||||
|
{
|
||||||
|
"id": "ierlieth:bite",
|
||||||
|
"name": "Bite",
|
||||||
|
"kind": "melee",
|
||||||
|
"attack_bonus": 4,
|
||||||
|
"damage": [
|
||||||
{
|
{
|
||||||
"id": "ierlieth:bite",
|
"formula": "1d4",
|
||||||
"name": "Bite",
|
"types": [
|
||||||
"kind": "melee",
|
"piercing"
|
||||||
"attack_bonus": 4,
|
]
|
||||||
"damage": [
|
|
||||||
{
|
|
||||||
"formula": "1d4",
|
|
||||||
"types": ["piercing"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"damage_bonus": 1,
|
|
||||||
"crit_range": 20,
|
|
||||||
"crit_mult": 2,
|
|
||||||
"reach_ft": 5,
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": "fiches_personnages/ierlieth_sheet.json",
|
"damage_bonus": 1,
|
||||||
"features": [
|
"crit_range": 20,
|
||||||
"Drake (race)",
|
"crit_mult": 2,
|
||||||
"Toughness",
|
"reach_ft": 5,
|
||||||
"Power Attack"
|
"count": 1,
|
||||||
],
|
"natural": true
|
||||||
"spells": [],
|
}
|
||||||
"casting_ability": "wis",
|
],
|
||||||
"spell_slots": {},
|
"source": "fiches_personnages/ierlieth_sheet.json",
|
||||||
"resources": {},
|
"features": [
|
||||||
"metamagic_feats": [],
|
"Drake (race)",
|
||||||
"skills": {}
|
"Toughness",
|
||||||
|
"Power Attack"
|
||||||
|
],
|
||||||
|
"spells": [],
|
||||||
|
"casting_ability": "wis",
|
||||||
|
"spell_slots": {},
|
||||||
|
"resources": {},
|
||||||
|
"metamagic_feats": [],
|
||||||
|
"skills": {}
|
||||||
}
|
}
|
||||||
|
|||||||
+164
-162
@@ -1,166 +1,168 @@
|
|||||||
{
|
{
|
||||||
"name": "Misty",
|
"name": "Misty",
|
||||||
"level": 4,
|
"level": 4,
|
||||||
"size": "Medium",
|
"size": "Medium",
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"str_score": 12,
|
"str_score": 12,
|
||||||
"dex_score": 19,
|
"dex_score": 19,
|
||||||
"con_score": 14,
|
"con_score": 14,
|
||||||
"int_score": 16,
|
"int_score": 16,
|
||||||
"wis_score": 7,
|
"wis_score": 7,
|
||||||
"cha_score": 18
|
"cha_score": 18
|
||||||
},
|
},
|
||||||
"hp_max": 31,
|
"hp_max": 31,
|
||||||
"ac": {
|
"ac": {
|
||||||
"total": 20,
|
"total": 20,
|
||||||
"touch": 14,
|
"touch": 14,
|
||||||
"flat_footed": 16
|
"flat_footed": 16
|
||||||
},
|
},
|
||||||
"bab": 3,
|
"bab": 3,
|
||||||
"initiative_mod": 4,
|
"initiative_mod": 4,
|
||||||
"speed_land_ft": 30,
|
"speed_land_ft": 30,
|
||||||
"saves": {
|
"saves": {
|
||||||
"fort": 7,
|
"fort": 7,
|
||||||
"ref": 9,
|
"ref": 9,
|
||||||
"will": 3
|
"will": 3
|
||||||
},
|
},
|
||||||
"casting_ability": "cha",
|
"casting_ability": "cha",
|
||||||
"spell_slots": {
|
"spell_slots": {
|
||||||
"1": 4,
|
"1": 4,
|
||||||
"2": 3
|
"2": 3
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"bardic_rounds": 12
|
"bardic_rounds": 12
|
||||||
},
|
},
|
||||||
"attacks": [
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "MWK Rapier",
|
||||||
|
"kind": "melee",
|
||||||
|
"attack_bonus": 8,
|
||||||
|
"damage": [
|
||||||
{
|
{
|
||||||
"name": "MWK Rapier",
|
"formula": "1d6",
|
||||||
"kind": "melee",
|
"types": [
|
||||||
"attack_bonus": 8,
|
"piercing"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": "fiches_personnages/misty_sheet.json",
|
"damage_bonus": 1,
|
||||||
"features": [
|
"crit_range": 18,
|
||||||
"Harmonic Spell",
|
"crit_mult": 2,
|
||||||
"Weapon Finesse",
|
"reach_ft": 5,
|
||||||
"Axe to Grind (combat)",
|
"count": 1
|
||||||
"Spell Vulnerability (divination; drawback)",
|
},
|
||||||
"Born Damned (race: tiefling)",
|
{
|
||||||
"Gifted Adept (dispel magic; magic)",
|
"name": "Dagger",
|
||||||
"Prehensile Tail (tiefling daemon-spawn)",
|
"kind": "melee",
|
||||||
"Scaled Skin (electricity) (tiefling daemon-spawn)",
|
"attack_bonus": 7,
|
||||||
"Maw or Claw (Bite) (tiefling daemon-spawn)"
|
"damage": [
|
||||||
],
|
{
|
||||||
"spells": [
|
"formula": "1d4",
|
||||||
"Prestidigitation",
|
"types": [
|
||||||
"Oath of Anonymity",
|
"piercing"
|
||||||
"Detect Magic",
|
]
|
||||||
"Read Magic",
|
}
|
||||||
"Disrupt Undead",
|
],
|
||||||
"Ray of Frost",
|
"damage_bonus": 1,
|
||||||
"Summon Instrument",
|
"crit_range": 19,
|
||||||
"Mage Hand",
|
"crit_mult": 2,
|
||||||
"Open/Close",
|
"reach_ft": 5,
|
||||||
"Arcane Mark",
|
"count": 1,
|
||||||
"Acid Splash",
|
"range_increment_ft": 10
|
||||||
"Grasp",
|
},
|
||||||
"Shield",
|
{
|
||||||
"Cure Light Wounds",
|
"name": "Talons (attaque naturelle)",
|
||||||
"Color Spray",
|
"kind": "melee",
|
||||||
"Saving Finale",
|
"attack_bonus": 4,
|
||||||
"Vanish",
|
"damage": [
|
||||||
"Shocking Grasp",
|
{
|
||||||
"Burning Hands",
|
"formula": "1d4",
|
||||||
"Alarm",
|
"types": [
|
||||||
"Scorching Ray",
|
"slashing"
|
||||||
"Bladed Dash",
|
]
|
||||||
"Mirror Image",
|
}
|
||||||
"Heroism"
|
],
|
||||||
]
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-122
@@ -1,126 +1,127 @@
|
|||||||
{
|
{
|
||||||
"name": "Oni Triumvir",
|
"name": "Oni Triumvir",
|
||||||
"level": 4,
|
"level": 4,
|
||||||
"size": "Medium",
|
"size": "Medium",
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"str_score": 12,
|
"str_score": 12,
|
||||||
"dex_score": 14,
|
"dex_score": 14,
|
||||||
"con_score": 14,
|
"con_score": 14,
|
||||||
"int_score": 12,
|
"int_score": 12,
|
||||||
"wis_score": 18,
|
"wis_score": 18,
|
||||||
"cha_score": 16
|
"cha_score": 16
|
||||||
},
|
},
|
||||||
"hp_max": 36,
|
"hp_max": 36,
|
||||||
"ac": {
|
"ac": {
|
||||||
"total": 19,
|
"total": 19,
|
||||||
"touch": 18,
|
"touch": 18,
|
||||||
"flat_footed": 17
|
"flat_footed": 17
|
||||||
},
|
},
|
||||||
"bab": 4,
|
"bab": 4,
|
||||||
"initiative_mod": 2,
|
"initiative_mod": 2,
|
||||||
"speed_land_ft": 30,
|
"speed_land_ft": 30,
|
||||||
"saves": {
|
"saves": {
|
||||||
"fort": 7,
|
"fort": 7,
|
||||||
"ref": 7,
|
"ref": 7,
|
||||||
"will": 9
|
"will": 9
|
||||||
},
|
},
|
||||||
"casting_ability": "wis",
|
"casting_ability": "wis",
|
||||||
"spell_slots": {
|
"spell_slots": {
|
||||||
"1": 4,
|
"1": 4,
|
||||||
"2": 3
|
"2": 3
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"channel_energy": 6
|
"channel_energy": 6
|
||||||
},
|
},
|
||||||
"attacks": [
|
"attacks": [
|
||||||
|
{
|
||||||
|
"name": "Longbow",
|
||||||
|
"kind": "ranged",
|
||||||
|
"attack_bonus": 6,
|
||||||
|
"damage": [
|
||||||
{
|
{
|
||||||
"name": "Longbow",
|
"formula": "1d8",
|
||||||
"kind": "ranged",
|
"types": [
|
||||||
"attack_bonus": 6,
|
"piercing"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": "fiches_personnages/oni_sheet.json",
|
"damage_bonus": 0,
|
||||||
"notes": "Armes placeholder Foundry sans dégâts encodés omises : 'une Pioche' (non maîtrisée) et 'Weapon' (à remplacer par la vraie arme).",
|
"crit_range": 20,
|
||||||
"features": [
|
"crit_mult": 3,
|
||||||
"Summon Good Monster",
|
"reach_ft": 5,
|
||||||
"Sacred Summons",
|
"count": 1,
|
||||||
"Selective Channeling",
|
"range_increment_ft": 100
|
||||||
"Extremely Fashionable",
|
},
|
||||||
"Exalted of the Society (Cleric, Pathfinder Society)",
|
{
|
||||||
"Bitter",
|
"name": "Alchemist's Fire",
|
||||||
"Ambitious"
|
"kind": "touch",
|
||||||
],
|
"attack_bonus": 6,
|
||||||
"spells": [
|
"damage": [
|
||||||
"Guidance",
|
{
|
||||||
"Detect Magic",
|
"formula": "1d6",
|
||||||
"Stabilize",
|
"types": [
|
||||||
"Read Magic",
|
"fire"
|
||||||
"Detect Poison",
|
]
|
||||||
"Purify Food and Drink",
|
}
|
||||||
"Summon Monster I",
|
],
|
||||||
"Shield of Faith",
|
"damage_bonus": 0,
|
||||||
"Forbid Action",
|
"crit_range": 20,
|
||||||
"Cure Light Wounds",
|
"crit_mult": 2,
|
||||||
"Hidden Diplomacy",
|
"reach_ft": 5,
|
||||||
"Protection from Evil",
|
"count": 1,
|
||||||
"Bless",
|
"range_increment_ft": 10
|
||||||
"Summon Monster II",
|
},
|
||||||
"Cure Moderate Wounds",
|
{
|
||||||
"Sound Burst",
|
"name": "Attaques naturelles (Shifter Claws / formes fées)",
|
||||||
"Suppress Charms and Compulsions",
|
"kind": "melee",
|
||||||
"Delay Poison",
|
"attack_bonus": 5,
|
||||||
"Ironskin",
|
"damage": [
|
||||||
"Restoration, Lesser"
|
{
|
||||||
]
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6699,6 +6699,12 @@ class CombatEngine:
|
|||||||
target_live = self._stats[target.combatant.id]
|
target_live = self._stats[target.combatant.id]
|
||||||
hp_before = target.hp
|
hp_before = target.hp
|
||||||
override = bonus_override
|
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:
|
if attack_penalty:
|
||||||
base = weapon.attack_bonus if override is None else override
|
base = weapon.attack_bonus if override is None else override
|
||||||
override = base + attack_penalty
|
override = base + attack_penalty
|
||||||
@@ -6829,6 +6835,12 @@ class CombatEngine:
|
|||||||
if weapon.count > 1:
|
if weapon.count > 1:
|
||||||
self._attack(attacker, target, weapon, attack_penalty=attack_penalty)
|
self._attack(attacker, target, weapon, attack_penalty=attack_penalty)
|
||||||
return
|
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(
|
n = min(
|
||||||
_MAX_ITERATIVES,
|
_MAX_ITERATIVES,
|
||||||
1 + max(0, (attacker.combatant.bab - 1) // _ITERATIVE_PENALTY),
|
1 + max(0, (attacker.combatant.bab - 1) // _ITERATIVE_PENALTY),
|
||||||
|
|||||||
@@ -242,14 +242,18 @@ class AttackSpec(_FrozenModel):
|
|||||||
increment and against full AC beyond it. `two_handed` marks a weapon swung
|
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
|
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
|
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"):
|
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
|
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
|
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). 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, which is a DISK: a Large creature's natural attack (its JSON
|
||||||
`reach_ft` IS its natural reach, 10 ft) still strikes and threatens
|
`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
|
id: str
|
||||||
@@ -266,6 +270,8 @@ class AttackSpec(_FrozenModel):
|
|||||||
count: int = Field(default=1, ge=1)
|
count: int = Field(default=1, ge=1)
|
||||||
firearm: bool = False
|
firearm: bool = False
|
||||||
two_handed: bool = False
|
two_handed: bool = False
|
||||||
|
natural: bool = False
|
||||||
|
secondary: bool = False
|
||||||
|
|
||||||
|
|
||||||
class DamageReduction(_FrozenModel):
|
class DamageReduction(_FrozenModel):
|
||||||
|
|||||||
@@ -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]
|
||||||
Reference in New Issue
Block a user