From 24b6125eb6b883349fb323cb1c55bbb71e371cb2 Mon Sep 17 00:00:00 2001 From: ctan Date: Sun, 30 Aug 2026 10:33:12 +0200 Subject: [PATCH] feat(data): companions and mounts --- data/pcs/esha_mount.json | 60 ++++++++ data/pcs/harvie.json | 34 ++--- data/pcs/ierlieth.json | 40 ++--- data/pcs/jeanne.json | 39 ++--- generate_spells.py | 321 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 421 insertions(+), 73 deletions(-) create mode 100644 data/pcs/esha_mount.json create mode 100644 generate_spells.py diff --git a/data/pcs/esha_mount.json b/data/pcs/esha_mount.json new file mode 100644 index 0000000..e234de1 --- /dev/null +++ b/data/pcs/esha_mount.json @@ -0,0 +1,60 @@ +{ + "name": "Esha's Mount", + "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": [ + { + "id": "esha_mount:hooves", + "name": "Hooves", + "kind": "melee", + "attack_bonus": 8, + "damage": [ + { + "formula": "1d6", + "types": ["bludgeoning"] + } + ], + "damage_bonus": 4, + "crit_range": 20, + "crit_mult": 2, + "reach_ft": 10, + "count": 2 + } + ], + "source": "fiches_personnages/esha_sheet.json (mount)", + "features": [ + "Combat Reflexes", + "Alertness", + "Armor Proficiency (Light)", + "Armor Proficiency (Medium)", + "Thoroughbred", + "Animal (race)" + ], + "spells": [], + "casting_ability": "wis", + "resources": {}, + "metamagic_feats": [], + "skills": {} +} diff --git a/data/pcs/harvie.json b/data/pcs/harvie.json index 69a6301..8fdfd9b 100644 --- a/data/pcs/harvie.json +++ b/data/pcs/harvie.json @@ -7,7 +7,7 @@ "dex_score": 15, "con_score": 12, "int_score": 2, - "wis_score": 14, + "wis_score": 12, "cha_score": 6 }, "hp_max": 15, @@ -19,42 +19,39 @@ "bab": 1, "initiative_mod": 2, "speed_land_ft": 10, + "speed_fly_ft": 80, "saves": { "fort": 4, "ref": 5, "will": 2 }, - "casting_ability": "wis", - "resources": {}, "attacks": [ { - "name": "Talons (attaque naturelle)", + "id": "harvie:bite", + "name": "Bite", "kind": "melee", - "attack_bonus": 2, + "attack_bonus": 3, "damage": [ { "formula": "1d4", - "types": [ - "slashing" - ] + "types": ["piercing"] } ], "damage_bonus": 0, "crit_range": 20, "crit_mult": 2, "reach_ft": 5, - "count": 2 + "count": 1 }, { - "name": "Bite (attaque naturelle)", + "id": "harvie:claw", + "name": "Claw", "kind": "melee", - "attack_bonus": -3, + "attack_bonus": 3, "damage": [ { - "formula": "1d4", - "types": [ - "piercing" - ] + "formula": "1d3", + "types": ["slashing"] } ], "damage_bonus": 0, @@ -65,11 +62,14 @@ } ], "source": "fiches_personnages/harvie_sheet.json", - "speed_fly_ft": 80, "features": [ "Friendly Face", "Fey-touched", "Animal (race)" ], - "spells": [] + "spells": [], + "casting_ability": "wis", + "resources": {}, + "metamagic_feats": [], + "skills": {} } diff --git a/data/pcs/ierlieth.json b/data/pcs/ierlieth.json index 889c75d..0b3814e 100644 --- a/data/pcs/ierlieth.json +++ b/data/pcs/ierlieth.json @@ -19,45 +19,25 @@ "bab": 3, "initiative_mod": 3, "speed_land_ft": 15, + "speed_fly_ft": 40, "saves": { "fort": 3, "ref": 6, "will": 3 }, - "casting_ability": "wis", - "resources": {}, "attacks": [ { - "name": "Bite (attaque naturelle)", + "id": "ierlieth:bite", + "name": "Bite", "kind": "melee", "attack_bonus": 4, "damage": [ { "formula": "1d4", - "types": [ - "piercing" - ] + "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, + "damage_bonus": 1, "crit_range": 20, "crit_mult": 2, "reach_ft": 5, @@ -66,9 +46,13 @@ ], "source": "fiches_personnages/ierlieth_sheet.json", "features": [ + "Drake (race)", "Toughness", - "Power Attack", - "Drake (race)" + "Power Attack" ], - "spells": [] + "spells": [], + "casting_ability": "wis", + "resources": {}, + "metamagic_feats": [], + "skills": {} } diff --git a/data/pcs/jeanne.json b/data/pcs/jeanne.json index 4e82611..a2ad697 100644 --- a/data/pcs/jeanne.json +++ b/data/pcs/jeanne.json @@ -24,44 +24,23 @@ "ref": 6, "will": 2 }, - "casting_ability": "wis", - "resources": {}, "attacks": [ { - "name": "Hooves (attaque naturelle)", + "id": "jeanne:hooves", + "name": "Hooves", "kind": "melee", "attack_bonus": 8, "damage": [ { - "formula": "1d4", - "types": [ - "bludgeoning" - ] + "formula": "1d6", + "types": ["bludgeoning"] } ], "damage_bonus": 4, "crit_range": 20, "crit_mult": 2, - "reach_ft": 5, + "reach_ft": 10, "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", @@ -70,8 +49,12 @@ "Alertness", "Armor Proficiency (Light)", "Armor Proficiency (Medium)", - "Thoroughbred (Human; Keleshite)", + "Thoroughbred", "Animal (race)" ], - "spells": [] + "spells": [], + "casting_ability": "wis", + "resources": {}, + "metamagic_feats": [], + "skills": {} } diff --git a/generate_spells.py b/generate_spells.py new file mode 100644 index 0000000..20e94d2 --- /dev/null +++ b/generate_spells.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +"""Generate spell JSON files from PC sheet data and existing templates.""" + +import json +from pathlib import Path + +# Load existing spell data +spells_dir = Path("data/spells") +existing = {} +for f in Path("data/spells").glob("*.json"): + data = json.loads(f.read_text()) + existing[data["name"]] = data + +# Load all spell names and levels from PC sheets +pc_files = [ + "fiches_personnages/tammara_sheet.json", + "fiches_personnages/oni_sheet.json", + "fiches_personnages/nairda_sheet.json", + "fiches_personnages/misty_sheet.json", + "fiches_personnages/esha_sheet.json", + "fiches_personnages/harvie_sheet.json", + "fiches_personnages/ierlieth_sheet.json", + "fiches_personnages/jeanne_sheet.json", +] + +all_spells = {} # name -> {name, level, ...} +for pc_file in [ + "fiches_personnages/tammara_sheet.json", + "fiches_personnages/nairda_sheet.json", +]: + with open(pc_file) as f: + data = json.load(f) + if "spellcasting" in data and "spells" in data["spellcasting"]: + for spell in data["spellcasting"]["spells"]: + name = spell.get("name", "").strip() + if name: + if name not in all_spells: + all_spells[name] = spell + +print(f"Total unique spells from PC sheets: {len(all_spells)}") + +# Reference spell data for manual completion (school, range, save, etc.) +# This would ideally come from a PF1e spell database +# For now, we'll use defaults and mark for manual review + +# Default templates by spell level patterns +DEFAULTS = { + # Common spell properties + "range_defaults": { + "personal": "personal", + "touch": "touch", + "close": "close", + "medium": "medium", + "long": "long", + }, + "save_defaults": { + "damage": "ref", + "condition": "will", + "heal": "will", + "buff": "none", + "area": "ref", + }, + "school_guesses": { + "Abundant Ammunition": "transmutation", + "Aid": "abjuration", + "Air Bubble": "conjuration", + "Alarm": "abjuration", + "Arcane Mark": "universal", + "Bane": "enchantment", + "Bear's Endurance": "transmutation", + "Bladed Dash": "transmutation", + "Bleed": "necromancy", + "Bless": "enchantment", + "Blindness/Deafness": "transmutation", + "Bull's Strength": "transmutation", + "Burning Hands": "evocation", + "Charm Person": "enchantment", + "Color Spray": "illusion", + "Comprehend Languages": "divination", + "Create Alcool": "transmutation", + "Create Water": "conjuration", + "Cure Light Wounds": "conjuration", + "Cure Moderate Wounds": "conjuration", + "Dancing Lights": "evocation", + "Darkness": "evocation", + "Daze": "enchantment", + "Deja Vu": "divination", + "Delay Disease": "conjuration", + "Delay Poison": "conjuration", + "Detect Fiendish Presence": "divination", + "Detect Magic": "divination", + "Detect Poison": "divination", + "Divine Favor": "evocation", + "Doom": "necromancy", + "Drunkard's Breath": "transmutation", + "Endure Elements": "abjuration", + "Enhance Water": "transmutation", + "Enhanced Diplomacy": "enchantment", + "Face of the Devourer": "transmutation", + "Flurry of Snowballs": "evocation", + "Forbid Action": "enchantment", + "Grasp": "transmutation", + "Guidance": "divination", + "Heroism": "enchantment", + "Hex Vulnerability": "transmutation", + "Hidden Diplomacy": "enchantment", + "Hide from Undead": "abjuration", + "Ice Slick": "conjuration", + "Ill Omen": "necromancy", + "Inflict Light Wounds": "necromancy", + "Ironskin": "transmutation", + "Light": "evocation", + "Mage Hand": "transmutation", + "Make Whole": "transmutation", + "Masterwork Transformation": "transmutation", + "Mending": "transmutation", + "Message": "transmutation", + "Mirror Image": "illusion", + "Oath of Anonymity": "abjuration", + "Obscuring Mist": "conjuration", + "Open/Close": "transmutation", + "Owl's Wisdom": "transmutation", + "Prestidigitation": "universal", + "Protection from Evil": "abjuration", + "Protection from Evil, Communal": "abjuration", + "Protection from Law": "abjuration", + "Protection from Law, Communal": "abjuration", + "Purify Food and Drink": "transmutation", + "Putrefy Food and Drink": "transmutation", + "Ray of Enfeeblement": "necromancy", + "Ray of Frost": "evocation", + "Read Magic": "divination", + "Resistance": "abjuration", + "Restoration, Lesser": "conjuration", + "Sanctuary": "abjuration", + "Saving Finale": "abjuration", + "Scorching Ray": "evocation", + "Shard of Chaos": "evocation", + "Shield": "abjuration", + "Shield of Faith": "abjuration", + "Shocking Grasp": "evocation", + "Silence": "illusion", + "Sleep": "enchantment", + "Sound Burst": "evocation", + "Spark": "evocation", + "Spear of Purity": "evocation", + "Spiritual Weapon": "evocation", + "Stabilize": "conjuration", + "Status": "divination", + "Stave Off Corruption": "abjuration", + "Summon Instrument": "conjuration", + "Summon Monster I": "conjuration", + "Summon Monster II": "conjuration", + "Suppress Charms and Compulsions": "abjuration", + "Surmount Affliction": "abjuration", + "Tears to Wine": "divination", + "Touch of Fatigue": "necromancy", + "Unseen Servant": "conjuration", + "Vanish": "illusion", + "Virtue": "transmutation", + "Weapon of Awe": "transmutation", + "Web": "conjuration", + }, + "range_guesses": { + "personal": "personal", + "touch": "touch", + "close": "close", + "medium": "medium", + "long": "long", + }, + "save_guesses": { + "none": "none", + "fort": "fort", + "ref": "ref", + "will": "will", + }, +} + +def guess_school(name): + return DEFAULTS["school_guesses"].get(name, "transmutation") + +def guess_range(name, level): + # Simple heuristic: level 0 = close, 1 = close, 2 = medium, 3+ = long + if level == 0: + return "close" + elif level <= 1: + return "close" + elif level <= 2: + return "medium" + else: + return "long" + +def guess_save(name, school): + # Heuristic based on school + if school in ("abjuration", "divination"): + return "none" + elif school == "evocation": + return "ref" + elif school in ("enchantment", "necromancy"): + return "will" + elif school in ("conjuration", "transmutation", "illusion"): + return "ref" + return "none" + +def guess_effects(name, level, school): + """Generate basic effect structure based on spell name and school.""" + effects = [] + + # Damage spells + if school == "evocation" or "Ray" in name or "Ray" in name or "Scorching" in name or "Flurry" in name or "Fireball" in name or "Lightning" in name or "Burning" in name or "Acid" in name or "Acid" in name or "Shocking" in name: + dice = "1d6" + if level >= 2: + dice = f"{level}d6" + if "Fireball" in name: + dice = "6d6" + elif "Lightning Bolt" in name: + dice = f"{level}d6" + effects.append({ + "type": "damage", + "formula": dice, + "types": ["fire"] if "Fire" in name or "Burning" in name or "Flurry" in name else + ["electricity"] if "Lightning" in name or "Shocking" in name else + ["acid"] if "Acid" in name else + ["cold"] if "Ice" in name else + ["force"] if "Force" in name else + ["fire"], + "half_on_save": True + }) + # Healing spells + elif school == "conjuration" and ("Cure" in name or "Heal" in name or "Restoration" in name): + dice = "1d8+1" + if "Moderate" in name or level >= 2: + dice = "2d8+2" + elif "Lesser" in name: + dice = "1d4" + effects.append({ + "type": "heal", + "formula": dice + }) + # Condition spells + elif school in ("enchantment", "necromancy", "transmutation") and any(kw in name for kw in ["Blind", "Deaf", "Sleep", "Hold", "Paralyze", "Stun", "Daze", "Fear", "Doom", "Confuse", "Charm", "Dominate", "Hold", "Hide", "Blind", "Deaf"]): + cond_map = { + "blind": "blinded", + "deaf": "deafened", + "sleep": "asleep", + "hold": "paralyzed", + "paralyze": "paralyzed", + "stun": "stunned", + "daze": "dazed", + "fear": "frightened", + "doom": "shaken", + } + cond = "shaken" + for k, v in cond_map.items(): + if k in name.lower(): + cond = v + break + effects.append({ + "type": "condition", + "condition_name": cond, + "duration_rounds": 1 if level <= 1 else 1 + level // 2, + "save_negates": True + }) + # Buff spells + elif school in ("abjuration", "transmutation", "divination") and any(kw in name for kw in ["Strength", "Endurance", "Wisdom", "Intelligence", "Charisma", "Dexterity", "Armor", "Shield", "Protection", "Resistance", "Bull", "Bear", "Cat", "Fox", "Owl", "Eagle", "Divine", "Magic", "Magic", "Shield", "Armor", "Barkskin", "Mage Armor", "Heroism", "Enhance", "Enlarge", "Reduce", "Fly", "Haste", "Speed", "Invisibility", "Blur", "Mirror", "Displacement", "Darkvision", "True Seeing", "See Invisibility", "Detect", "Read", "Comprehend", "Tongues", "Message", "Status", "Guidance", "Resistance", "Virtue", "Endure", "Endure", "Ironskin", "Barkskin", "Mage Armor", "Shield", "Shield of Faith", "Protection", "Magic Vestment", "Magic Weapon", "Greater Magic Weapon", "Magic Fang", "Greater Magic Fang", "Magic Circle", "Align Weapon", "Holy Aura", "Unholy Aura", "Magic Circle", "Death Ward", "Freedom of Movement", "Mind Blank", "Spell Resistance", "Spell Immunity", "Anti-Magic", "Dispel", "Break Enchantment", "Remove", "Neutralize", "Delay", "Restore", "Restoration", "Heal", "Regenerate", "Reincarnate", "Raise", "Resurrection", "True Resurrection", "Breath of Life", "Raise Dead", "Reincarnate", "Clone", "Contingency", "Spell Turning", "Spell Immunity", "Anti-Magic", "Dispel", "Break", "Dismissal", "Banishment", "Planar", "Teleport", "Dimension Door", "Word of Recall", "Teleport", "Plane Shift", "Astral", "Ethereal", "Phase", "Maze", "Banishment", "Dismissal", "Imprisonment", "Trap the Soul", "Soul Bind", "Trap the Soul", "Temporal Stasis", "Time Stop", "Wish", "Miracle", "Limited Wish", "Wish", "Miracle"]): + pass # Buff handled by default + + # Default: no effects + return effects + +# Generate JSON files for all spells +output_dir = Path("data/spells") +output_dir.mkdir(parents=True, exist_ok=True) + +created = 0 +updated = 0 +skipped = 0 + +for name, data in sorted(all_spells.items()): + # Clean name for filename + safe_name = name.replace(" ", "_").replace("/", "_").replace(",", "").replace("'", "").replace("(", "").replace(")", "") + filename = f"{safe_name.lower()}.json" + filepath = Path("data/spells") / filename + + level = data.get("level", 0) + school = DEFAULTS["school_guesses"].get(name, "transmutation") + spell_range = "touch" if "Touch" in name else "close" if "Ray" in name else "close" + + # Check if file exists + existing_data = existing.get(name) + if existing_data: + # Update existing + updated += 1 + continue + + # Create new spell JSON + save_type = "none" # default + + # Determine effects + effects = [] + # This is simplified - in reality we'd need a proper spell database + + # For now, create minimal valid JSON + spell_json = { + "name": name, + "level": data.get("level", 0), + "school": DEFAULTS["school_guesses"].get(name, "transmutation"), + "range": "touch" if "Touch" in name else "close", + "save": "none", + "base_dc": 10, + "effects": [], + "description": f"TODO: Complete spell data for {name}" + } + + filepath = Path("data/spells") / f"{name.replace(' ', '_').replace('/', '_').replace(',', '').replace(\"'\", '').replace('(', '').replace(')', '').lower()}.json" + with open(filepath, "w") as f: + json.dump(spell_json, f, indent=2) + created += 1 + print(f"Created: {filepath}") + +print(f"\nDone: {created} created, {updated} updated, {skipped} skipped")