feat(data): companions and mounts

This commit is contained in:
2026-09-04 22:55:46 +02:00
parent df624cc041
commit b300a413be
5 changed files with 421 additions and 73 deletions
+60
View File
@@ -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": {}
}
+17 -17
View File
@@ -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": {}
}
+12 -28
View File
@@ -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": {}
}
+11 -28
View File
@@ -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": {}
}
+321
View File
@@ -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")