Cards: 3x2 front/back sprites, appearance overlays, flip via the Toggle menu

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 11:26:11 +08:00
1 parent 90f0075fd4
commit 42608a4acd
42 files changed
+315 -74

No files matched your search

+231 -67
View File
@@ -77,79 +77,208 @@ class Canvas:
f.write(data)
def gpu(accent, accent_dark, burnt=False):
"""显卡(32×32,2×2 格),略微俯视:顶面是带两个风扇的散热罩,左端金属挡板(带接口),
前沿露出 PCB 和金手指,右端一道暗侧面,顶上一排供电口。每格中心都不透明。"""
c = Canvas(32, 32)
TOP0, TOP1 = 4, 22 # 顶面行范围
L, R = 6, 29 # 顶面左右(左边是挡板)
# 金属 I/O 挡板(左端竖条,带两个接口孔)
c.rect(1, 2, 5, 27, 'b')
c.rect(1, 2, 1, 27, 'h'); c.rect(5, 2, 5, 27, 'd')
for y in (6, 12): # 两个视频接口(深色矩形孔,带高光边)
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'g')
c.put(3, 20, 'k'); c.put(3, 22, 'k'); c.put(3, 24, 'k') # 下部散热孔
# 散热罩顶面
for y in range(TOP0, TOP1 + 1):
for x in range(L, R + 1):
c.put(x, y, 'm')
c.put(R, y, 'd'); c.put(R - 1, y, 'd')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
# 品牌色条(沿罩顶前沿)
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, accent)
c.put(L + 1, TOP1, accent_dark); c.put(R - 2, TOP1, accent_dark)
# 两个风扇(圆形,叶片用十字 + 斜线)
for cx in (11, 22):
cy = 13
for y in range(cy - 6, cy + 7):
for x in range(cx - 6, cx + 7):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= 36:
c.put(x, y, 'k')
if 20 <= d2 <= 30:
c.put(x, y, 'g')
for k in range(-4, 5):
if abs(k) >= 1:
c.put(cx + k, cy, 'g'); c.put(cx, cy + k, 'g')
c.put(cx - 2, cy - 2, 'g'); c.put(cx + 2, cy + 2, 'g'); c.put(cx + 2, cy - 2, 'g'); c.put(cx - 2, cy + 2, 'g')
c.rect(cx - 1, cy - 1, cx + 1, cy + 1, 'b')
c.put(cx, cy, 'h')
# 前沿:PCB 绿色薄边 + 金手指
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'g2')
c.put(x, TOP1 + 2, 'g2')
for x in range(L, R + 1): # 金手指:连续金色带,中间一道缺口(PCIe 键位),带深色触点线
c.put(x, TOP1 + 3, 'Y'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'e')
for y in (TOP1 + 3, TOP1 + 4):
c.put(L + 13, y, 'k'); c.put(L + 14, y, 'k')
# 顶上一排供电口(两个黑色插口)
for x0 in (L + 3, L + 9):
c.rect(x0, TOP0 - 1, x0 + 4, TOP0 - 1, 'k')
# 保证每格中心不透明:下半部(第二行格子,y 16..31 的中心 y=24)落在金手指/PCB 上
for x in range(L, R + 1):
for y in range(TOP1 + 1, 28):
if c.px[y][x] is None:
c.put(x, y, 'k')
if burnt:
for x, y in [(9, 8), (10, 9), (13, 17), (24, 8), (25, 9), (27, 18), (17, 12), (18, 11)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, 'e')
for x, y in [(15, 5), (16, 5), (20, 21), (26, 14)]:
c.put(x, y, 'c')
# 轮廓
FONT = { # 3×5 像素字(丝印用)
'R': '110101110101101', 'Z': '111001010100111', 'X': '101101010101101', 'P': '110101110100100',
'M': '101111111101101', 'D': '110101101101110', 'T': '111010010010010', 'i': '010000010010010',
'0': '111101101101111', '1': '010110010010111', '2': '110001010100111', '3': '110001010001110',
'4': '101101111001001', '5': '111100110001110', '6': '011100111101111', '7': '111001010010010',
'8': '111101111101111', '9': '111101111001110',
}
def text(c, x, y, s, color, skew=False):
"""在画布上写 3×5 像素字;skew 为重新印过的效果:字一高一低、间距不匀。"""
i = 0
for ch in s:
if ch == ' ':
x += 2
continue
dy = (i % 2) if skew else 0
for k, bit in enumerate(FONT[ch]):
if bit == '1':
c.put(x + k % 3, y + k // 3 + dy, color)
x += 4 + (1 if skew and i % 3 == 2 else 0)
i += 1
W, H = 48, 32
TOP0, TOP1 = 4, 22 # 顶面行范围
L, R = 6, 45 # 顶面左右(左边是挡板)
FANS = ((17, 13), (34, 13)) # 两个风扇中心
def outline(c):
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')
def bracket(c):
"""左端金属挡板,三个接口孔。"""
c.rect(1, 2, 5, 27, 'b')
c.rect(1, 2, 1, 27, 'h'); c.rect(5, 2, 5, 27, 'd')
for y in (5, 11, 17):
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'g')
c.put(3, 22, 'k'); c.put(3, 24, 'k')
def gpu_front(accent, accent_dark, burnt=False):
"""显卡正面(48×32,3×2 格),略微俯视:顶面是带两个风扇的散热罩,左端金属挡板(三个接口),
前沿露出 PCB 和金手指,右端一道暗侧面。每格中心都不透明。"""
c = Canvas(W, H)
bracket(c)
for y in range(TOP0, TOP1 + 1):
for x in range(L, R + 1):
c.put(x, y, 'm')
c.put(R, y, 'd'); c.put(R - 1, y, 'd')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, accent)
c.put(L + 1, TOP1, accent_dark); c.put(R - 2, TOP1, accent_dark)
for cx, cy in FANS:
for y in range(cy - 7, cy + 8):
for x in range(cx - 7, cx + 8):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= 49:
c.put(x, y, 'k')
if 26 <= d2 <= 38:
c.put(x, y, 'g')
for k in range(-5, 6):
if abs(k) >= 1:
c.put(cx + k, cy, 'g'); c.put(cx, cy + k, 'g')
for dx, dy in ((-3, -3), (3, 3), (3, -3), (-3, 3)):
c.put(cx + dx, cy + dy, 'g')
c.rect(cx - 1, cy - 1, cx + 1, cy + 1, 'b')
c.put(cx, cy, 'h')
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'g2'); c.put(x, TOP1 + 2, 'g2')
for x in range(L, R + 1):
c.put(x, TOP1 + 3, 'Y'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'e')
for y in (TOP1 + 3, TOP1 + 4):
c.put(L + 17, y, 'k'); c.put(L + 18, y, 'k')
for x0 in (L + 4, L + 12): # 顶上供电口
c.rect(x0, TOP0 - 1, x0 + 4, TOP0 - 1, 'k')
for x in range(L, R + 1):
for y in range(TOP1 + 1, 28):
if c.px[y][x] is None:
c.put(x, y, 'k')
if burnt:
for x, y in [(12, 8), (13, 9), (18, 17), (35, 8), (36, 9), (40, 18), (24, 12), (25, 11)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, 'e')
outline(c)
return c
def gpu_back(accent, accent_dark, burnt=False):
"""显卡背面(翻面后):左端仍是挡板;黑色背板,上面一行丝印(型号用叠加图画),
右下一张序列号贴纸,贴纸边上一道封条。"""
c = Canvas(W, H)
bracket(c)
for y in range(TOP0, 28):
for x in range(L, R + 1):
c.put(x, y, 'd')
c.put(R, y, 'k'); c.put(R - 1, y, 'k')
for x in range(L, R + 1):
c.put(x, TOP0, 'm'); c.put(x, TOP0 + 1, 'm')
for x in range(L + 1, R - 1):
c.put(x, 16, accent)
c.put(L + 1, 17, accent_dark); c.put(R - 2, 17, accent_dark)
for x in range(L + 2, R - 2, 5): # 背板螺丝
for y in (22, 25):
c.put(x, y, 'b'); c.put(x + 1, y, 'k')
c.rect(26, 18, 41, 21, 'h') # 序列号贴纸(印刷条码)
for x in range(27, 41, 2):
c.rect(x, 19, x, 20, 'k')
c.rect(44, 17, 44, 22, 'R') # 封条(红色)
c.rect(43, 17, 43, 22, 'r2')
for x in range(L, R + 1):
for y in range(28, 28):
pass
if burnt:
for x, y in [(14, 9), (15, 10), (30, 11), (36, 13), (20, 14)]:
c.put(x, y, 'c')
outline(c)
return c
def overlay():
return Canvas(W, H)
def ov_fan(kind):
"""风扇叠加:1 积灰(叶片上一层灰),2 叶片缺角(边缘缺一块并露出黑底)。"""
c = overlay()
for cx, cy in FANS:
if kind == 1:
for dx, dy in ((-4, -2), (-2, -4), (3, -3), (4, 1), (-3, 3), (2, 4), (-5, 0), (0, 5), (1, -5), (-1, 2), (5, -1), (3, 2)):
c.put(cx + dx, cy + dy, 'l')
for dx, dy in ((-4, -1), (-1, -4), (4, -2), (2, 3), (-3, 4)):
c.put(cx + dx, cy + dy, 'h')
else:
for dx, dy in ((4, -4), (5, -4), (5, -3), (4, -3), (3, -4), (5, -5), (6, -3)):
c.put(cx + dx, cy + dy, 'k')
c.put(cx - 5, cy + 4, 'k'); c.put(cx - 4, cy + 5, 'k'); c.put(cx - 5, cy + 5, 'k')
return c
def ov_fingers(kind):
"""金手指叠加:1 发暗(触点变褐),2 氧化发绿。"""
c = overlay()
for x in range(L, R + 1):
for y in (TOP1 + 3, TOP1 + 4):
if x in (L + 17, L + 18):
continue
if kind == 1:
c.put(x, y, 'e' if (x + y) % 3 else 'Y')
else:
c.put(x, y, 'g2' if (x + y) % 2 else 'G')
return c
def ov_bracket(kind):
"""挡板叠加:1 划痕;2 接口数量不对(第三个接口被封成一块平板,只剩两个)。"""
c = overlay()
if kind == 1:
for x, y in ((2, 9), (3, 10), (4, 11), (2, 15), (3, 15), (3, 16), (2, 20), (3, 21), (4, 22), (4, 25), (3, 25)):
c.put(x, y, 'l' if (x + y) % 2 else 'd')
else:
c.rect(2, 17, 4, 20, 'b')
c.rect(2, 17, 4, 17, 'h')
return c
def ov_text(model, skew):
"""背面丝印叠加:写型号。skew 为重新印过的版本(字高低不齐)。"""
c = overlay()
text(c, 10, 8, model, 'l' if skew else 'h', skew)
return c
def ov_sticker(kind):
"""贴纸叠加:1 手写序列号(条码被盖住、写了歪歪扭扭的字);2 封条缺失(只剩撕过的残胶)。"""
c = overlay()
if kind == 1:
c.rect(26, 18, 41, 21, 'h')
for x, y in ((27, 20), (28, 19), (29, 20), (30, 19), (32, 20), (33, 19), (33, 21), (35, 19), (36, 20), (37, 19), (38, 21), (39, 20), (40, 19)):
c.put(x, y, 'O')
else:
c.rect(43, 17, 44, 22, 'd')
for y in (17, 19, 21, 22):
c.put(44, y, 'm')
c.put(43, 18, 'm'); c.put(43, 20, 'm')
return c
MODELS = ['RZX 50', 'RZX 60', 'RZX 70', 'RZX 80 Ti', 'RZX 90', 'RZ 470', 'RZ 580', 'RZ 6800', 'RZ 7900']
def rig():
"""矿架(48×48,3×3 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
@@ -283,14 +412,33 @@ def terminal():
return c
def key(model):
return model.replace(' ', '').lower()
SPRITES = {
'gpu_probidia': lambda: gpu('G', 'g2'),
'gpu_pmd': lambda: gpu('R', 'r2'),
'gpu_burnt': lambda: gpu('e', 'c', burnt=True),
'gpu_probidia_f': lambda: gpu_front('G', 'g2'),
'gpu_probidia_b': lambda: gpu_back('G', 'g2'),
'gpu_pmd_f': lambda: gpu_front('R', 'r2'),
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
'gpu_burnt_f': lambda: gpu_front('e', 'c', burnt=True),
'gpu_burnt_b': lambda: gpu_back('e', 'c', burnt=True),
'rig': rig,
'terminal': terminal,
}
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
OVERLAYS = {
'gpu_ov_fan1': lambda: ov_fan(1), 'gpu_ov_fan2': lambda: ov_fan(2),
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
'gpu_ov_brk1': lambda: ov_bracket(1), 'gpu_ov_brk2': lambda: ov_bracket(2),
'gpu_ov_stk1': lambda: ov_sticker(1), 'gpu_ov_stk2': lambda: ov_sticker(2),
}
for _m in MODELS:
OVERLAYS['gpu_ov_txt_' + key(_m)] = (lambda m=_m: ov_text(m, False))
OVERLAYS['gpu_ov_txs_' + key(_m)] = (lambda m=_m: ov_text(m, True))
def check_cells(canvas, name):
"""游戏按 mod 物品图每格的中心像素是否不透明来判断占格(行从图的下沿往上数)。有空格就报错。"""
cols, rows = canvas.w // 16, canvas.h // 16
@@ -306,6 +454,20 @@ if __name__ == '__main__':
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(sys.argv[2], name + '.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
# 预览:把各种叠加图叠在底图上
def stack(base, *ovs):
b = SPRITES[base]()
for o in ovs:
oc = OVERLAYS[o]()
for y in range(oc.h):
for x in range(oc.w):
if oc.px[y][x] is not None:
b.px[y][x] = oc.px[y][x]
return b
stack('gpu_probidia_f', 'gpu_ov_fan1', 'gpu_ov_fin1', 'gpu_ov_brk1').png(os.path.join(sys.argv[2], 'prev_front_worn.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
stack('gpu_pmd_f', 'gpu_ov_fan2', 'gpu_ov_fin2', 'gpu_ov_brk2').png(os.path.join(sys.argv[2], 'prev_front_fake.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
stack('gpu_probidia_b', 'gpu_ov_txt_rzx80ti').png(os.path.join(sys.argv[2], 'prev_back_ok.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
stack('gpu_pmd_b', 'gpu_ov_txs_rz6800', 'gpu_ov_stk1', 'gpu_ov_stk2').png(os.path.join(sys.argv[2], 'prev_back_fake.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'))
@@ -313,4 +475,6 @@ if __name__ == '__main__':
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(out, name + '.png'))
for name, make in OVERLAYS.items():
make().png(os.path.join(out, name + '.png'))
print(out)