The game decides which cells an item occupies by the alpha of one pixel at the centre of each 16x16 cell. The rig's rack bays left gaps exactly on the centre rows, so 8 of 9 cells counted as empty and other items were placed underneath it; the terminal's stand did the same. The rig gets a solid back plate, the terminal's stand is widened, and art/sprites.py now checks every cell the way the game does and refuses to write a sprite with a hole. Verified in game: new rigs are 3x3 solid, new terminals 2x2 solid. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
181 lines
7.0 KiB
Python
181 lines
7.0 KiB
Python
#!/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 格):开放式机架,四层显卡位,左下电源。"""
|
||
c = Canvas(48, 48)
|
||
c.box(2, 3, 45, 44, 'd') # 外框 + 背板:内部不透明,否则游戏按格子中心像素判断占格会把空隙当成空格
|
||
for y in range(4, 44):
|
||
c.put(3, y, 'b'); c.put(44, y, 'd')
|
||
for x in range(3, 45):
|
||
c.put(x, 4, 'l'); c.put(x, 43, 'k')
|
||
for i, y in enumerate((8, 16, 24, 32)): # 四层卡位
|
||
c.rect(6, y, 41, y + 5, 'k')
|
||
c.rect(6, y, 41, y, 'g')
|
||
for x in range(8, 40, 3):
|
||
c.put(x, y + 2, 'g'); c.put(x, y + 3, 'g')
|
||
c.put(40, y + 2, 'G' if i % 2 == 0 else 'R')
|
||
c.box(6, 38, 17, 42, 'd') # 电源
|
||
c.put(15, 40, 'S')
|
||
for x in range(20, 42, 2): # 底部走线
|
||
c.put(x, 40, 'm')
|
||
c.rect(2, 45, 45, 46, 'k') # 脚
|
||
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)
|