feat(data): lich monster

This commit is contained in:
2026-09-04 22:55:48 +02:00
parent 46ac13a91e
commit bc5bf918b5
3 changed files with 90 additions and 406 deletions
+90
View File
@@ -0,0 +1,90 @@
{
"name": "Lich",
"level": 11,
"size": "Medium",
"abilities": {
"str_score": 12,
"dex_score": 14,
"con_score": 14,
"int_score": 20,
"wis_score": 14,
"cha_score": 16
},
"hp_max": 100,
"ac": {
"total": 25,
"touch": 14,
"flat_footed": 23
},
"bab": 8,
"initiative_mod": 6,
"speed_land_ft": 30,
"saves": {
"fort": 10,
"ref": 10,
"will": 14
},
"casting_ability": "int",
"resources": {
"spell_slots_1": 4,
"spell_slots_2": 4,
"spell_slots_3": 4,
"spell_slots_4": 4,
"spell_slots_5": 4,
"spell_slots_6": 3
},
"attacks": [
{
"id": "lich:touch",
"name": "Paralyzing Touch",
"kind": "touch",
"attack_bonus": 10,
"damage": [
{
"formula": "1d8+5",
"types": ["negative"]
}
],
"damage_bonus": 5,
"crit_range": 20,
"crit_mult": 2,
"reach_ft": 5,
"count": 1
}
],
"dr": {
"amount": 15,
"bypass": ["bludgeoning", "magic"]
},
"source": "bestiary",
"features": [
"Fear Aura",
"Paralyzing Touch",
"Undead Traits"
],
"spells": [
"Mage Armor",
"Shield",
"Magic Missile",
"Scorching Ray",
"Fireball",
"Lightning Bolt",
"Hold Person",
"Wall of Fire",
"Cloudkill",
"Disintegrate",
"Horrid Wilting",
"Power Word Kill"
],
"casting_ability": "int",
"resources": {
"spell_slots_1": 4,
"spell_slots_2": 4,
"spell_slots_3": 4,
"spell_slots_4": 4,
"spell_slots_5": 4,
"spell_slots_6": 3
},
"metamagic_feats": [],
"skills": {}
}
-321
View File
@@ -1,321 +0,0 @@
#!/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")
-85
View File
@@ -1,85 +0,0 @@
def _execute_deed(self, state: CombatantState, target: CombatantState | None, deed_name: str | None) -> None:
"""Execute a gunslinger deed (up_close_and_deadly, quick_clear)."""
if deed_name not in ("up_close_and_deadly", "quick_clear"):
return
if not state.spend_resource("grit", 1):
self._log(f"round {self._current_round} {state.combatant.id}: deed failed (no grit)")
return
if deed_name == "up_close_and_deadly":
# Up Close and Deadly: +1d6 damage within 30ft
if target is None or not target.active:
return
dist_ft = self._grid.distance(state.pos, target.pos) * _SQUARE_FT
if dist_ft <= 30:
# Apply +1d6 damage to next attack this round
state.deed_up_close_and_deadly = True
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"deed Up Close and Deadly activated (+1d6 dmg within 30ft)"
)
else:
self._log(f"round {self._current_round} {state.combatant.id}: deed Up Close and Deadly - target out of range")
elif deed_name == "quick_clear":
# Quick Clear: swift action, clears misfire
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"deed Quick Clear - firearm cleared"
)
# In a full implementation, this would clear the misfire condition
def _execute_spell_combat(self, state: CombatantState, target: CombatantState | None, spell_name: str | None) -> None:
"""Execute Spell Combat (cast spell + full attack in same round)."""
if target is None or not target.active:
return
if not state.spend_resource("arcane_pool", 1):
# Arcane pool not tracked yet, allow for now
pass
# Cast the spell
self._cast_spell(state, target, spell_name)
# Then make a full attack
self._execute_attack(state, target, "full_attack")
def _execute_inspire_courage(self, state: CombatantState, target: CombatantState | None) -> None:
"""Execute Inspire Courage (morale +1 attack/damage, 30ft radius, costs bardic_rounds)."""
if not state.spend_resource("bardic_rounds", 1):
self._log(f"round {self._current_round} {state.combatant.id}: inspire courage failed (no bardic rounds)")
return
if target is None:
target = state
# Apply to all allies within 30ft (6 cells)
radius_cells = 30 // _SQUARE_FT
center = state.pos
affected: list[CombatantState] = []
for state_other in self._states:
if not state_other.active:
continue
if state_other.side == state.side:
dist = self._grid.distance(center, state_other.pos)
if dist <= radius_cells:
affected.append(state_other)
if not affected:
self._log(f"round {self._current_round} {state.combatant.id}: inspire courage - no allies in range")
return
# Apply +1 morale bonus to attack and damage
for ally in affected:
ally.active_buffs.append(
ActiveBuff(
modifiers=(
StatModifier(target="attack", value=1, bonus_type="morale"),
StatModifier(target="damage", value=1, bonus_type="morale"),
),
rounds_left=1,
source=f"inspire courage by {state.combatant.id}",
)
)
# Also update old effects field for backwards compatibility
ally.effects.extend((
StatModifier(target="attack", value=1, bonus_type="morale"),
StatModifier(target="damage", value=1, bonus_type="morale"),
))
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"inspire courage +1 morale attack/damage to {len(affected)} allies"
)