Files
cobblemon-survival-pack/tools/zone_bosses.py
T
claudeandClaude Opus 5.5 4083791944 Phase 9: reproducible builds, balance from the server, spark check, play feedback
Build: every tool pinned (JDK, packwiz, Gradle plugins, Python packages), exports
normalised, tools/check_reproducible.py proves two clean builds are byte-identical.

Server-side balance: a server can load config/cobblesurvival/datapack.zip in place of
the jar's data, /reload applies it, tools/server_update.py packages it and refuses
when a client change is needed. JEI drop pages are sent by the server. Code-held
balance numbers moved to tuning/server.json.

Performance: spark in the pack (server only), tools/perf_test.py with fake players
whose chunks really are active, docs/performance.md.

Adrian's feedback: default keys R, U, J, §; drop pages link to Cobblemon Info and show
real chances; bosses only hurt in battles; battle XP halved; channels between the
islands low level; temperature ×3 with insulation doubled; hazards start at once; no
item dots on the map; a wild attack starts a battle; the starter only faints; quest
book on join with a basics page and an HMs page; regional forms obtainable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 16:26:30 +01:00

108 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Write the boss fights (Phase 7; Adrian, 2026-11-02: every zone guardian gets area attacks, more and harder in
harder zones; Eternatus replaces the Ender Dragon).
For every guardian in datapacks/core/data/cobblesurvival/guardians/ (tools/zone_spawns.py) this writes
datapacks/core/data/cobblesurvival/bosses/<zone>.json, and bosses/end.json for the End. The tier comes from the
guardian's level (TIERS): which patterns it uses, how long they telegraph, how often they come; damage grows with
level; a Fire, Ice or Poison guardian burns, freezes or poisons. Placeholder numbers, see docs/balance.md.
Usage: tools/zone_bosses.py
"""
import json
import zipfile
from pathlib import Path
import lock
ROOT = Path(__file__).resolve().parent.parent
DATA = ROOT / "datapacks/core/data/cobblesurvival"
OUT = DATA / "bosses"
ARENA = 24
ARENA_BY_ZONE = {"basalt": 32}
END = {"species": "eternatus", "level": 80, "respawn_hours": 5, "xp_first_win": 12000, "xp": 500}
# Adrian, 2026-11-02: Eternatus was too small (0.2x, about a block). Scaled to stand this many blocks tall.
END_HEIGHT = 15
END_ARENA = 40
# A summoned legendary's fight (world/summons.json; Adrian, 2026-11-09): catchable, same rules as a boss.
SUMMON_ARENA = 32
# Up to this level: these patterns.
TIERS = [
(20, ["circle"]),
(35, ["circle", "line"]),
(50, ["circle", "line", "cone"]),
(65, ["circle", "line", "cone", "ring"]),
(999, ["circle", "line", "cone", "ring", "wide_ring"]),
]
def pattern(name, tier, damage, effect):
telegraph = 50 - 5 * tier
base = {"telegraph_ticks": telegraph, "damage": damage, "effect": effect}
if name == "circle":
return {"shape": "circle", "radius": 2.5 + 0.25 * tier, "aim": "player", **base,
"trigger": {"every_ticks": 160 - 12 * tier, "offset": 40}}
if name == "line":
return {"shape": "line", "length": 14, "width": 2, "aim": "player", **base,
"trigger": {"every_ticks": 220 - 15 * tier, "offset": 100}}
if name == "cone":
return {"shape": "cone", "length": 10, "angle": 70, "aim": "player", **base,
"trigger": {"every_ticks": 260 - 15 * tier, "offset": 160}}
if name == "ring":
return {"shape": "ring", "inner": 3, "outer": 9, "aim": "boss", **base, "trigger": {"below_health": 0.5}}
if name == "wide_ring":
return {"shape": "ring", "inner": 2, "outer": 12, "aim": "boss", **base, "trigger": {"below_health": 0.25}}
raise ValueError(name)
def boss(level, types, arena, **extra):
tier = next(i for i, (top, _) in enumerate(TIERS) if level <= top)
effect = "fire" if "fire" in types else "ice" if "ice" in types else "poison" if "poison" in types else "none"
damage = max(2, round(level / 6))
return {**extra, "arena": arena, "patterns": [pattern(name, tier, damage, effect) for name in TIERS[tier][1]]}
def species_data(jar):
"""({species: its types}, {species: how tall it stands at scale 1, in blocks})."""
types, heights = {}, {}
with zipfile.ZipFile(jar) as z:
for name in z.namelist():
if name.startswith("data/cobblemon/species/") and name.endswith(".json"):
d = json.loads(z.read(name))
types[d["name"].lower()] = {t for t in (d.get("primaryType"), d.get("secondaryType")) if t}
types[Path(name).stem] = types[d["name"].lower()]
heights[d["name"].lower()] = (d.get("hitbox") or {}).get("height", 1) * d.get("baseScale", 1)
return types, heights
def main():
types, heights = species_data(lock.mod_jar("cobblemon"))
OUT.mkdir(parents=True, exist_ok=True)
for old in OUT.glob("*.json"):
old.unlink()
comment = "Generated by tools/zone_bosses.py; do not edit by hand. See docs/design.md (Phase 7) for the format."
for path in sorted((DATA / "guardians").glob("*.json")):
guardian = json.loads(path.read_text())
species, *props = guardian["pokemon"].split()
level = int(next(p.split("=")[1] for p in props if p.startswith("level=")))
zone = guardian["zone"]
data = boss(level, types.get(species, set()), ARENA_BY_ZONE.get(zone, ARENA), guardian=zone)
(OUT / f"{zone}.json").write_text(json.dumps({"_comment": comment, **data}, indent=1) + "\n")
print(f"{zone}: {species} Lv {level}, {len(data['patterns'])} patterns")
end_def = {**END, "scale": round(END_HEIGHT / heights[END["species"]], 2)}
end = boss(END["level"], types.get(END["species"], set()), END_ARENA, end=end_def)
(OUT / "end.json").write_text(json.dumps({"_comment": comment, **end}, indent=1) + "\n")
print(f"end: {END['species']} Lv {END['level']}, {len(end['patterns'])} patterns")
table = json.loads((ROOT / "world/summons.json").read_text())
for species, s in table["summons"].items():
fight = boss(table["tiers"][str(s["tier"])]["level"], types.get(s.get("species", species), set()), SUMMON_ARENA, summon=species)
(OUT / f"summon_{species}.json").write_text(json.dumps({"_comment": comment, **fight}, indent=1) + "\n")
print(f"{len(table['summons'])} summoned legendaries")
if __name__ == "__main__":
main()