Phase 3: strong flyers only spawn in high-level zones
- tools/zone_spawns.py: a species ridable through the air with top speed >= 60 or top stamina >= 85 never spawns in a zone whose band starts below Lv 30; lines that start as one are only placed in such zones, and the tool refuses to write pools that break the rule. Drampa moves to Sunscorch; Aerodactyl is fossil-only; Salamence never spawns wild (Bagon and Shelgon stay in the Dripstone caves). - Ride stats stay Cobblemon's until play-tested (species_additions can override riding later). - Phase 3 checklist: all steps in, approval checkpoint for Adrian. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
173203eeb5
commit
55c336f89e
12 files changed
+2115
-576
No files matched your search
+47
-13
@@ -21,7 +21,8 @@ datapacks/core/data/cobblesurvival/spawn_pool_world/<zone>.json:
|
||||
doesn't spawn there. If no stage fits, the closest one spawns across the whole band.
|
||||
3. world/zones.json → "species" pins a line to a zone by hand: {"pikachu": "southwold"}.
|
||||
4. In cave zones, sky-light minimums, "can see sky" and the "natural" ground preset are dropped.
|
||||
5. Each zone with a "guardian" gets datapacks/core/data/cobblesurvival/guardians/<zone>.json: the
|
||||
5. Strong flyers (STRONG_FLYER_*) only spawn in zones whose band starts at HIGH_ZONE_LEVEL or more.
|
||||
6. Each zone with a "guardian" gets datapacks/core/data/cobblesurvival/guardians/<zone>.json: the
|
||||
strongest Pokémon spawning in the zone (or "species"), uncatchable, half again as big, five levels
|
||||
above the band, with the zone's boss drops and world/zones.json → guardian_respawn_hours.
|
||||
|
||||
@@ -54,6 +55,13 @@ MIN_CELLS = 5 # a zone "has" a biome when at least this many grid cells of it l
|
||||
# tags): its biomes say nothing about a home, so it only goes to a land zone, by strength. Its cave and
|
||||
# water entries still spawn underground and in the rivers of that zone.
|
||||
ALL_ROUNDER = 0.8
|
||||
# Roadmap Phase 3: strong flyers only spawn in high-level zones. A species you can ride through the air with a top
|
||||
# flying speed or stamina this high never spawns in a zone whose band starts below HIGH_ZONE_LEVEL (its weaker
|
||||
# pre-evolutions still do), and a line that starts as one is placed in such a zone only; if none fits its habitat,
|
||||
# it doesn't spawn in the wild.
|
||||
STRONG_FLYER_SPEED = 60
|
||||
STRONG_FLYER_STAMINA = 85
|
||||
HIGH_ZONE_LEVEL = 30
|
||||
|
||||
|
||||
def cobblemon_jar():
|
||||
@@ -98,16 +106,23 @@ def zone_biomes(plan, layout, preview):
|
||||
return shares
|
||||
|
||||
|
||||
def base_stat_totals(jar):
|
||||
"""{species id: base-stat total} from Cobblemon's species files."""
|
||||
totals = {}
|
||||
def species_data(jar):
|
||||
"""({species id: base-stat total}, {species ids that are strong flyers}) from Cobblemon's species files."""
|
||||
totals, flyers = {}, set()
|
||||
with zipfile.ZipFile(jar) as z:
|
||||
for name in z.namelist():
|
||||
m = re.fullmatch(r"data/cobblemon/species/[^/]+/([^/]+)\.json", name)
|
||||
if m:
|
||||
stats = json.loads(z.read(name)).get("baseStats", {})
|
||||
totals[f"cobblemon:{m[1]}"] = sum(stats.values())
|
||||
return totals
|
||||
if not m:
|
||||
continue
|
||||
data = json.loads(z.read(name))
|
||||
totals[f"cobblemon:{m[1]}"] = sum(data.get("baseStats", {}).values())
|
||||
air = ((data.get("riding") or {}).get("behaviours") or {}).get("AIR") or {}
|
||||
stats = air.get("stats", {})
|
||||
def top(stat):
|
||||
return int(str(stats.get(stat, "0")).split("-")[-1])
|
||||
if air and (top("SPEED") >= STRONG_FLYER_SPEED or top("STAMINA") >= STRONG_FLYER_STAMINA):
|
||||
flyers.add(f"cobblemon:{m[1]}")
|
||||
return totals, flyers
|
||||
|
||||
|
||||
def load_before(zone, room, stretch, target):
|
||||
@@ -132,7 +147,7 @@ def main():
|
||||
species = guide["species"]
|
||||
jar = cobblemon_jar()
|
||||
entries = load_entries(jar)
|
||||
bst = base_stat_totals(jar)
|
||||
bst, strong_flyers = species_data(jar)
|
||||
|
||||
zones = {z["id"]: z for z in plan["zones"]} | {c["id"]: c for c in plan["caves"]}
|
||||
shares = zone_biomes(plan, layout, preview)
|
||||
@@ -161,6 +176,10 @@ def main():
|
||||
lines[line_root(member, species)].append((entry, set(s["biomes"]), member, s["level"]))
|
||||
|
||||
pinned = {f"cobblemon:{k}" if ":" not in k else k: v for k, v in plan.get("species", {}).items()}
|
||||
flying_lines = {root for root in lines if root in strong_flyers}
|
||||
|
||||
def allowed(root, zid):
|
||||
return root not in flying_lines or zones[zid]["levels"][0] >= HIGH_ZONE_LEVEL
|
||||
wanted = {zid: z.get("lines", 20) for zid, z in zones.items()}
|
||||
scale = len(lines) / sum(wanted.values())
|
||||
target = {zid: n * scale for zid, n in wanted.items()}
|
||||
@@ -191,7 +210,7 @@ def main():
|
||||
fit = 1 - abs(rank[root] - tier[zid])
|
||||
return habitat * specific ** 0.5 * fit ** 2 / (1 + load[zid] / target[zid])
|
||||
|
||||
candidates = [zid for zid in zones if line_biomes & biomes_of[zid]]
|
||||
candidates = [zid for zid in zones if line_biomes & biomes_of[zid] and allowed(root, zid)]
|
||||
if candidates:
|
||||
home[root] = max(candidates, key=score)
|
||||
load[home[root]] += 1
|
||||
@@ -206,14 +225,21 @@ def main():
|
||||
for root in sorted(all_rounders, key=lambda r: rank[r]):
|
||||
while load[current] >= load_before(current, room, stretch, target) and current != by_tier[-1]:
|
||||
current = next(zone_iter)
|
||||
home[root] = current
|
||||
load[current] += 1
|
||||
# A strong flyer skips ahead to the first high zone.
|
||||
home[root] = current if allowed(root, current) else next((z for z in by_tier if allowed(root, z)), None)
|
||||
if home[root] is None:
|
||||
del home[root]
|
||||
continue
|
||||
load[home[root]] += 1
|
||||
|
||||
pools = collections.defaultdict(list)
|
||||
report = collections.defaultdict(list)
|
||||
for root, zid in home.items():
|
||||
lo, hi = zones[zid]["levels"]
|
||||
usable = [(e, b, m, lv) for e, b, m, lv in lines[root] if b & biomes_of[zid]]
|
||||
high = zones[zid]["levels"][0] >= HIGH_ZONE_LEVEL
|
||||
usable = [(e, b, m, lv) for e, b, m, lv in lines[root] if b & biomes_of[zid] and (high or m not in strong_flyers)]
|
||||
if not usable:
|
||||
continue
|
||||
fitting = [(e, m, max(lv[0], lo), min(lv[1], hi)) for e, b, m, lv in usable if lv[0] <= hi and lv[1] >= lo]
|
||||
if not fitting:
|
||||
# No stage reaches the band: the closest stage spawns across all of it.
|
||||
@@ -238,6 +264,11 @@ def main():
|
||||
names = sorted({species[m]["name"] for _, m, _, _ in fitting})
|
||||
report[zid].append((species.get(root, {}).get("name", root), names))
|
||||
|
||||
low_flyers = [(zid, s["pokemon"]) for zid, spawns in pools.items() for s in spawns
|
||||
if zones[zid]["levels"][0] < HIGH_ZONE_LEVEL and f"cobblemon:{s['pokemon'].split()[0].lower()}" in strong_flyers]
|
||||
if low_flyers:
|
||||
sys.exit(f"strong flyers would spawn below Lv {HIGH_ZONE_LEVEL}: {low_flyers[:5]}")
|
||||
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
for old in OUT.glob("*.json"):
|
||||
old.unlink()
|
||||
@@ -292,6 +323,9 @@ def main():
|
||||
REPORT.write_text("\n".join(lines_out) + "\n")
|
||||
|
||||
unplaced = [r for r in lines if r not in home]
|
||||
grounded = sorted(species.get(r, {}).get("name", r) for r in unplaced if r in flying_lines)
|
||||
if grounded:
|
||||
print(f"strong flyers kept out of the wild (no high zone fits their habitat): {', '.join(grounded)}")
|
||||
for zid, zone in zones.items():
|
||||
print(f" {zone['name']:18s} {len(report.get(zid, [])):3d} lines {len(pools.get(zid, [])):4d} spawn entries")
|
||||
print(f"{len(home)} lines placed, {len(unplaced)} with no matching zone; wrote {OUT.relative_to(ROOT)} and {REPORT.relative_to(ROOT)}")
|
||||
|
||||
Reference in new issue
Block a user