feat(data): lich monster
This commit is contained in:
@@ -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": {}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user