Phase 4 play-test fixes: deep caves, fuller cave spawns, strays, shared loot
From Adrian's play-test (2026-10-30): - The Deep Dark no longer fills the deepslate layer: world/biomes.json splits it by temperature (Deep Dark under cold and temperate land, basalt deltas under the hottest, ordinary caves between); at y -52 it drops from 30 % to 14 % of the land, basalt 7 %. - Cave zones fill up to their targets with lines whose primary type fits the cave; top stages spawn up to the band's top (Deep Dark Lv 60, Basalt 70); no time-of-day conditions underground. - Wild Pokemon more than 10 levels above the band of the zone they wander into despawn (zone/Wanderers.kt). - Loot is shared: all players in one scoreboard team, Lootr team_loot. - Basalt ore nodes reach the deep basalt. Self-tests strays_despawn and loot_is_shared; the catching rules from the previous commit are now tested too. Template rebuilt (world check passed), island quick check passed (36 self-tests and the restart test). Shoulder Pokemon noted for Phase 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6361636da7
commit
d49684a2eb
40 files changed
+8212
-9412
No files matched your search
+44
-8
@@ -13,14 +13,18 @@ datapacks/core/data/cobblesurvival/spawn_pool_world/<zone>.json:
|
||||
level band, divided by how full the zone already is against its "lines" target. So strong
|
||||
lines (pseudo-legendaries) end up in high-level zones. Legendary, mythical, Ultra Beast
|
||||
and Paradox Pokémon are left out. Lines that can spawn in nearly every biome (Cobblemon's
|
||||
catch-all tags) are placed last: by strength into the land zones, weakest first, filling
|
||||
each zone up to its target.
|
||||
catch-all tags) are placed last: first into the cave zones, which take those whose first stage's
|
||||
primary type fits the cave (world/zones.json → caves → "types") up to their target, the ones closest to the
|
||||
cave's level first; the rest by strength into the land zones, weakest first, filling each zone
|
||||
up to its target.
|
||||
2. In its home zone a line keeps Cobblemon's spawn entries (bucket, weight, time, light,
|
||||
presets) for the biomes the zone has, plus "zone": "<id>". Levels are clamped into the
|
||||
zone's band, so the band picks the evolution stages: a stage whose levels miss the band
|
||||
doesn't spawn there. If no stage fits, the closest one spawns across the whole band.
|
||||
doesn't spawn there. If no stage fits, the closest one spawns across the whole band. The
|
||||
line's top stage spawns up to the top of the band (Cobblemon's own ranges stop earlier).
|
||||
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.
|
||||
4. In cave zones every entry spawns in the cave's biome, and sky-light minimums, "can see sky", time
|
||||
of day, excluded biomes and the "natural" ground preset are dropped (there's no sky or day underground).
|
||||
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
|
||||
@@ -215,14 +219,28 @@ def main():
|
||||
home[root] = max(candidates, key=score)
|
||||
load[home[root]] += 1
|
||||
|
||||
# 2. All-rounders fill the land zones up to their targets, weakest lines into the lowest zones.
|
||||
# 2. Cave zones take the all-rounders of the types that fit them, up to their targets, the ones closest
|
||||
# to the cave's level first (Adrian, 2026-10-30: the deep caves' tables were too small).
|
||||
# A line's type here is its first stage's primary type (a line of many types, like Eevee's, shouldn't count as all of them).
|
||||
primary_type = {root: (species.get(root, {}).get("types") or [None])[0] for root in lines}
|
||||
for zid in sorted(cave_zones, key=lambda z: tier[z]):
|
||||
fits = set(zones[zid].get("types", []))
|
||||
while load[zid] < target[zid]:
|
||||
pool = [r for r in all_rounders if r not in home and primary_type[r] in fits and allowed(r, zid)]
|
||||
if not pool:
|
||||
break
|
||||
root = min(pool, key=lambda r: (abs(rank[r] - tier[zid]), r))
|
||||
home[root] = zid
|
||||
load[zid] += 1
|
||||
|
||||
# 3. The other all-rounders fill the land zones up to their targets, weakest lines into the lowest zones.
|
||||
by_tier = sorted(land_zones, key=lambda z: tier[z])
|
||||
room = {z: max(0.0, target[z] - load[z]) for z in by_tier}
|
||||
total_room = sum(room.values()) or 1
|
||||
stretch = max(1.0, len(all_rounders) / total_room)
|
||||
zone_iter = iter(by_tier)
|
||||
current = next(zone_iter)
|
||||
for root in sorted(all_rounders, key=lambda r: rank[r]):
|
||||
for root in sorted((r for r in all_rounders if r not in home), key=lambda r: rank[r]):
|
||||
while load[current] >= load_before(current, room, stretch, target) and current != by_tier[-1]:
|
||||
current = next(zone_iter)
|
||||
# A strong flyer skips ahead to the first high zone.
|
||||
@@ -234,17 +252,24 @@ def main():
|
||||
|
||||
pools = collections.defaultdict(list)
|
||||
report = collections.defaultdict(list)
|
||||
cave_biome = {c["id"]: c["biome"] for c in plan["caves"]}
|
||||
for root, zid in home.items():
|
||||
lo, hi = zones[zid]["levels"]
|
||||
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)]
|
||||
# Underground every entry gets the cave's biome below, so any of the line's entries will do.
|
||||
usable = [(e, b, m, lv) for e, b, m, lv in lines[root]
|
||||
if (zid in cave_zones or 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]
|
||||
reaching = [u for u in usable if u[3][0] <= hi and u[3][1] >= lo]
|
||||
top = max(reaching, key=lambda u: u[3][1])[2] if reaching else None
|
||||
# The top stage spawns up to the band's top; Cobblemon's own ranges often stop well below it.
|
||||
fitting = [(e, m, max(lv[0], lo), hi if m == top else min(lv[1], hi)) for e, b, m, lv in reaching]
|
||||
if not fitting:
|
||||
# No stage reaches the band: the closest stage spawns across all of it.
|
||||
e, b, m, lv = min(usable, key=lambda u: min(abs(u[3][0] - hi), abs(u[3][1] - lo)))
|
||||
fitting = [(e, m, lo, hi)]
|
||||
seen = set()
|
||||
for entry, member, a, b in fitting:
|
||||
spawn = copy.deepcopy(entry)
|
||||
spawn["id"] = f"{zid}-{entry.get('id') or member.split(':')[1]}-{len(pools[zid])}"
|
||||
@@ -260,6 +285,17 @@ def main():
|
||||
# "natural" only allows natural ground; cave floors (basalt deltas: magma, smooth basalt) often aren't.
|
||||
if "natural" in spawn.get("presets", []):
|
||||
spawn["presets"] = [x for x in spawn["presets"] if x != "natural"]
|
||||
# The zone condition places it; the cave's biome stands in for Cobblemon's surface biomes, and
|
||||
# there's no day or night underground.
|
||||
condition["biomes"] = [cave_biome[zid]]
|
||||
condition.pop("timeRange", None)
|
||||
spawn.get("anticondition", {}).pop("biomes", None)
|
||||
if spawn.get("anticondition") == {}:
|
||||
del spawn["anticondition"]
|
||||
key = (spawn["pokemon"], spawn.get("spawnablePositionType"), spawn.get("bucket"), spawn["level"])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
pools[zid].append(spawn)
|
||||
names = sorted({species[m]["name"] for _, m, _, _ in fitting})
|
||||
report[zid].append((species.get(root, {}).get("name", root), names))
|
||||
|
||||
Reference in new issue
Block a user