Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards

Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus
the Kotlin companion mod (cobblesurvival) and its built-in data pack.

- Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and
  export tooling, Prism instance sync, test server with smoke checks.
- Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26),
  Adrian's biome list, no natural ore, structures without ore or vanilla
  mobs, no Nether portals, world template builder and world report.
- Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap
  fixes), Revival Charm, expensive Healing Machine, Lootr refreshing
  loot, in-game self-tests on Minecraft's GameTest framework.
- Phase 3 so far: 14 zones with their own spawn pools and level bands
  (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition),
  Cold Sweat heat and cold as gates with boss-drop insulation recipes,
  spawn survey and hazard self-tests in tools/quick_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-29 19:01:21 +01:00
commit 398301f0be
232 files changed
+72665

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
"""Item icons rendered from the jars' item models into build/field-guide/icons/.
Flat items composite their layer textures; block items get a small isometric
cube from their block model's faces; cross-shaped plants use their texture.
The PNGs are third-party assets (Cobblemon's assets are not MPL-licensed), so
they only ever go to build/ and are never committed.
"""
import io
from PIL import Image, ImageEnhance
from .datasource import split_id
S = 16
FACES = { # texture keys tried for each visible face of a cube
"top": ("up", "top", "end", "all", "texture", "side", "particle"),
"left": ("south", "front", "side", "all", "texture", "north", "particle"),
"right": ("east", "side", "all", "texture", "west", "particle"),
}
FLAT_PARENTS = ("item/generated", "item/handheld", "item/handheld_rod", "builtin/generated")
CROSS_PARENTS = ("block/cross", "block/tinted_cross", "block/flower_pot_cross", "block/crop")
FOLIAGE = (72, 181, 24)
class Icons:
def __init__(self, stack, out_dir):
self.stack, self.out = stack, out_dir
self.models = {}
self.missing = []
def model(self, mid):
"""Merged model: (parent chain ids, textures, elements)."""
if mid in self.models:
return self.models[mid]
ns, path = split_id(mid)
d = self.stack.json(f"assets/{ns}/models/{path}.json") or {}
chain, textures, elements = [f"{ns}:{path}"], {}, d.get("elements")
parent = d.get("parent")
if parent and len(chain) < 20:
pchain, ptex, pel = self.model(parent if ":" in parent else "minecraft:" + parent)
chain += pchain
textures.update(ptex)
elements = elements or pel
textures.update(d.get("textures", {}))
self.models[mid] = (chain, textures, elements)
return self.models[mid]
def texture(self, ref, textures, depth=0):
while isinstance(ref, str) and ref.startswith("#") and depth < 10:
ref, depth = textures.get(ref[1:]), depth + 1
if not isinstance(ref, str):
return None
ns, path = split_id(ref)
raw = self.stack.read(f"assets/{ns}/textures/{path}.png")
if raw is None:
return None
img = Image.open(io.BytesIO(raw)).convert("RGBA")
if img.height > img.width: # animated strip: first frame
img = img.crop((0, 0, img.width, img.width))
return img.resize((S, S), Image.NEAREST) if img.size != (S, S) else img
def render(self, iid):
ns, path = split_id(iid)
chain, tex, elements = self.model(f"{ns}:item/{path}")
if len(chain) == 1 and not tex:
chain, tex, elements = self.model(f"{ns}:block/{path}")
if len(chain) == 1 and not tex: # no model at all: KubeJS items get item/generated with their own texture
chain, tex = [f"{ns}:item/{path}", "minecraft:item/generated"], {"layer0": f"{ns}:item/{path}"}
names = [c.split(":", 1)[1] for c in chain]
if any(n in FLAT_PARENTS for n in names) or any(k.startswith("layer") for k in tex):
layers = sorted((k for k in tex if k.startswith("layer")), key=lambda k: int(k[5:] or 0))
img = None
for k in layers:
t = self.texture(tex[k], tex)
if t is not None:
img = t if img is None else Image.alpha_composite(img, t)
return img.resize((32, 32), Image.NEAREST) if img else None
if any(n in CROSS_PARENTS for n in names):
t = self.texture(tex.get("cross") or tex.get("crop") or tex.get("plant") or tex.get("particle"), tex)
return t.resize((32, 32), Image.NEAREST) if t else None
faces = {}
for face, keys in FACES.items():
for k in keys:
if k in tex:
faces[face] = self.texture(tex[k], tex)
if faces[face] is not None:
break
if not any(v is not None for v in faces.values()):
return None
tinted = any(n in ("block/leaves",) for n in names) or path.endswith("leaves")
return iso_cube(faces, tinted)
def write_all(self, ids):
out = {}
for iid in sorted(ids):
try:
img = self.render(iid)
except (OSError, ValueError) as e:
self.missing.append(f"{iid}: {e}")
img = None
if img is None:
self.missing.append(iid)
continue
ns, path = split_id(iid)
rel = f"icons/{ns}/{path}.png"
dst = self.out / rel
dst.parent.mkdir(parents=True, exist_ok=True)
img.save(dst)
out[iid] = rel
return out
def _tint(img, rgb):
r, g, b, a = img.split()
grey = img.convert("L")
return Image.merge("RGBA", [grey.point(lambda v, c=c: v * c // 255) for c in rgb] + [a])
def iso_cube(faces, tinted=False):
"""32x32 isometric block icon from top/left/right 16x16 face textures."""
canvas = Image.new("RGBA", (32, 32), (0, 0, 0, 0))
geometry = { # origin, u axis, v axis (output pixels per texture pixel)
"top": ((16, 0), (1, 0.5), (-1, 0.5)),
"left": ((0, 8), (1, 0.5), (0, 1)),
"right": ((16, 16), (1, -0.5), (0, 1)),
}
shade = {"top": 1.0, "left": 0.8, "right": 0.62}
for face in ("top", "left", "right"):
tex = faces.get(face) or faces.get("top") or next(v for v in faces.values() if v is not None)
if tinted:
tex = _tint(tex, FOLIAGE)
(ox, oy), (ux, uy), (vx, vy) = geometry[face]
det = ux * vy - uy * vx
# inverse affine: texture coords from output coords
a, b = vy / det, -vx / det
d, e = -uy / det, ux / det
c, f = -(a * ox + b * oy), -(d * ox + e * oy)
layer = tex.transform((32, 32), Image.AFFINE, (a, b, c, d, e, f), resample=Image.NEAREST,
fillcolor=(0, 0, 0, 0))
if shade[face] != 1.0:
rgb = ImageEnhance.Brightness(layer.convert("RGB")).enhance(shade[face])
layer = Image.merge("RGBA", (*rgb.split(), layer.split()[3]))
canvas = Image.alpha_composite(canvas, layer)
return canvas