#!/usr/bin/env python3 """Writes the machine cores' textures, block models, item models and blockstates (Phase 6). Each core is an "orientable" block (front, side, top) facing the player who placed it. The 16x16 textures are drawn here, from a palette and an emblem per machine, so they can be redrawn after a change. Our own art (pack license). The cores are see-through (Adrian, 2026-11-02: the Pokémon in the chamber behind the core must be visible): a solid frame around tinted glass, the emblem faint on the front; the models render translucent. A working core (block state active=true; Adrian, 2026-11-02: show that something is happening, like fire in a furnace) switches to an animated front: the emblem glows and pulses, sparks drift up the glass, and the furnace's flame flickers. The core also gives off light then, and particles show (mod: MachineEffects). Usage: tools/machine_assets.py """ import json import math import random from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parent.parent ASSETS = ROOT / "mod/src/main/resources/assets/cobblesurvival" # name: (dark, mid, light, accent, emblem) CORES = { "heat_core": ((92, 34, 26), (150, 62, 44), (186, 92, 66), (255, 160, 40), "flame"), "dynamo_core": ((112, 58, 36), (184, 104, 70), (224, 150, 108), (255, 230, 70), "bolt"), "synthesis_core": ((70, 74, 80), (128, 134, 142), (180, 186, 194), (90, 220, 120), "drop"), "altar_core": ((22, 18, 26), (52, 44, 58), (84, 72, 92), (190, 60, 220), "eye"), "rebirth_core": ((70, 40, 110), (130, 90, 190), (186, 150, 236), (255, 200, 245), "star"), } EMBLEMS = { "flame": [ "......#.........", ".....##.........", ".....###..#.....", "....####.##.....", "...#########....", "...##########...", "..###++######...", "..##++++#####...", "..##+++++####...", "...##+++++##....", "....########....", ], "bolt": [ "........####....", ".......####.....", "......####......", ".....####.......", "....#######.....", ".......####.....", "......####......", ".....###........", "....##..........", "...#............", ], "drop": [ ".......#........", "......###.......", ".....#####......", "....###+###.....", "...###+++###....", "...##+++++##....", "...###+++###....", "....#######.....", ".....#####......", ], "eye": [ "................", ".....######.....", "...##......##...", "..#...####...#..", ".#...##++##...#.", "..#...####...#..", "...##......##...", ".....######.....", ], "star": [ ".......#........", ".......#........", "......###.......", "..###########...", "....#######.....", ".....#####......", "....###.###.....", "...##.....##....", ], } GLASS_ALPHA = 70 EMBLEM_ALPHA = 150 # The working front: this many frames, each shown this many ticks, blended into one another. FRAMES = 8 FRAME_TICKS = 3 FLAME_COLORS = [(255, 70, 20), (255, 140, 30), (255, 200, 60), (255, 240, 150)] def frame(image, dark, mid, light): """A two-pixel frame: light top-left edge, dark bottom-right edge, mid inner ring.""" for i in range(16): for a, b in ((i, 0), (0, i)): image.putpixel((a, b), light + (255,)) for a, b in ((i, 15), (15, i)): image.putpixel((a, b), dark + (255,)) for i in range(1, 15): for a, b in ((i, 1), (1, i), (i, 14), (14, i)): image.putpixel((a, b), mid + (255,)) def glass(dark, mid, light, accent): """Tinted glass inside the frame, with a few lighter streaks so the face still reads as a surface.""" image = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) tint = tuple((m + a) // 2 for m, a in zip(mid, accent)) for y in range(2, 14): for x in range(2, 14): streak = (x - y) in (3, 4) or (x - y) == -6 image.putpixel((x, y), (light if streak else tint) + (GLASS_ALPHA + (50 if streak else 0),)) frame(image, dark, mid, light) return image def side(dark, mid, light, accent): return glass(dark, mid, light, accent) def top(dark, mid, light, accent): image = glass(dark, mid, light, accent) for x, y in ((2, 2), (13, 2), (2, 13), (13, 13)): image.putpixel((x, y), accent + (255,)) return image def front(dark, mid, light, accent, emblem): image = glass(dark, mid, light, accent) rows = EMBLEMS[emblem] top_row = (16 - len(rows)) // 2 glow = tuple(min(255, c + 60) for c in accent) for y, row in enumerate(rows): for x, c in enumerate(row): if 2 <= x <= 13 and 2 <= top_row + y <= 13 and c in "#+": image.putpixel((x, top_row + y), (accent if c == "#" else glow) + (EMBLEM_ALPHA,)) return image def lerp(a, b, t): return tuple(round(x + (y - x) * t) for x, y in zip(a, b)) def working_front(dark, mid, light, accent, emblem, name): """The animated front of a working core: FRAMES frames stacked top to bottom.""" rng = random.Random(name) rows = EMBLEMS[emblem] top_row = (16 - len(rows)) // 2 glow = tuple(min(255, c + 60) for c in accent) white = (255, 255, 240) strip = Image.new("RGBA", (16, 16 * FRAMES), (0, 0, 0, 0)) sparks = [(rng.randint(3, 12), rng.randint(0, 15)) for _ in range(3)] for f in range(FRAMES): pulse = 0.5 + 0.5 * math.sin(2 * math.pi * f / FRAMES) image = glass(dark, mid, light, accent) for y, row in enumerate(rows): for x, c in enumerate(row): if not (2 <= x <= 13 and 2 <= top_row + y <= 13 and c in "#+"): continue if emblem == "flame": # Flicker: each pixel picks a flame colour, hotter towards the core; the tips come and go. if y < 3 and rng.random() < 0.35: continue heat = (1 if c == "+" else 0) + rng.choice((0, 0, 1)) color = FLAME_COLORS[min(len(FLAME_COLORS) - 1, heat + (1 if pulse > 0.6 else 0))] else: color = lerp(accent if c == "#" else glow, white, pulse * (0.6 if c == "+" else 0.35)) image.putpixel((x, top_row + y), color + (round(EMBLEM_ALPHA + (255 - EMBLEM_ALPHA) * (0.5 + 0.5 * pulse)),)) # Sparks drifting up the glass, two pixels a frame. for sx, sy in sparks: y = 13 - (sy + 2 * f) % 12 image.putpixel((sx, y), glow + (230,)) strip.paste(image, (0, 16 * f)) return strip def write_json(path, data): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2) + "\n") def main(): textures = ASSETS / "textures/block" textures.mkdir(parents=True, exist_ok=True) for name, (dark, mid, light, accent, emblem) in CORES.items(): side(dark, mid, light, accent).save(textures / f"{name}_side.png") top(dark, mid, light, accent).save(textures / f"{name}_top.png") front(dark, mid, light, accent, emblem).save(textures / f"{name}_front.png") working_front(dark, mid, light, accent, emblem, name).save(textures / f"{name}_front_on.png") write_json(textures / f"{name}_front_on.png.mcmeta", {"animation": {"frametime": FRAME_TICKS, "interpolate": True}}) for model, front_texture in ((name, f"{name}_front"), (f"{name}_on", f"{name}_front_on")): write_json(ASSETS / f"models/block/{model}.json", { "parent": "minecraft:block/orientable", "render_type": "minecraft:translucent", "textures": {"top": f"cobblesurvival:block/{name}_top", "front": f"cobblesurvival:block/{front_texture}", "side": f"cobblesurvival:block/{name}_side"}, }) write_json(ASSETS / f"models/item/{name}.json", {"parent": f"cobblesurvival:block/{name}"}) rotation = {"north": 0, "east": 90, "south": 180, "west": 270} write_json(ASSETS / f"blockstates/{name}.json", { "variants": {f"active={str(active).lower()},facing={facing}": { "model": f"cobblesurvival:block/{name}{'_on' if active else ''}", **({"y": y} if y else {})} for active in (False, True) for facing, y in rotation.items()}, }) print(f"wrote {len(CORES)} cores to {ASSETS.relative_to(ROOT)}") if __name__ == "__main__": main()