Files
2026-09-15 09:29:11 +02:00

285 lines
13 KiB
Python

#!/usr/bin/env python3
"""Compile the reviewed 88-species combat catalogue; never edits worlds or historical eggs."""
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
# Species: initiative, signature, learned alternative. The engine composes the
# same native interactions; these explicit rows prevent silent generic fallbacks.
ROWS = """
bat MARK MARK DECOY
cat STRIKE PUSH INTERCEPT
chicken STRIKE PUSH GLIDE
cod WATER WATER PUSH
cow STRIKE CLEANSE GUARD
donkey STRIKE FOOD PUSH
fox STRIKE PICKUP DECOY
frog STRIKE PULL RESCUE
horse CHARGE CHARGE DASH
llama SPIT SPIT SPIT
mule STRIKE DELIVERY PICKUP
pig STRIKE TRACK PUSH
rabbit STRIKE DECOY LEAP
salmon CHARGE PUSH DASH
sheep GUARD GUARD PLATFORM
skeleton ARROW ARROW ARROW
slime STRIKE REBOUND PLATFORM
spider STRIKE WEB CHARGE
squid STRIKE CLOUD PUSH
tadpole MARK TRACK GUARD
tropical_fish MARK MARK DECOY
zombie STRIKE ROOT HEAL
armadillo GUARD GUARD CHARGE
bee STRIKE MARK STRIKE
camel STRIKE LEAP GUARD
cave_spider STRIKE STRIKE WEB
copper_golem STRIKE REDSTONE GUARD
dolphin CHARGE DASH RESCUE
drowned STRIKE ROOT ANCHOR
glow_squid MARK MARK MARK
goat STRIKE CHARGE INTERCEPT
husk STRIKE CLOUD STRIKE
ocelot STRIKE CHARGE INTERCEPT
panda STRIKE CHARGE GUARD
parrot MARK DECOY MARK
pufferfish STRIKE ZONE PUSH
polar_bear STRIKE GUARD STRIKE
snow_golem SNOWBALL SNOWBALL WATER
strider STRIKE DASH RESCUE
turtle GUARD GUARD GUARD
villager GUARD GUARD FOOD
wolf STRIKE INTERCEPT CHARGE
bogged ARROW ARROW ZONE
parched ARROW ARROW ARROW
allay PICKUP DELIVERY PICKUP
axolotl STRIKE VANISH HEAL
blaze FIREBALL FIREBALL ZONE
breeze WIND WIND GUARD
camel_husk STRIKE CLOUD RESCUE
creeper STRIKE EXPLOSION EXPLOSION
endermite STRIKE SWAP CHARGE
guardian BEAM BEAM BEAM
hoglin STRIKE CHARGE GUARD
magma_cube STRIKE ZONE ZONE
mooshroom STRIKE FOOD CLOUD
nautilus WATER WATER GUARD
phantom CHARGE CHARGE GLIDE
piglin STRIKE STRIKE ARROW
pillager ARROW ARROW ARROW
silverfish STRIKE ROOT CAVITY
sulfur_cube SPIT ZONE SPIT
trader_llama SPIT SPIT RALLY
wandering_trader MARK VANISH FOOD
zombie_horse STRIKE ANCHOR PUSH
zombie_villager STRIKE HEAL HEAL
zombified_piglin STRIKE MARK INTERCEPT
stray ARROW ARROW ZONE
vindicator STRIKE STRIKE STRIKE
zoglin STRIKE CHARGE PUSH
creaking STRIKE ROOT GUARD
elder_guardian BEAM GUARD PUSH
enderman STRIKE RESCUE PORTAL
evoker FANGS FANGS FANGS
ghast FIREBALL FIREBALL FIREBALL
happy_ghast GUARD LEAP PLATFORM
iron_golem STRIKE GUARD CHARGE
piglin_brute STRIKE GUARD DECOY
ravager STRIKE CHARGE PUSH
shulker SPIT SPIT GUARD
skeleton_horse STRIKE DASH RESCUE
sniffer STRIKE ROOT TRACK
vex CHARGE CHARGE CHARGE
witch POTION POTION CLOUD
wither_skeleton STRIKE STRIKE GUARD
zombie_nautilus STRIKE RESCUE ANCHOR
ender_dragon CHARGE BREATH PUSH
warden STRIKE BEAM MARK
wither SKULL SKULL SKULL
"""
EN_NAMES = """
bat|Echolocation|Diversion
cat|Arched back|Swift paw
chicken|Great wingbeat|Soft landing
cod|Rescue bubble|Splash
cow|Emergency milk|Steady shoulder
donkey|Supply point|Back off!
fox|Snatch|Feint
frog|Tongue lash|To the rescue
horse|Breakthrough|Clear the way
llama|Stopping spit|Volley
mule|Urgent delivery|Relay
pig|Snout to the ground|Shove
rabbit|False trail|Shared leap
salmon|Against the current|Wake
sheep|Wool cocoon|Cushion
skeleton|Aimed shot|Covering fire
slime|Bounce|Springboard
spider|Taut thread|Surprise descent
squid|Ink screen|Rear jet
tadpole|Way out!|Guard bubble
tropical_fish|Colour flash|False target
zombie|Hold on|Second wind
armadillo|Rolling shield|Ricochet
bee|Pollen|Precise sting
camel|Stride|High rampart
cave_spider|Tenacious bite|Web nest
copper_golem|Trigger|Copper parry
dolphin|Evacuation wake|Rescue
drowned|Grapple|Shared anchor
glow_squid|Glowing ink|Rally signal
goat|Battering ram|Counter-charge
husk|Dust|Dry grip
ocelot|Ambush|Sidestep
panda|Roll|Steady seat
parrot|Imitation|Targeted warning
pufferfish|Prickles|Sudden inflation
polar_bear|Cover me|Paw swipe
snow_golem|Snow flurry|Cool down
strider|Burning passage|Evacuation
turtle|Forward shell|Lockdown
villager|Makeshift shelter|Distribution
wolf|Interception|Pursuit
bogged|Poison arrow|Low shot
parched|Exhausting bolt|Reserve fire
allay|Aerial delivery|Recovery
axolotl|Play dead|Convalescence
blaze|Salvo|Heat curtain
breeze|Gust|Deflection
camel_husk|Dusty crossing|Extraction
creeper|Detonation|Ambush
endermite|Swap|Detour
guardian|Focused beam|Sweep
hoglin|Gore|Barrage
magma_cube|Burning landing|Warm core
mooshroom|Shared soup|Spore veil
nautilus|Air bell|Shared shell
phantom|Dive|Rescue wing
piglin|Armed feint|Covered retreat
pillager|Covering fire|Crossfire
silverfish|Cling|Wall probe
sulfur_cube|Acid puddle|Splash
trader_llama|Guard circle|Rally
wandering_trader|Quiet escape|Emergency cache
zombie_horse|Stubborn road|Return kick
zombie_villager|Vigil|Relief
zombified_piglin|Designation|Shared retaliation
stray|Cold arrow|Frost line
vindicator|Cleave|Guard breaker
zoglin|Continuous charge|Turnaround
creaking|Rooting|Sentry
elder_guardian|Sanctuary|Recoil wave
enderman|Extraction|Passage
evoker|Jaws|Fang circle
ghast|Explosive fireball|Barrage fire
happy_ghast|Hoist|Landing
iron_golem|Interposition|Uppercut
piglin_brute|Parry and riposte|Challenge
ravager|Ram|Stomp
shulker|Rising bullet|Shell
skeleton_horse|Spectral crossing|Shore relay
sniffer|Defensive tilling|Scent
vex|Spectral breakthrough|Lightning return
witch|Precise throw|Mist
wither_skeleton|Funereal slash|Funereal guard
zombie_nautilus|Ascent|Mooring
ender_dragon|Dragon breath|Protective wings
warden|Sonic wave|Deep listening
wither|Three skulls|Reaping
"""
def compile_catalogue():
rows = {parts[0]: parts[1:] for line in ROWS.strip().splitlines() if (parts := line.split())}
names = {parts[0]: parts[1:] for line in EN_NAMES.strip().splitlines() if (parts := line.split('|'))}
source = (ROOT / 'docs/familiar-combat-design.md').read_text()
entries = {}
for line in source.splitlines():
match = re.match(r'\| (\d+) · .*?\(`([^`]+)`\)', line)
if not match:
continue
fields = [part.strip() for part in line.split('|')[1:-1]]
entries[match[2]] = fields
registry = (ROOT / 'mods/sanctuary/src/main/java/fr/koka/sanctuary/companion/CompanionType.java').read_text()
rarity = {name.lower(): group.lower() for name, group in re.findall(r'^ (\w+)\(Rarity\.(\w+),', registry, re.M)}
assert set(rows) == set(entries) == set(rarity) == set(names) and len(rows) == 88
tanks = set('cow sheep armadillo panda polar_bear turtle drowned hoglin nautilus creaking elder_guardian iron_golem piglin_brute ravager zombie_nautilus'.split())
fragile = set('bat rabbit cod tadpole tropical_fish bee parrot allay endermite silverfish vex'.split())
ranged = {'ARROW','SPIT','SNOWBALL','FIREBALL','WIND','SKULL','BEAM','FANGS','POTION'}
# Parameters are reviewable data, bounded again on the server at load time.
result = []
for species, actions in rows.items():
fields = entries[species]
role = re.search(r'\*\*(.*?)\*\*', fields[1])[1].rstrip('.')
techniques = []
for index, action in enumerate(actions):
auto = index == 0
title_fr = 'Initiative' if auto else re.search(r'\*\*(.*?)\*\*', fields[index+1])[1]
title_en = 'Initiative' if auto else names[species][index-1]
desc_fr = fields[1] if auto else fields[index+1]
desc_fr = re.sub(r'\*\*.*?\*\*\s*:?\s*', '', desc_fr).strip()
power = 1.5 if species in fragile else 2.5 if species in tanks else 2
if not auto: power *= 2
cooldown = (60 if species in tanks else 40) if auto else (360 if index == 1 else 420)
windup = (10 if action in ranged else 5) if auto else 20
if species in {'ghast','ravager','ender_dragon','warden','wither'}:
power += 1; cooldown = 80 if auto else 600; windup = 25 if auto else 40
value = dict(action=action,nameFr=title_fr,nameEn=title_en,helpFr=desc_fr,
helpEn='',cooldown=cooldown,windup=windup,range=12 if action in ranged else 8,
power=power,duration=40 if auto else 80,radius=1.5 if auto else 3,
resource='',effect='',count=1)
if action == 'STRIKE': value['range'] = 1.7; value['radius'] = 1
if action == 'ARROW': value['resource'] = 'arrow'
if action == 'SNOWBALL': value['resource'] = 'snowball'
if action == 'POTION': value['resource'] = 'splash_potion'
if action == 'HEAL': value['resource'] = 'food'; value['power'] = 4
if action == 'FOOD': value['resource'] = 'offhand'; value['count'] = 1 if species == 'donkey' else 4
if action == 'DELIVERY': value['resource'] = 'offhand'; value['count'] = 4
if action == 'CLEANSE': value['resource'] = 'milk'
if action == 'GUARD': value['power'] = 2 if auto else 6; value['duration'] = 40 if auto else 100
if action == 'GUARD' and species == 'villager': value['resource'] = 'shield'
if action == 'PLATFORM' and species == 'sheep': value['resource'] = 'wool'
if action == 'ZONE': value['power'] = 1; value['duration'] = 100
if action == 'WATER': value['power'] = 10 if auto else 30
if action in {'MARK','TRACK','DECOY'}: value['duration'] = 40 if auto else 100
if action in {'PUSH','PULL','REBOUND','WIND'}: value['power'] = .25 if auto else .7
if species in {'bee','cave_spider','pufferfish','bogged'} and action in {'STRIKE','ARROW','ZONE'}: value['effect'] = 'poison'
if species in {'husk','parched'} and not auto: value['effect'] = 'weakness'
if species == 'stray': value['effect'] = 'slowness'
if species == 'wither_skeleton' and index == 1 or species == 'wither': value['effect'] = 'wither'
if species == 'shulker': value['effect'] = 'levitation'
if species in {'blaze','magma_cube'}: value['effect'] = 'fire'
if species == 'sulfur_cube': value['effect'] = 'acid'
if species == 'ghast': value['effect'] = 'explosive'; value['windup'] += 10
if species == 'creeper': value['windup'] = 30 if index == 1 else 40; value['power'] = 6; value['radius'] = 3
if index and species in {'llama','trader_llama','blaze','pillager','snow_golem','wither'}:
value['count'] = 3 if species != 'llama' or index == 2 else 1
if index == 2 and species in {'vindicator','polar_bear','evoker','guardian','ghast'}: value['count'] = 3
if species == 'turtle' and index == 2: value['power'] = 10; value['effect'] = 'stationary'
if species == 'armadillo': value['effect'] = 'projectile'
if species == 'happy_ghast' and action == 'GUARD': value['effect'] = 'overhead'
if species == 'breeze' and action == 'GUARD': value['effect'] = 'deflect'
if species == 'cow' and action == 'GUARD' or species == 'iron_golem' and action == 'GUARD': value['effect'] = 'share_health'
if species == 'strider': value['effect'] = 'lava'
if species == 'skeleton_horse': value['effect'] = 'water'
if species == 'witch' and action == 'CLOUD': value['resource'] = 'splash_potion'; value['effect'] = 'potion'
if species == 'wandering_trader' and action == 'VANISH': value['resource'] = 'invisibility'
if species == 'axolotl' and action == 'VANISH': value['effect'] = 'play_dead'
if species == 'spider' and index == 2: value['effect'] = 'from_above'
if species == 'ocelot' and index == 1: value['effect'] = 'flank'
if species in {'endermite','vex'} and action == 'CHARGE': value['effect'] = 'phase'
if species == 'wither' and index == 2: value['effect'] = 'lifesteal'; value['count'] = 1
value['helpEn'] = f"{title_en}: {action.lower().replace('_',' ')} from the companion. Preparation {value['windup']/20:g}s; recovery {value['cooldown']/20:g}s."
techniques.append(value)
result.append(dict(schema=1,id='sanctuary:'+species,revision=1,entity='minecraft:'+species,
rarity=rarity[species],health=24 if species in tanks else 10 if species in fragile else 16,
speed=.23 if species in tanks else .34 if species in fragile else .28,
distance=7 if actions[0] in ranged else 3,initiative=techniques[0],signature=techniques[1],variant=techniques[2],
legacyWork=species,roleFr=role,roleEn=actions[0].lower().replace('_',' ')))
target=ROOT/'mods/sanctuary/src/main/resources/data/sanctuary/sanctuary_familiars/vanilla.json'
target.parent.mkdir(parents=True,exist_ok=True)
target.write_text(json.dumps(dict(schema=1,profiles=result),ensure_ascii=False,indent=2)+'\n')
print(f'{len(result)} profiles compiled')
if __name__ == '__main__':
compile_catalogue()