Files
minecraft-aiworld/scripts/build-textures.py
T
sysadminandClaude Opus 5 75219f9d1d
Deploy Addons / deploy (push) Failing after 0s
fix(naturalist): draw the three missing item icons
frog_leg, cooked_frog_leg and snake_egg_block are declared in the behavior
pack and mapped in item_texture.json, but the PNGs were never present, so all
three rendered as missing-texture wherever they appeared. All three already
had lang names, so only the art was absent.

Drawn through scripts/build-textures.py to match the pack's existing style --
small sprite on transparency, muted natural palette, single highlight. The
frog legs share one drumstick routine, raw carrying a green skin tint along
the top contour and cooked a browned crust. snake_egg_block is a clutch of
three eggs reusing the palette of the pack's existing single snake_egg.png,
so the block form reads as a group of the item.

RP bumped to 1.0.3 and mya's pin updated to match, otherwise clients keep
serving the cached pack and never fetch the new files.

Note: naturalist-lite-addon is absent from deploy.yml's PATHS, so CI never
deploys it -- this was applied to the host by hand. That absence is also why
the host sat on RP 1.0.1 while main was already at 1.0.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC
2026-08-28 13:14:49 +01:00

628 lines
22 KiB
Python

#!/usr/bin/env python3
"""Generate 16x16 block textures for smart_crafting_table, post_office, mailbox."""
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
# ── Palette ─────────────────────────────────────────────────
OAK_LIGHT = (150, 110, 65)
OAK_DARK = (115, 80, 45)
OAK_SHADOW = (80, 55, 28)
GOLD = (218, 165, 32)
GOLD_BRIGHT = (255, 214, 85)
REDSTONE = (210, 40, 40)
REDSTONE_BRIGHT = (255, 90, 90)
BRICK_RED = (138, 58, 48)
BRICK_DARK = (92, 38, 30)
MORTAR = (68, 52, 42)
SLATE = (58, 62, 70)
CREAM = (240, 225, 190)
ENVELOPE_LINE = (45, 30, 20)
STAMP_RED = (190, 50, 50)
MB_RED = (175, 40, 40)
MB_RED_DARK = (115, 25, 25)
MB_RED_HL = (220, 75, 75)
MB_BLACK = (20, 20, 20)
MB_FLAG = (235, 190, 55)
MB_POST = (90, 60, 35)
# Tent canvas — weathered green army-tent fabric
CANVAS_LIGHT = (118, 138, 88)
CANVAS_MID = (90, 110, 68)
CANVAS_DARK = (66, 84, 50)
CANVAS_SHADOW = (44, 58, 36)
CANVAS_STITCH = (190, 175, 130)
def save(img: Image.Image, rel: str) -> None:
out = ROOT / rel
out.parent.mkdir(parents=True, exist_ok=True)
img.save(out)
print(f"wrote {rel}")
def px(img: Image.Image, x: int, y: int, color) -> None:
if 0 <= x < img.width and 0 <= y < img.height:
img.putpixel((x, y), color)
def rect(img: Image.Image, x0: int, y0: int, x1: int, y1: int, color) -> None:
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
px(img, x, y, color)
# ── Smart Crafting Table ───────────────────────────────────
# Top-down style: oak planks, bold gold frame, 2x2 inlay with redstone + diamond.
def smart_crafting_table() -> Image.Image:
img = Image.new("RGBA", (16, 16), OAK_LIGHT)
# Oak plank background with subtle grain
for y in range(16):
band = (y // 4) % 2
color = OAK_LIGHT if band == 0 else OAK_DARK
rect(img, 0, y, 15, y, color)
# Knots / grain speckle
for (x, y) in [(3, 1), (11, 2), (6, 5), (13, 6), (2, 9), (9, 10), (5, 13), (12, 14)]:
px(img, x, y, OAK_SHADOW)
# Plank seams
for y in (3, 7, 11):
rect(img, 0, y, 15, y, OAK_SHADOW)
# Bold gold frame (2px thick)
rect(img, 0, 0, 15, 1, GOLD)
rect(img, 0, 14, 15, 15, GOLD)
rect(img, 0, 0, 1, 15, GOLD)
rect(img, 14, 0, 15, 15, GOLD)
# Inner frame highlight
rect(img, 1, 1, 14, 1, GOLD_BRIGHT)
rect(img, 1, 14, 14, 14, (165, 120, 20))
rect(img, 1, 2, 1, 13, GOLD_BRIGHT)
rect(img, 14, 2, 14, 13, (165, 120, 20))
# Corner rivets
for (x, y) in [(0, 0), (15, 0), (0, 15), (15, 15)]:
px(img, x, y, (90, 65, 10))
# 2x2 inset panels with gold dividers at mid (x=7..8, y=7..8)
# Top-left = redstone, top-right = diamond, bottom-left = emerald, bottom-right = gold ingot
TL = (3, 3, 6, 6) # redstone area
TR = (9, 3, 12, 6) # diamond area
BL = (3, 9, 6, 12) # emerald area
BR = (9, 9, 12, 12) # gold area
# Dark recessed wells
for (x0, y0, x1, y1) in (TL, TR, BL, BR):
rect(img, x0, y0, x1, y1, (40, 25, 12))
# Redstone gem (red)
rect(img, 4, 4, 5, 5, REDSTONE)
px(img, 4, 4, REDSTONE_BRIGHT)
# Diamond (cyan)
rect(img, 10, 4, 11, 5, (120, 220, 235))
px(img, 10, 4, (200, 250, 255))
# Emerald (green)
rect(img, 4, 10, 5, 11, (50, 170, 90))
px(img, 4, 10, (120, 220, 150))
# Gold nugget
rect(img, 10, 10, 11, 11, GOLD)
px(img, 10, 10, GOLD_BRIGHT)
# Gold cross dividers
rect(img, 7, 2, 8, 13, GOLD)
rect(img, 2, 7, 13, 8, GOLD)
# Center jewel
px(img, 7, 7, REDSTONE_BRIGHT)
px(img, 8, 7, REDSTONE_BRIGHT)
px(img, 7, 8, REDSTONE)
px(img, 8, 8, REDSTONE)
return img
# ── Post Office ────────────────────────────────────────────
# Big envelope front: cream background, bold dark fold lines, red stamp, "POST" bar.
def post_office() -> Image.Image:
img = Image.new("RGBA", (16, 16), CREAM)
# Outer envelope border (1px)
rect(img, 0, 0, 15, 0, ENVELOPE_LINE)
rect(img, 0, 15, 15, 15, ENVELOPE_LINE)
rect(img, 0, 0, 0, 15, ENVELOPE_LINE)
rect(img, 15, 0, 15, 15, ENVELOPE_LINE)
# Paper shade (slight gradient on bottom half)
for y in range(8, 15):
for x in range(1, 15):
if (x + y) % 3 == 0:
px(img, x, y, (225, 210, 175))
# Envelope fold: top-left and top-right diagonals meeting at center-top
# Lines go from (1,1) -> (7,7) and (14,1) -> (8,7)
for i in range(7):
px(img, 1 + i, 1 + i, ENVELOPE_LINE)
px(img, 14 - i, 1 + i, ENVELOPE_LINE)
# Second pass for thickness
for i in range(6):
px(img, 2 + i, 1 + i, (90, 70, 50))
px(img, 13 - i, 1 + i, (90, 70, 50))
# Horizontal seam at y=8 (where flaps meet paper)
rect(img, 1, 8, 14, 8, ENVELOPE_LINE)
# Red stamp: top-right square (3x3) with crosshatch
rect(img, 11, 2, 13, 4, STAMP_RED)
px(img, 12, 3, (255, 220, 220)) # stamp center highlight
# Stamp border
px(img, 10, 2, ENVELOPE_LINE)
px(img, 10, 3, ENVELOPE_LINE)
px(img, 10, 4, ENVELOPE_LINE)
px(img, 11, 5, ENVELOPE_LINE)
px(img, 12, 5, ENVELOPE_LINE)
px(img, 13, 5, ENVELOPE_LINE)
# Address bar: dark rectangle bottom-center reading like lines of text
rect(img, 3, 11, 12, 12, (120, 100, 75))
rect(img, 3, 13, 10, 13, (120, 100, 75))
# "POST" mark: a small red circle stamp bottom-right
rect(img, 11, 11, 13, 13, STAMP_RED)
px(img, 11, 11, (0, 0, 0, 0))
px(img, 13, 11, (0, 0, 0, 0))
px(img, 11, 13, (0, 0, 0, 0))
px(img, 13, 13, (0, 0, 0, 0))
px(img, 12, 12, (255, 230, 230))
return img
# ── Mailbox ───────────────────────────────────────────────
# Classic red pillar-mailbox front: domed top, slot, base, flag.
def mailbox() -> Image.Image:
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
# Post base (wooden) across bottom
rect(img, 6, 14, 9, 15, MB_POST)
px(img, 6, 14, (70, 45, 25))
px(img, 9, 14, (70, 45, 25))
# Mailbox body (rounded: rows 2..13)
# Body rectangle x=2..13, y=4..13
rect(img, 2, 4, 13, 13, MB_RED)
# Rounded top corners
px(img, 2, 4, (0, 0, 0, 0))
px(img, 13, 4, (0, 0, 0, 0))
# Add a dome/top-bump
rect(img, 3, 2, 12, 3, MB_RED)
px(img, 3, 2, (0, 0, 0, 0))
px(img, 12, 2, (0, 0, 0, 0))
rect(img, 4, 1, 11, 1, MB_RED)
rect(img, 5, 0, 10, 0, MB_RED_DARK)
# Left-side highlight (lighter vertical stripe)
rect(img, 3, 5, 3, 12, MB_RED_HL)
px(img, 4, 2, MB_RED_HL)
# Right-side shadow
rect(img, 12, 5, 12, 12, MB_RED_DARK)
rect(img, 11, 13, 13, 13, MB_RED_DARK)
# Black letter slot (horizontal bar)
rect(img, 5, 6, 10, 7, MB_BLACK)
# Slot highlight
rect(img, 5, 6, 10, 6, (50, 50, 50))
# Circular latch below slot
px(img, 7, 10, MB_BLACK)
px(img, 8, 10, MB_BLACK)
px(img, 7, 11, MB_BLACK)
px(img, 8, 11, MB_BLACK)
px(img, 7, 10, (90, 90, 90))
# Yellow flag on the right side
rect(img, 14, 3, 15, 6, MB_FLAG)
px(img, 14, 3, (180, 140, 40))
px(img, 15, 6, (180, 140, 40))
# Flag pole
rect(img, 14, 7, 14, 10, (60, 60, 60))
return img
# ── Tent Canvas ────────────────────────────────────────────
# Weathered army-canvas fabric: subtle horizontal weave + occasional darker
# mottling + a vertical seam stitch line. Fully opaque so it doesn't ghost
# against the alpha_test renderer.
def tent_canvas() -> Image.Image:
img = Image.new("RGBA", (16, 16), CANVAS_MID)
# Horizontal weave bands (every 2 rows alternating tone)
for y in range(16):
row = CANVAS_MID if (y // 2) % 2 == 0 else CANVAS_DARK
rect(img, 0, y, 15, y, row)
# Diagonal weave specks for fabric texture
for y in range(16):
for x in range(16):
if (x + y) % 5 == 0:
px(img, x, y, CANVAS_LIGHT)
elif (x - y) % 7 == 0:
px(img, x, y, CANVAS_SHADOW)
# Wear / sun-bleached patches (clusters)
for (x, y) in [(2, 3), (3, 3), (10, 6), (11, 6), (5, 11), (6, 11), (13, 12)]:
px(img, x, y, CANVAS_LIGHT)
# Vertical seam stitch down the center
for y in range(0, 16, 2):
px(img, 7, y, CANVAS_STITCH)
px(img, 8, y, CANVAS_SHADOW)
# Edge darken (gives the panel some depth at block boundaries)
rect(img, 0, 0, 15, 0, CANVAS_SHADOW)
rect(img, 0, 15, 15, 15, CANVAS_SHADOW)
rect(img, 0, 0, 0, 15, CANVAS_SHADOW)
rect(img, 15, 0, 15, 15, CANVAS_SHADOW)
return img
# ── Mailbox v2 — block-style (full 16x16 face, no transparency) ─
# The previous sprite was an item-sized icon centered on transparency, which
# reads as a placeholder when applied to all 6 faces of a cube. This version
# fills the face: red body panel, vertical seam, slot, latch, riveted corners.
def mailbox_block() -> Image.Image:
img = Image.new("RGBA", (16, 16), MB_RED)
# Vertical highlight strip on the left
rect(img, 0, 0, 1, 15, MB_RED_HL)
# Vertical shadow strip on the right
rect(img, 14, 0, 15, 15, MB_RED_DARK)
# Top dome highlight (1px band)
rect(img, 2, 0, 13, 0, MB_RED_HL)
# Bottom shadow band
rect(img, 2, 15, 13, 15, MB_RED_DARK)
# Horizontal mail slot (centered)
rect(img, 4, 6, 11, 8, MB_BLACK)
rect(img, 4, 6, 11, 6, (50, 50, 50)) # slot lip highlight
rect(img, 4, 8, 11, 8, (5, 5, 5)) # slot lip shadow
# Round latch below the slot
rect(img, 7, 11, 8, 12, MB_BLACK)
px(img, 7, 11, (90, 90, 90))
# Yellow flag emblem in the top-right corner
rect(img, 12, 2, 13, 4, MB_FLAG)
px(img, 12, 2, (180, 140, 40))
# Flag pole
px(img, 13, 5, (60, 60, 60))
# Rivets in the corners
for (x, y) in [(2, 2), (13, 2), (2, 13), (13, 13)]:
px(img, x, y, (40, 15, 15))
return img
# ── Post Office v2 — block-style facade ─────────────────────
# A small post-office building face: brick wall, "POST" plaque, awning hint.
def post_office_block() -> Image.Image:
img = Image.new("RGBA", (16, 16), BRICK_RED)
# Brick courses with mortar lines (offset every other row)
for y in range(16):
# mortar line every 4 rows
if y % 4 == 0:
rect(img, 0, y, 15, y, MORTAR)
else:
# vertical mortar joints, offset by course
offset = 0 if (y // 4) % 2 == 0 else 4
for x in range(16):
if (x + offset) % 8 == 0:
px(img, x, y, MORTAR)
else:
# subtle brick tone variation
if (x + y) % 3 == 0:
px(img, x, y, BRICK_DARK)
# Cream-colored "POST" plaque (centered)
rect(img, 2, 5, 13, 10, CREAM)
rect(img, 2, 5, 13, 5, ENVELOPE_LINE) # top border
rect(img, 2, 10, 13, 10, ENVELOPE_LINE) # bottom border
rect(img, 2, 5, 2, 10, ENVELOPE_LINE) # left border
rect(img, 13, 5, 13, 10, ENVELOPE_LINE) # right border
# "POST" lettering — 4 chunky chars on the plaque
# P
rect(img, 3, 7, 4, 8, ENVELOPE_LINE); px(img, 3, 6, ENVELOPE_LINE)
# O
rect(img, 6, 6, 7, 9, ENVELOPE_LINE); px(img, 6, 7, CREAM); px(img, 7, 7, CREAM); px(img, 6, 8, CREAM); px(img, 7, 8, CREAM)
# S (simplified stub)
rect(img, 9, 6, 10, 6, ENVELOPE_LINE); px(img, 9, 7, ENVELOPE_LINE); rect(img, 9, 8, 10, 8, ENVELOPE_LINE); px(img, 10, 9, ENVELOPE_LINE); rect(img, 9, 9, 10, 9, ENVELOPE_LINE)
# T
rect(img, 11, 6, 12, 6, ENVELOPE_LINE); rect(img, 11, 7, 11, 9, ENVELOPE_LINE)
# Red stamp accent in the top-right
rect(img, 12, 1, 14, 3, STAMP_RED)
px(img, 13, 2, (255, 220, 220))
# Awning suggestion (alternating red/cream stripes at the top)
for x in range(16):
if (x // 2) % 2 == 0:
px(img, x, 1, (210, 70, 70))
else:
px(img, x, 1, CREAM)
rect(img, 0, 0, 15, 0, ENVELOPE_LINE)
return img
# ── Portal Field ───────────────────────────────────────────
# Translucent swirling-energy texture for the walk-through portal column.
# Renders with alpha blending — the dark areas read as voids, the bright
# center streaks read as concentrated portal energy.
def portal_field() -> Image.Image:
import math
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
cx, cy = 7.5, 7.5
for y in range(16):
for x in range(16):
dx = x - cx
dy = y - cy
dist = math.sqrt(dx * dx + dy * dy)
angle = math.atan2(dy, dx)
# swirl: angle modulated by distance
swirl = math.sin(angle * 3 + dist * 1.5) * 0.5 + 0.5
# purple/violet base
r = int(80 + swirl * 100)
g = int(20 + swirl * 30)
b = int(140 + swirl * 90)
# alpha falls off near edges, bright in middle bands
edge = max(0, 1 - dist / 8.5)
alpha = int(160 * edge + swirl * 60)
alpha = max(0, min(220, alpha))
img.putpixel((x, y), (r, g, b, alpha))
# Bright vertical core streaks
for y in range(16):
for x in (7, 8):
r, g, b, _ = img.getpixel((x, y))
img.putpixel((x, y), (min(255, r + 60), min(255, g + 30), min(255, b + 60), 220))
return img
# ── Redstone Link transmitter / receiver ───────────────────
# Machine faces, not icons. Following the mailbox lesson: the motif has to
# reach the edges or a cube of it reads as a placeholder. Both share a gunmetal
# plate with corner rivets and a beveled frame; the transmitter carries redstone
# traces radiating OUT to all four edges, the receiver carries traces running IN
# to a collector dish. Same silhouette, mirrored behaviour, different accent.
LINK_PLATE = (62, 66, 74)
LINK_PLATE_HL = (92, 97, 107)
LINK_PLATE_SH = (38, 41, 47)
LINK_RIVET = (140, 146, 158)
TX_TRACE = (168, 30, 28)
TX_TRACE_HOT = REDSTONE_BRIGHT
TX_CORE = (255, 140, 110)
RX_TRACE = (34, 96, 150)
RX_TRACE_HOT = (92, 178, 240)
RX_CORE = (200, 240, 255)
def _link_plate() -> Image.Image:
"""Shared gunmetal base: beveled frame + corner rivets, fully opaque."""
img = Image.new("RGBA", (16, 16), LINK_PLATE)
# Bevel: lit top/left, shadowed bottom/right
rect(img, 0, 0, 15, 0, LINK_PLATE_HL)
rect(img, 0, 0, 0, 15, LINK_PLATE_HL)
rect(img, 0, 15, 15, 15, LINK_PLATE_SH)
rect(img, 15, 0, 15, 15, LINK_PLATE_SH)
# Corner rivets
for cx, cy in ((2, 2), (13, 2), (2, 13), (13, 13)):
px(img, cx, cy, LINK_RIVET)
px(img, cx, cy + 1, LINK_PLATE_SH)
return img
def redstone_link_tx() -> Image.Image:
img = _link_plate()
# Traces running from the core out to all four edges: this is what makes a
# cube of it read as wiring rather than a centred sprite.
rect(img, 7, 0, 8, 15, TX_TRACE)
rect(img, 0, 7, 15, 8, TX_TRACE)
# Diagonal spurs toward the corners
for i in range(3, 7):
px(img, i, i, TX_TRACE)
px(img, 15 - i, i, TX_TRACE)
px(img, i, 15 - i, TX_TRACE)
px(img, 15 - i, 15 - i, TX_TRACE)
# Emitter core, hot centre
rect(img, 6, 6, 9, 9, TX_TRACE_HOT)
rect(img, 7, 7, 8, 8, TX_CORE)
px(img, 7, 7, (255, 235, 220))
# Trace highlight so the cross reads as raised
rect(img, 7, 1, 7, 5, TX_TRACE_HOT)
rect(img, 1, 7, 5, 7, TX_TRACE_HOT)
return img
def redstone_link_rx() -> Image.Image:
img = _link_plate()
# Collector rings, broken at the axes so the in-running traces read clearly
for x in range(3, 13):
px(img, x, 3, RX_TRACE)
px(img, x, 12, RX_TRACE)
for y in range(3, 13):
px(img, 3, y, RX_TRACE)
px(img, 12, y, RX_TRACE)
for x in range(5, 11):
px(img, x, 5, RX_TRACE_HOT)
px(img, x, 10, RX_TRACE_HOT)
for y in range(5, 11):
px(img, 5, y, RX_TRACE_HOT)
px(img, 10, y, RX_TRACE_HOT)
# Feed lines from the edges into the dish
rect(img, 7, 0, 8, 2, RX_TRACE)
rect(img, 7, 13, 8, 15, RX_TRACE)
rect(img, 0, 7, 2, 8, RX_TRACE)
rect(img, 13, 7, 15, 8, RX_TRACE)
# Collector core
rect(img, 7, 7, 8, 8, RX_TRACE_HOT)
px(img, 7, 7, RX_CORE)
px(img, 8, 8, RX_CORE)
return img
# ── Lobby portal blocks ────────────────────────────────────
# These previously pointed terrain_texture.json at vanilla paths
# (emerald_block, amethyst_block, prismarine_bricks, end_portal_frame_top)
# which a custom pack cannot resolve, so they rendered as missing-texture
# once minecraft:geometry made material_instances take effect. Ship real
# textures instead: a dark stone frame around a glowing field, one hue per
# child, so the three destinations are distinguishable at a glance.
PORTAL_STONE = (58, 60, 68)
PORTAL_STONE_HL = (86, 89, 99)
PORTAL_STONE_SH = (34, 35, 41)
def _portal_face(core, bright, glow):
img = Image.new("RGBA", (16, 16), PORTAL_STONE)
# chiselled frame
rect(img, 0, 0, 15, 0, PORTAL_STONE_HL)
rect(img, 0, 0, 0, 15, PORTAL_STONE_HL)
rect(img, 0, 15, 15, 15, PORTAL_STONE_SH)
rect(img, 15, 0, 15, 15, PORTAL_STONE_SH)
rect(img, 2, 2, 13, 13, PORTAL_STONE_SH)
# glowing field
rect(img, 3, 3, 12, 12, core)
rect(img, 4, 4, 11, 11, bright)
rect(img, 6, 6, 9, 9, glow)
# corner sparks so the field reads as energy, not flat paint
for cx, cy in ((4, 4), (11, 4), (4, 11), (11, 11)):
px(img, cx, cy, glow)
# dark centre seam
rect(img, 7, 7, 8, 8, core)
return img
def portal_jamie(): return _portal_face((22, 92, 40), (46, 156, 66), (140, 240, 150))
def portal_lyla(): return _portal_face((78, 40, 130), (126, 74, 190), (214, 168, 255))
def portal_mya(): return _portal_face((22, 96, 104), (46, 152, 160), (150, 238, 244))
def portal_frame():
img = Image.new("RGBA", (16, 16), PORTAL_STONE)
rect(img, 0, 0, 15, 0, PORTAL_STONE_HL)
rect(img, 0, 0, 0, 15, PORTAL_STONE_HL)
rect(img, 0, 15, 15, 15, PORTAL_STONE_SH)
rect(img, 15, 0, 15, 15, PORTAL_STONE_SH)
# inset panel with a dull gold ring, echoing an end portal frame
rect(img, 3, 3, 12, 12, (48, 50, 57))
for x in range(4, 12):
px(img, x, 4, GOLD); px(img, x, 11, GOLD)
for y in range(4, 12):
px(img, 4, y, GOLD); px(img, 11, y, GOLD)
rect(img, 6, 6, 9, 9, (70, 66, 40))
px(img, 7, 7, GOLD_BRIGHT); px(img, 8, 8, GOLD_BRIGHT)
return img
# ── Naturalist Lite: three items shipped without icons ─────
# frog_leg, cooked_frog_leg and snake_egg_block are declared in the BP and
# mapped in item_texture.json, but the PNGs were never present, so they
# rendered as missing-texture. Drawn to match the pack's existing style:
# small sprite on transparency, muted natural palette, single highlight.
FROG_RAW = (176, 122, 128)
FROG_RAW_HI = (214, 164, 168)
FROG_RAW_SH = (126, 82, 92)
FROG_SKIN = (122, 148, 96)
FROG_COOK = (156, 104, 56)
FROG_COOK_HI = (198, 146, 88)
FROG_COOK_SH = (104, 64, 32)
BONE = (232, 226, 206)
BONE_SH = (176, 170, 152)
EGG_SHELL = (222, 214, 196)
EGG_SHELL_HI = (244, 240, 228)
EGG_SHELL_SH = (170, 162, 144)
EGG_SPOT = (150, 138, 112)
def _frog_leg(meat, hi, sh, skin=None):
"""Drumstick: meat mass upper-left, bone stub lower-right."""
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
# meat mass
rect(img, 4, 2, 10, 8, meat)
rect(img, 3, 3, 3, 7, meat)
rect(img, 11, 3, 11, 7, meat)
rect(img, 5, 1, 9, 1, meat)
rect(img, 5, 9, 9, 9, meat)
# shading
rect(img, 4, 8, 10, 9, sh)
rect(img, 11, 4, 11, 7, sh)
# highlight
rect(img, 5, 2, 7, 3, hi)
px(img, 5, 4, hi)
# raw frog keeps a green tinge along the top contour rather than a patch,
# so it reads as skin on the meat instead of a floating square
if skin:
for sx, sy in ((6, 1), (7, 1), (8, 1), (9, 2), (10, 3), (5, 2)):
px(img, sx, sy, skin)
px(img, 8, 2, (140, 166, 112))
# bone stub running to lower-right
rect(img, 9, 10, 10, 11, BONE)
rect(img, 11, 12, 12, 13, BONE)
px(img, 12, 14, BONE)
px(img, 10, 11, BONE_SH)
px(img, 12, 13, BONE_SH)
px(img, 13, 14, BONE_SH)
return img
def frog_leg(): return _frog_leg(FROG_RAW, FROG_RAW_HI, FROG_RAW_SH, FROG_SKIN)
def cooked_frog_leg(): return _frog_leg(FROG_COOK, FROG_COOK_HI, FROG_COOK_SH)
def snake_egg_block():
"""A clutch of eggs, so it reads as the block form of the single egg item."""
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
def egg(cx, cy, w, h):
rect(img, cx, cy, cx + w, cy + h, EGG_SHELL)
rect(img, cx, cy + h, cx + w, cy + h, EGG_SHELL_SH)
rect(img, cx + w, cy + 1, cx + w, cy + h, EGG_SHELL_SH)
px(img, cx + 1, cy + 1, EGG_SHELL_HI)
egg(2, 6, 4, 6)
egg(9, 5, 4, 6)
egg(5, 8, 4, 5)
# speckles
for sx, sy in ((3, 9), (11, 8), (6, 11), (4, 7), (12, 10)):
px(img, sx, sy, EGG_SPOT)
return img
def main() -> None:
save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png")
save(post_office_block(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png")
save(mailbox_block(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png")
save(tent_canvas(), "camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png")
save(portal_field(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png")
save(portal_frame(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_frame.png")
save(portal_jamie(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_jamie.png")
save(portal_lyla(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_lyla.png")
save(portal_mya(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_mya.png")
save(redstone_link_tx(), "redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_tx.png")
save(redstone_link_rx(), "redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png")
NAT = "naturalist-lite-addon/naturalist_lite_RP/textures/sf/nba/items/"
save(frog_leg(), NAT + "frog_leg.png")
save(cooked_frog_leg(), NAT + "cooked_frog_leg.png")
save(snake_egg_block(), NAT + "snake_egg_block.png")
if __name__ == "__main__":
main()