Files
probably-decentralized/art/sprites.py
T
ZHU MINCHAOandClaude Sonnet 5.5 9d51c74f56 Rig sprite: slightly top-down view like the game's machines
Top face with vents and an indicator, lit left edge, darker right side,
front with three card bays and a power port, two feet, rounded outline.
The cell-centre check still passes, so nothing can sit under it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 10:05:06 +08:00

236 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""物品像素图(游戏物品为 100 PPU,一格 16 像素;形状由不透明像素决定)。
用法:python3 art/sprites.py → 写出 src/Decentralized/ModFiles/Textures/Items/*.png
python3 art/sprites.py --preview DIR → 另存放大 6 倍、桌面底色的预览
画法沿用 Probably Automated 对讲机图:轮廓统一用 #292234,光从左上来,颜色取自游戏物品图集的调色板。
"""
import os
import struct
import sys
import zlib
C = {
'O': (0x29, 0x22, 0x34), # 轮廓
'k': (0x37, 0x35, 0x41), # 最暗
'd': (0x44, 0x46, 0x4C), # 暗面
'm': (0x5F, 0x68, 0x68), # 中间
'b': (0x88, 0x94, 0x8A), # 亮面
'l': (0xA8, 0xAE, 0xA5), # 受光
'h': (0xC4, 0xC4, 0xB0), # 亮边
'g': (0x53, 0x58, 0x5B), # 散热片
's': (0x27, 0x56, 0x54), # 屏幕底
'S': (0x67, 0xAA, 0xA5), # 屏幕字
'T': (0x8A, 0xBF, 0xBB), # 屏幕高光
'G': (0x5E, 0x9C, 0x4E), # Probidia 绿
'g2': (0x3E, 0x6B, 0x36),
'R': (0xB0, 0x4A, 0x3E), # PMD 红
'r2': (0x7A, 0x33, 0x2E),
'Y': (0xC8, 0xA4, 0x4A), # 金手指
'c': (0x2E, 0x2A, 0x2A), # 焦黑
'e': (0x6B, 0x4A, 0x3A), # 焦痕
}
class Canvas:
def __init__(self, w, h):
self.w, self.h = w, h
self.px = [[None] * w for _ in range(h)]
def put(self, x, y, c):
if 0 <= x < self.w and 0 <= y < self.h:
self.px[y][x] = c
def rect(self, x0, y0, x1, y1, c):
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
self.put(x, y, c)
def box(self, x0, y0, x1, y1, fill, outline='O'):
self.rect(x0, y0, x1, y1, fill)
for x in range(x0, x1 + 1):
self.put(x, y0, outline); self.put(x, y1, outline)
for y in range(y0, y1 + 1):
self.put(x0, y, outline); self.put(x1, y, outline)
def png(self, path, scale=1, bg=None):
raw = bytearray()
for row in self.px:
line = bytearray()
for c in row:
r, g, b, a = (0, 0, 0, 0) if c is None else C[c] + (255,)
if bg is not None:
r, g, b = (round(v * a / 255 + w * (255 - a) / 255) for v, w in zip((r, g, b), bg))
a = 255
line += bytes((r, g, b, a)) * scale
for _ in range(scale):
raw.append(0); raw += line
def chunk(tag, data):
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data) & 0xFFFFFFFF)
data = b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', self.w * scale, self.h * scale, 8, 6, 0, 0, 0)) \
+ chunk(b'IDAT', zlib.compress(bytes(raw), 9)) + chunk(b'IEND', b'')
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
with open(path, 'wb') as f:
f.write(data)
def gpu(accent, accent_dark, burnt=False):
"""显卡(32×16,2×1 格):外壳、两个风扇、品牌色条、金手指。"""
c = Canvas(32, 16)
c.box(1, 2, 30, 12, 'm')
c.rect(2, 3, 29, 3, 'l') # 顶边受光
c.rect(2, 11, 29, 11, 'd') # 底边暗面
for cx in (9, 21): # 风扇
for y in range(5, 11):
for x in range(cx - 4, cx + 5):
dx, dy = x - cx, y - 7.5
if dx * dx + dy * dy <= 16:
c.put(x, y, 'k')
if 6 <= dx * dx + dy * dy <= 16:
c.put(x, y, 'g')
c.put(cx, 7, 'b'); c.put(cx, 8, 'b')
c.rect(2, 4, 29, 4, accent) # 品牌色条
c.put(2, 5, accent_dark); c.put(29, 5, accent_dark)
c.rect(6, 13, 25, 13, 'Y') # 金手指
for x in range(6, 26, 2):
c.put(x, 14, 'Y')
c.rect(30, 3, 30, 11, 'k') # 挡板
if burnt:
for x, y in [(7, 5), (8, 6), (10, 9), (19, 6), (20, 7), (22, 9), (14, 8), (15, 7), (24, 5)]:
c.put(x, y, 'c')
c.rect(2, 4, 29, 4, 'e')
for x, y in [(12, 3), (13, 3), (17, 11), (26, 10)]:
c.put(x, y, 'c')
return c
def rig():
"""矿架(48×48,3×3 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
c = Canvas(48, 48)
FL, FR, FY, DEP = 3, 44, 24, 19 # 正面左右边、顶面前沿所在行、顶面深度(顶面后沿在 y=6,中心行 y=8 落在顶面上)
BOT = 44 # 正面底边
inset = lambda v: v // 6 # 顶面后部只略微收窄:原版机器的顶面也近似矩形,并且要让左右两格的中心落在顶面内
# 顶面:后部受光、前部稍暗;左缘亮、右缘暗
for v in range(DEP):
y = FY - v
x0, x1 = FL + inset(v), FR - inset(v)
for x in range(x0, x1 + 1):
c.put(x, y, 'l' if v >= 4 else 'b')
c.put(x0, y, 'h'); c.put(x0 + 1, y, 'l')
c.put(x1, y, 'd'); c.put(x1 - 1, y, 'm')
for x in range(FL + inset(DEP - 1), FR - inset(DEP - 1) + 1):
c.put(x, FY - DEP + 1, 'l')
# 顶面上的散热格栅和一个小指示灯
for v in range(4, 16, 3):
for u in range(6, 24):
c.put(FL + inset(v) + u, FY - v, 'g')
c.put(FR - 9, FY - 8, 'G'); c.put(FR - 8, FY - 8, 'G')
# 正面:中间色,左端受光、右端暗;顶部一条暗折线
for y in range(FY + 1, BOT + 1):
for x in range(FL, FR + 1):
c.put(x, y, 'm')
c.put(FL, y, 'b'); c.put(FL + 1, y, 'l' if y < FY + 5 else 'b')
for dx in range(4):
c.put(FR - dx, y, 'd' if dx else 'k')
for x in range(FL, FR + 1):
c.put(x, FY + 1, 'k')
c.put(x, BOT, 'd')
for x in range(FL + 1, FR):
c.put(x, FY, 'h')
c.put(x, FY + 2, 'b')
for x in range(FL + 1, FR):
c.put(x, BOT - 1, 'd')
# 正面里的四层显卡位(深色槽,上沿亮、下沿暗,右端一颗状态灯)
# 正面四层显卡位(深色槽,右端一颗状态灯);间隔行用中间色,保证每格中心都不透明
for i, y in enumerate((FY + 3, FY + 8, FY + 13)):
for x in range(FL + 3, FR - 4):
for dy in range(3):
c.put(x, y + dy, 'k')
c.put(x, y, 'g')
for x in range(FL + 6, FR - 6, 3):
c.put(x, y + 1, 'd')
c.put(FR - 4, y + 1, 'G' if i % 2 == 0 else 'R')
# 电源口(左下)
for x in range(FL + 3, FL + 12):
for dy in (-3, -2):
c.put(x, BOT + dy, 'k')
c.put(FL + 10, BOT - 2, 'S')
for x in range(FL + 15, FR - 4, 2):
c.put(x, BOT - 2, 'g')
# 底部两只脚
for x in (FL + 2, FL + 3, FR - 3, FR - 2):
c.put(x, BOT + 1, 'O'); c.put(x, BOT + 2, 'O')
# 轮廓:实体像素挨着透明的一圈描成轮廓色,再削掉尖角
solid = lambda v: v is not None
get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None
edge = [(x, y) for y in range(c.h) for x in range(c.w)
if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))]
for x, y in edge:
c.put(x, y, 'O')
for y in range(c.h):
for x in range(c.w):
if get(x, y) == 'O' and sum(solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1))) <= 1:
c.px[y][x] = None
# 圆角:正面底部两角各削一个像素,让轮廓和原版机器一样不是尖方块
for x, y in ((FL, BOT), (FR, BOT), (FL, BOT + 0), (FR, BOT + 0)):
c.px[y][x] = None
c.px[y][x + (1 if x == FL else -1)] = 'O'
return c
def terminal():
"""交易终端(32×32,2×2 格):小型显示器 + 键盘,屏幕上是 K 线。"""
c = Canvas(32, 32)
c.box(3, 2, 28, 21, 'm')
c.rect(4, 3, 27, 3, 'l')
c.box(5, 4, 26, 18, 's')
candles = [(8, 12, 15), (11, 10, 14), (14, 11, 13), (17, 7, 12), (20, 8, 11), (23, 5, 10)]
for x, top, bot in candles:
c.rect(x, top, x, bot, 'S')
c.put(x, top - 1, 'T')
c.rect(3, 22, 28, 23, 'd') # 支架(拉通,保证每格中心点不透明)
c.box(2, 24, 29, 30, 'b') # 键盘
for y in (26, 28):
for x in range(4, 28, 2):
c.put(x, y, 'm')
return c
SPRITES = {
'gpu_probidia': lambda: gpu('G', 'g2'),
'gpu_pmd': lambda: gpu('R', 'r2'),
'gpu_burnt': lambda: gpu('e', 'c', burnt=True),
'rig': rig,
'terminal': terminal,
}
def check_cells(canvas, name):
"""游戏按 mod 物品图每格的中心像素是否不透明来判断占格(行从图的下沿往上数)。有空格就报错。"""
cols, rows = canvas.w // 16, canvas.h // 16
bad = [(q, r) for r in range(rows) for q in range(cols) if canvas.px[canvas.h - 1 - (r * 16 + 8)][q * 16 + 8] is None]
if bad:
raise SystemExit(f"{name}: 这些格子中心像素透明(游戏会当成空格,别的物品会叠上来):{bad}")
if __name__ == '__main__':
here = os.path.dirname(os.path.abspath(__file__))
if len(sys.argv) == 3 and sys.argv[1] == '--preview':
for name, make in SPRITES.items():
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(sys.argv[2], name + '.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
print(sys.argv[2])
else:
out = os.path.normpath(os.path.join(here, '..', 'src', 'Decentralized', 'ModFiles', 'Textures', 'Items'))
for name, make in SPRITES.items():
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(out, name + '.png'))
print(out)