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
This commit is contained in:
2026-08-17 22:49:50 +02:00
parent cd7f4f7042
commit 7297494e56
10 changed files with 736 additions and 5 deletions
+17 -5
View File
@@ -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
+85
View File
@@ -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"
}
+67
View File
@@ -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
}
+66
View File
@@ -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"
}
+66
View File
@@ -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"
}
+121
View File
@@ -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"
}
+67
View File
@@ -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"
}
+87
View File
@@ -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)."
}
+123
View File
@@ -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"
}
+37
View File
@@ -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: