feat: A-frame tent + portal walk-through field + texture polish
All checks were successful
Deploy Addons / deploy (push) Successful in 14s
All checks were successful
Deploy Addons / deploy (push) Successful in 14s
- camping: replace cube tent with A-frame slope panels (tent_panel_l/r) + cardinal_direction permutations; vote-skip sleep that mixes player.isSleeping bed sleepers with tent occupants and respects the playersSleepingPercentage gamerule; new weathered-canvas texture. - lobby: walk-through silverlabs:portal_field block (no collision, translucent swirl, cross-plane geo) auto-placed above each portal frame; invisible silverlabs:portal_label entity floats above each portal with the destination world name; transfer detection now scans down through the field to find the destination frame. - postal: regenerate post_office and mailbox block textures so they fill the full block face (brick + POST plaque, full red panel with slot/latch /flag/rivets) instead of small sprites floating on transparent. - dynamite + tow-boat: ship the addons (volumes wired into all four worlds; enabled_packs registers them into Mya's world). - art: build-textures.py extended; build-art-catalog.py added to project. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
111
scripts/build-art-catalog.py
Normal file
111
scripts/build-art-catalog.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build a contact sheet of every texture under art/ — each PNG upscaled
|
||||
with nearest-neighbour (so pixels stay crisp) and labelled with its
|
||||
relative path. Output: art/CATALOG.png
|
||||
|
||||
Run from repo root:
|
||||
python3 scripts/build-art-catalog.py
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
ART = REPO / "art"
|
||||
OUT = ART / "CATALOG.png"
|
||||
|
||||
TILE = 128 # upscaled texture size
|
||||
COLS = 6 # tiles per row
|
||||
PAD_X = 20 # horizontal padding around each tile
|
||||
PAD_Y = 60 # vertical padding (extra room for label below)
|
||||
LABEL_PX = 14 # label font size
|
||||
BG = (30, 30, 46) # dark background
|
||||
FG = (230, 230, 240)
|
||||
SECTION_BG = (50, 60, 90)
|
||||
|
||||
|
||||
def load_font(size):
|
||||
for cand in (
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
"/mnt/c/Windows/Fonts/consola.ttf",
|
||||
"/mnt/c/Windows/Fonts/arial.ttf",
|
||||
):
|
||||
if os.path.exists(cand):
|
||||
return ImageFont.truetype(cand, size)
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def collect():
|
||||
"""Return {pack_name: [(relative_path, abs_path), ...]} grouped & sorted."""
|
||||
groups = {}
|
||||
for png in sorted(ART.rglob("*.png")):
|
||||
if png.name == "CATALOG.png":
|
||||
continue
|
||||
rel = png.relative_to(ART)
|
||||
pack = rel.parts[0]
|
||||
groups.setdefault(pack, []).append((rel, png))
|
||||
return groups
|
||||
|
||||
|
||||
def upscale(png_path, size):
|
||||
img = Image.open(png_path).convert("RGBA")
|
||||
return img.resize((size, size), Image.NEAREST)
|
||||
|
||||
|
||||
def main():
|
||||
groups = collect()
|
||||
if not groups:
|
||||
print("No PNGs found under art/. Nothing to do.")
|
||||
return
|
||||
|
||||
font_label = load_font(LABEL_PX)
|
||||
font_section = load_font(LABEL_PX + 8)
|
||||
|
||||
# Calculate overall canvas size
|
||||
section_height = LABEL_PX + 24
|
||||
tile_block_h = TILE + PAD_Y
|
||||
total_h = 40
|
||||
for pack, items in groups.items():
|
||||
rows = (len(items) + COLS - 1) // COLS
|
||||
total_h += section_height + rows * tile_block_h + 20
|
||||
total_w = COLS * (TILE + PAD_X) + PAD_X + 40
|
||||
|
||||
canvas = Image.new("RGBA", (total_w, total_h), BG)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
y = 20
|
||||
for pack, items in groups.items():
|
||||
# Section header
|
||||
draw.rectangle([20, y, total_w - 20, y + section_height - 4], fill=SECTION_BG)
|
||||
draw.text((32, y + 4), pack, font=font_section, fill=FG)
|
||||
y += section_height + 6
|
||||
|
||||
# Tiles
|
||||
for i, (rel, abs_path) in enumerate(items):
|
||||
col = i % COLS
|
||||
row = i // COLS
|
||||
x = 20 + PAD_X // 2 + col * (TILE + PAD_X)
|
||||
ty = y + row * tile_block_h
|
||||
|
||||
try:
|
||||
tile = upscale(abs_path, TILE)
|
||||
canvas.paste(tile, (x, ty), tile)
|
||||
except Exception as e:
|
||||
draw.rectangle([x, ty, x + TILE, ty + TILE], outline=(200, 80, 80), width=2)
|
||||
draw.text((x + 4, ty + 4), f"ERR: {e}", font=font_label, fill=(255, 120, 120))
|
||||
|
||||
# Label: show subpath + filename below the tile
|
||||
label = "/".join(rel.parts[1:]) # drop pack prefix
|
||||
draw.text((x, ty + TILE + 4), label, font=font_label, fill=FG)
|
||||
|
||||
rows = (len(items) + COLS - 1) // COLS
|
||||
y += rows * tile_block_h + 20
|
||||
|
||||
canvas.save(OUT)
|
||||
print(f"Wrote {OUT} — {sum(len(v) for v in groups.values())} textures across {len(groups)} packs.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,6 +30,13 @@ 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
|
||||
@@ -228,10 +235,174 @@ def mailbox() -> Image.Image:
|
||||
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
|
||||
|
||||
|
||||
def main() -> None:
|
||||
save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png")
|
||||
save(post_office(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png")
|
||||
save(mailbox(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user