Bosses (Adrian, 2026-11-02: Eternatus too small and too easy; guardians way bigger and tougher; only defensive stats up; no attack takes more than 25 %): - Guardians scale to about 8 blocks tall (1.5x-12x), Eternatus to 15 blocks (2.5x, was 0.2x). Cave guardians look for a spot with room above them. Bosses already in a world grow on the next check outside battle. - Perfect IVs and 170 EVs each in HP, Defense and Sp. Def, nothing offensive. - The Guardian's Ward, a Showdown-only held item (held_items/guardiansward.js) handed to every boss by a HeldItemProvider: Defense and Sp. Def x2 while defending, and one attack (all hits together) takes at most 25 % of max HP. - Area attacks that start at the boss reach half its width further. Machines (Adrian, 2026-11-02): a working core is active: it glows, its front animates (flickering fire on the furnace core, pulsing emblems elsewhere), and particles show in the chamber and at the core (client side). The Pokemon inside flinches about every 15 s (red flash, Cobblemon's hit sound, no damage). Roadmap Phase 8: every item that can drop gets a JEI page naming its sources. Self-tests: practice_bosses_spawn checks size and training, boss_takes_a_quarter_per_attack runs a real battle (each Lv 100 Bullet Seed takes exactly a quarter), heat_core_cooks_with_hoppers checks the active state and the flinch. Self-tests and the island check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
4.6 KiB
Python
101 lines
4.6 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.
|
|
|
|
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()
|