#!/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/.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. Needs the Cobblemon jar in build/server/mods (any test-server run) for the guardians' types. Usage: tools/zone_bosses.py """ import json import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent DATA = ROOT / "datapacks/core/data/cobblesurvival" OUT = DATA / "bosses" MODS = ROOT / "build/server/mods" 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 # 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} heights[d["name"].lower()] = (d.get("hitbox") or {}).get("height", 1) * d.get("baseScale", 1) return types, heights def main(): jar = next(MODS.glob("Cobblemon-*.jar")) types, heights = species_data(jar) 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") if __name__ == "__main__": main()