From 7297494e56e3d3e97a2226ebeb202e2cada56b20 Mon Sep 17 00:00:00 2001 From: Thien An Date: Mon, 17 Aug 2026 22:49:50 +0200 Subject: [PATCH] feat(data): transpose Foundry PC sheets into data/pcs All 8 sheets from fiches_personnages/ converted to the strict Combatant JSON schema (data/monsters convention) so the CLI can run PCs directly: --side players data/pcs/esha.json - id derived from the file stem; source field points at the original sheet - oni: placeholder attacks without encoded damage ('une Pioche', 'Weapon') left out, documented in notes (pick stats ambiguous light/heavy in the rules KB; no identification possible for 'Weapon') - README: PC section now documents data/pcs usage - tests: parametrized load check + golden spots for the 8 PC files --- README.md | 22 +++++-- data/pcs/esha.json | 85 ++++++++++++++++++++++++ data/pcs/harvie.json | 67 +++++++++++++++++++ data/pcs/ierlieth.json | 66 +++++++++++++++++++ data/pcs/jeanne.json | 66 +++++++++++++++++++ data/pcs/misty.json | 121 ++++++++++++++++++++++++++++++++++ data/pcs/nairda.json | 67 +++++++++++++++++++ data/pcs/oni.json | 87 +++++++++++++++++++++++++ data/pcs/tammara.json | 123 +++++++++++++++++++++++++++++++++++ tests/test_monster_loader.py | 37 +++++++++++ 10 files changed, 736 insertions(+), 5 deletions(-) create mode 100644 data/pcs/esha.json create mode 100644 data/pcs/harvie.json create mode 100644 data/pcs/ierlieth.json create mode 100644 data/pcs/jeanne.json create mode 100644 data/pcs/misty.json create mode 100644 data/pcs/nairda.json create mode 100644 data/pcs/oni.json create mode 100644 data/pcs/tammara.json diff --git a/README.md b/README.md index e76ce83..39221d2 100644 --- a/README.md +++ b/README.md @@ -232,12 +232,24 @@ Champ `range_increment_ft` (optionnel, uniquement pour `kind: ranged`) : portée maximale de tir — au-delà, l'arme n'est pas utilisable. `damage_bonus` et `dr` sont optionnels. Deux monstres d'exemple sont fournis : gobelin et orc. -### Fiches de personnages +### Fiches de personnages (PJ) -`fiches_personnages/` contient 8 fiches de PJ au format Foundry VTT -(`pf1-sheet/v1`) ; le chargeur `loaders/foundry.py` sait les lire. Les -rencontres CLI se construisent pour l'instant avec des monstres JSON — les -fiches Foundry alimenteront la couche PJ dans une phase ultérieure. +`fiches_personnages/` contient les 8 fiches de PJ au format Foundry VTT +(`pf1-sheet/v1`) ; le chargeur `loaders/foundry.py` sait les lire. Elles sont +transposées en JSON au schéma `Combatant` dans `data/pcs/` (`esha.json`, +`harvie.json`, `ierlieth.json`, `jeanne.json`, `misty.json`, `nairda.json`, +`oni.json`, `tammara.json`) et se passent directement à la CLI comme les +monstres : + +```bash +uv run pf1e-sim --map data/maps/sample_arena.yaml \ + --side players data/pcs/oni.json \ + --side monsters data/pcs/tammara.json --runs 200 +``` + +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. ## Règles implémentées et limites de la Phase 0 diff --git a/data/pcs/esha.json b/data/pcs/esha.json new file mode 100644 index 0000000..bca89b4 --- /dev/null +++ b/data/pcs/esha.json @@ -0,0 +1,85 @@ +{ + "name": "Esha Randu", + "level": 4, + "size": "Medium", + "abilities": { + "str_score": 14, + "dex_score": 18, + "con_score": 14, + "int_score": 16, + "wis_score": 12, + "cha_score": 9 + }, + "hp_max": 32, + "ac": { + "total": 19, + "touch": 15, + "flat_footed": 14 + }, + "bab": 4, + "initiative_mod": 5, + "speed_land_ft": 25, + "saves": { + "fort": 6, + "ref": 10, + "will": 5 + }, + "attacks": [ + { + "name": "Lance", + "kind": "melee", + "attack_bonus": 7, + "damage": [ + { + "formula": "1d8", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 4, + "crit_range": 20, + "crit_mult": 3, + "reach_ft": 10, + "count": 1 + }, + { + "name": "Composite Shortbow +3", + "kind": "ranged", + "attack_bonus": 9, + "damage": [ + { + "formula": "1d6", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 3, + "crit_range": 20, + "crit_mult": 3, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 70 + }, + { + "name": "Earth Breaker", + "kind": "melee", + "attack_bonus": 8, + "damage": [ + { + "formula": "2d6", + "types": [ + "bludgeoning" + ] + } + ], + "damage_bonus": 4, + "crit_range": 20, + "crit_mult": 3, + "reach_ft": 5, + "count": 1 + } + ], + "source": "fiches_personnages/esha_sheet.json" +} diff --git a/data/pcs/harvie.json b/data/pcs/harvie.json new file mode 100644 index 0000000..6c10f79 --- /dev/null +++ b/data/pcs/harvie.json @@ -0,0 +1,67 @@ +{ + "name": "Harvie", + "level": 2, + "size": "Small", + "abilities": { + "str_score": 10, + "dex_score": 15, + "con_score": 12, + "int_score": 2, + "wis_score": 14, + "cha_score": 6 + }, + "hp_max": 15, + "ac": { + "total": 14, + "touch": 13, + "flat_footed": 12 + }, + "bab": 1, + "initiative_mod": 2, + "speed_land_ft": 10, + "saves": { + "fort": 4, + "ref": 5, + "will": 2 + }, + "attacks": [ + { + "name": "Talons (attaque naturelle)", + "kind": "melee", + "attack_bonus": 2, + "damage": [ + { + "formula": "1d4", + "types": [ + "slashing" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 2 + }, + { + "name": "Bite (attaque naturelle)", + "kind": "melee", + "attack_bonus": -3, + "damage": [ + { + "formula": "1d4", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + } + ], + "source": "fiches_personnages/harvie_sheet.json", + "speed_fly_ft": 80 +} diff --git a/data/pcs/ierlieth.json b/data/pcs/ierlieth.json new file mode 100644 index 0000000..6ad9cd1 --- /dev/null +++ b/data/pcs/ierlieth.json @@ -0,0 +1,66 @@ +{ + "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, + "saves": { + "fort": 3, + "ref": 6, + "will": 3 + }, + "attacks": [ + { + "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": "Tail (attaque naturelle)", + "kind": "melee", + "attack_bonus": -1, + "damage": [ + { + "formula": "1d4", + "types": [ + "bludgeoning" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + } + ], + "source": "fiches_personnages/ierlieth_sheet.json" +} diff --git a/data/pcs/jeanne.json b/data/pcs/jeanne.json new file mode 100644 index 0000000..38e69ea --- /dev/null +++ b/data/pcs/jeanne.json @@ -0,0 +1,66 @@ +{ + "name": "Jeanne", + "level": 4, + "size": "Large", + "abilities": { + "str_score": 18, + "dex_score": 14, + "con_score": 15, + "int_score": 2, + "wis_score": 12, + "cha_score": 6 + }, + "hp_max": 31, + "ac": { + "total": 23, + "touch": 11, + "flat_footed": 21 + }, + "bab": 3, + "initiative_mod": 2, + "speed_land_ft": 40, + "saves": { + "fort": 6, + "ref": 6, + "will": 2 + }, + "attacks": [ + { + "name": "Hooves (attaque naturelle)", + "kind": "melee", + "attack_bonus": 8, + "damage": [ + { + "formula": "1d4", + "types": [ + "bludgeoning" + ] + } + ], + "damage_bonus": 4, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 2 + }, + { + "name": "Bite (attaque naturelle)", + "kind": "melee", + "attack_bonus": 3, + "damage": [ + { + "formula": "1d3", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 2, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + } + ], + "source": "fiches_personnages/jeanne_sheet.json" +} diff --git a/data/pcs/misty.json b/data/pcs/misty.json new file mode 100644 index 0000000..c182fc7 --- /dev/null +++ b/data/pcs/misty.json @@ -0,0 +1,121 @@ +{ + "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 + }, + "attacks": [ + { + "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 + } + ], + "source": "fiches_personnages/misty_sheet.json" +} diff --git a/data/pcs/nairda.json b/data/pcs/nairda.json new file mode 100644 index 0000000..3826857 --- /dev/null +++ b/data/pcs/nairda.json @@ -0,0 +1,67 @@ +{ + "name": "Nairda Guisenda", + "level": 4, + "size": "Medium", + "abilities": { + "str_score": 9, + "dex_score": 16, + "con_score": 14, + "int_score": 21, + "wis_score": 14, + "cha_score": 12 + }, + "hp_max": 31, + "ac": { + "total": 13, + "touch": 13, + "flat_footed": 10 + }, + "bab": 3, + "initiative_mod": 3, + "speed_land_ft": 30, + "saves": { + "fort": 4, + "ref": 5, + "will": 7 + }, + "attacks": [ + { + "name": "Coup à mains nues (aucune arme encodée)", + "kind": "melee", + "attack_bonus": 2, + "damage": [ + { + "formula": "1d3", + "types": [ + "bludgeoning" + ] + } + ], + "damage_bonus": -1, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + }, + { + "name": "Attaque à distance (aucune arme encodée)", + "kind": "ranged", + "attack_bonus": 6, + "damage": [ + { + "formula": "1d4", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 10 + } + ], + "source": "fiches_personnages/nairda_sheet.json" +} diff --git a/data/pcs/oni.json b/data/pcs/oni.json new file mode 100644 index 0000000..bf318c4 --- /dev/null +++ b/data/pcs/oni.json @@ -0,0 +1,87 @@ +{ + "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 + }, + "attacks": [ + { + "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 + } + ], + "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)." +} diff --git a/data/pcs/tammara.json b/data/pcs/tammara.json new file mode 100644 index 0000000..4ecf22d --- /dev/null +++ b/data/pcs/tammara.json @@ -0,0 +1,123 @@ +{ + "name": "Tammara Cailean", + "level": 4, + "size": "Medium", + "abilities": { + "str_score": 14, + "dex_score": 20, + "con_score": 14, + "int_score": 9, + "wis_score": 17, + "cha_score": 12 + }, + "hp_max": 36, + "ac": { + "total": 20, + "touch": 16, + "flat_footed": 14 + }, + "bab": 4, + "initiative_mod": 7, + "speed_land_ft": 30, + "saves": { + "fort": 7, + "ref": 10, + "will": 8 + }, + "attacks": [ + { + "name": "Rapier (masterwork)", + "kind": "melee", + "attack_bonus": 7, + "damage": [ + { + "formula": "1d6", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 2, + "crit_range": 18, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + }, + { + "name": "Braises d'Automne (pistolet ?)", + "kind": "ranged", + "attack_bonus": 9, + "damage": [ + { + "formula": "1d8", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 4, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 20 + }, + { + "name": "Adieux du Printemps (pistolet, masterwork)", + "kind": "ranged", + "attack_bonus": 10, + "damage": [ + { + "formula": "1d8", + "types": [ + "piercing" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 4, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 20 + }, + { + "name": "Alchemist's Fire", + "kind": "touch", + "attack_bonus": 9, + "damage": [ + { + "formula": "1d6", + "types": [ + "fire" + ] + } + ], + "damage_bonus": 0, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1, + "range_increment_ft": 10 + }, + { + "name": "Unarmed Strike", + "kind": "melee", + "attack_bonus": 6, + "damage": [ + { + "formula": "1d3", + "types": [ + "bludgeoning" + ] + } + ], + "damage_bonus": 2, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 5, + "count": 1 + } + ], + "source": "fiches_personnages/tammara_sheet.json" +} diff --git a/tests/test_monster_loader.py b/tests/test_monster_loader.py index a7d0610..360f7dd 100644 --- a/tests/test_monster_loader.py +++ b/tests/test_monster_loader.py @@ -11,6 +11,43 @@ from pf1e_simulator.dice import DiceExpr from pf1e_simulator.loaders.monster import MonsterLoadError, load_monster MONSTERS_DIR = Path(__file__).resolve().parents[1] / "data" / "monsters" +PCS_DIR = Path(__file__).resolve().parents[1] / "data" / "pcs" + + +@pytest.mark.parametrize( + "stem", + ["esha", "harvie", "ierlieth", "jeanne", "misty", "nairda", "oni", "tammara"], +) +def test_pc_sheet_loads(stem: str) -> None: + """Every transposed PC sheet loads through the strict monster JSON path.""" + combatant = load_monster(PCS_DIR / f"{stem}.json") + assert combatant.attacks + assert combatant.hp_max >= 1 + assert combatant.ac.total >= 10 + assert combatant.speed_land_ft >= 0 + + +def test_pc_sheets_golden_spots() -> None: + esha = load_monster(PCS_DIR / "esha.json") + assert esha.name == "Esha Randu" + assert [a.name for a in esha.attacks] == [ + "Lance", + "Composite Shortbow +3", + "Earth Breaker", + ] + assert esha.attacks[0].reach_ft == 10 + assert esha.attacks[1].range_increment_ft == 70 + + harvie = load_monster(PCS_DIR / "harvie.json") + assert harvie.size == "Small" + assert harvie.speed_fly_ft == 80 + + oni = load_monster(PCS_DIR / "oni.json") + assert "Pioche" in oni.notes + assert "Weapon" in oni.notes + + tammara = load_monster(PCS_DIR / "tammara.json") + assert tammara.initiative_mod == 7 def _write(tmp_path: Path, payload: dict[str, object]) -> Path: