feat(rules): carried items + real steal transfers, unarmed-disarm auto-pickup

- Combatant.carried_items (APG Steal: non-wielded, non-closely-worn gear),
  loaded from JSON, seeded into CombatantState, exposed in the snapshot.
- _maneuver_steal: success transfers the first carried item victim -> thief
  (logged); empty list keeps the documented nothing-stealable behaviour.
- _maneuver_disarm: unarmed attacker's success auto-picks the dropped weapon
  into its carried_items (SRD Disarm); armed disarms unchanged. Dropped-weapon
  recovery via retrieve_weapon (move action) verified + pinned by tests.
- data/pcs: gear transcribed from Foundry sheet inventories (esha 13,
  jeanne 3, misty 9, nairda 7, oni 6, tammara 20); harvie/ierlieth/
  esha_mount and all monsters empty (no stealable inventory), noted in README.
This commit is contained in:
2026-09-20 16:05:07 +02:00
parent 74f5d64c43
commit d0f7f3a107
14 changed files with 260 additions and 22 deletions
+49 -10
View File
@@ -299,7 +299,14 @@ Le `reach_ft` d'une attaque naturelle (morsure, sabots) n'est **pas** une arme
à allonge : il encode l'allonge naturelle de la créature, qui reste un disque.
`spell_resistance` (entier), `resistances` (dict type → montant) et
`immunities` (liste de types) sont optionnels.
Deux monstres d'exemple sont fournis : gobelin et orc.
Le tableau `carried_items` (optionnel, défaut vide) énumère les **objets
transportés non maniés et non portés serré** — potions, scrolls, outils,
bourses, clés, vivres… — seuls objets volables par la manœuvre « Steal » de
l'APG (voir « Manœuvres de combat »). Les armes maniées vivent dans
`attacks` (elles se prennent par disarm, pas par steal) ; l'équipement porté
serré (armure, sac à dos, vêtements, anneaux) ne peut pas être volé et n'a
pas à être encodé — la CA des JSON reste une valeur finale. Deux monstres
d'exemple sont fournis : gobelin et orc.
### Fiches de personnages (PJ)
@@ -318,7 +325,17 @@ uv run pf1e-sim --map data/maps/sample_arena.yaml \
Les armes placeholder des fiches sans dégâts encodés sont omises (notées
dans `notes` le cas échéant) ; le champ `source` de chaque fichier pointe
vers la fiche Foundry d'origine.
vers la fiche Foundry d'origine. Les `carried_items` (objets volables par
« Steal ») sont transcrits des inventaires `inventory.items` des fiches
Foundry (esha 13, jeanne 3, misty 9, nairda 7, oni 6, tammara 20), filtrés
conformément à l'APG : armes maniées (déjà dans `attacks`), équipement porté
serré (armures, anneaux, amulettes, capes, ceintures, wayfinders, vêtements,
sacs à dos) et contenants portés exclus. Trois fiches n'ont **aucun** objet
volable : `harvie.json` (fiche sans inventaire), `ierlieth.json` (seul un
« Backpack of Naidra » — sac à dos = porté serré, non volable) et
`esha_mount.json` (monture sans inventaire propre) — listes laissées vides.
Les monstres (`data/monsters/*.json`, issu du bestiaire, sans fiche Foundry
ni inventaire encodé) portent `carried_items` vide.
> **`data/pcs/*.json` est la référence canonique des PJ.** Le chargeur
> `loaders/foundry.py` est un outil de migration **one-shot et incomplet** :
@@ -1504,7 +1521,16 @@ Règles modélisées :
trip → `prone` ; disarm → l'arme tenue est **au sol** (SRD « Disarm » :
« your target drops one item it is carrying » — `weapon_dropped`, exclue
de `usable_attacks()` jusqu'à un `retrieve_weapon` ; un échec de 10 ou
plus fait tomber **l'arme de l'attaquant**) ; bull_rush → pousse la
plus fait tomber **l'arme de l'attaquant**) ; conformance (SRD « Disarm »)
: « If you successfully disarm your opponent without using a weapon, you
may automatically pick up the item dropped » — un désarmement **réussi**
par un attaquant **sans aucune arme de mêlée utilisable** (tentative à
mains nues) saisit aussitôt l'arme tombée : elle quitte le sol et entre
dans les **objets transportés** du désarmeur (`carried_items`, journalisé
`unarmed disarm pickup`) — simplification documentée : l'arme saisie
n'étant pas une `AttackSpec` du désarmeur, elle est **tenue, pas maniée**
(pas utilisable pour attaquer) ; l'arme reste exclue des attaques de la
victime jusqu'à son propre `retrieve_weapon` ; bull_rush → pousse la
cible (direction sélectionnable via `Action.direction`) ; conformance L6
(SRD « Bull Rush ») : la poussée vaut 5 ft plus « For every 5 by which
your attack exceeds your opponent's CMD you can push the target back an
@@ -1558,12 +1584,23 @@ Règles modélisées :
« Dirty Trick » ;
`blinded`/`dazzled`/`deafened`/`entangled`/`shaken`/`sickened` possibles
(liste exacte de l'APG), `nauseated` exclu car hors de
la liste RAW, et rejeté à l'exécution avec journalisation) ; steal → le modèle ne porte
aucun objet transporté non tenu : en application de l'APG « Steal » (« Items
held in the hands (such as wielded weapons or wands) also cannot be taken
with the steal maneuver—you must use the disarm combat maneuver instead »),
une réussite ne vole rien (journalisé `nothing stealable — wielded weapons
require disarm`) ; reposition → déplacement de la cible de 5 ft plus
la liste RAW, et rejeté à l'exécution avec journalisation) ; steal → le
champ `carried_items` du JSON de combattant porte les **objets transportés
non maniés et non portés serré** (potions, scrolls, outils, bourses,
vivres…) — seuls objets volables selon l'APG « Steal » (« any item that is
neither held nor hidden in a bag or pack [...] Items that are closely worn
(such as armor, backpacks, boots, clothing, or rings) cannot be taken [...]
Items held in the hands (such as wielded weapons or wands) also cannot be
taken with the steal maneuver—you must use the disarm combat maneuver
instead ») : une réussite **transfère un objet** de la liste de la cible
vers celle du voleur (journalisé `took <objet>`) ; simplifications
documentées : le moteur prend le **premier** objet de la liste (le choix
RAW de l'objet n'a pas de slot d'action), la main libre obligatoire et le
+5 CMD des objets attachés (capes, armes fourrées, bourses) ne sont pas
modélisés ; si la cible n'a **aucun objet transporté**, une réussite ne
vole rien (journalisé `nothing stealable — wielded weapons
require disarm`) ; les armes maniées (`attacks`) restent insaisissables
par steal — il faut désarmer ; reposition → déplacement de la cible de 5 ft plus
« For every 5 by which your attack exceeds your opponent's CMD, you can
move the target an additional 5 feet » (APG « Reposition ») — `1 +
marge//5` cases le long de la direction choisie (premier voisin libre par
@@ -1702,7 +1739,9 @@ Règles modélisées :
state) -> tuple[Action, ...]`). Schéma enrichi (E1-E5) :
- Par combattant : id, side, pos, occupied_cells, hp/hp_max, active/dead,
dying/stabilized, conditions (noms + rounds_left), resources_left (+ `spell_slot_{level}`),
weapon_broken/weapon_destroyed/weapon_stolen/weapon_dropped, grappled_with, moved_this_turn, power_attack,
weapon_broken/weapon_destroyed/weapon_stolen/weapon_dropped, carried_items
(tuple dans l'ordre de la liste — les objets transportés volables, APG
« Steal »), grappled_with, moved_this_turn, power_attack,
name, level, size, natural_reach_ft (S4c : allonge naturelle du gabarit
en pieds — 0 pour Tiny/Diminutive/Fine, 5 pour Small/Medium, 10 pour
Large… ; l'allonge effective d'une attaque reste `max(allonge naturelle,