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:
commit
398301f0be
232 files changed
+72665
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user