31 Commits
Author SHA1 Message Date
ZHU MINCHAOandClaude Sonnet 5.5 45f73580a5 Changelog: phase 1 complete
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:45:50 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e95588a76f Terminal: stablecoin price/status table in Web3; AML preview (window total, taint share, review chance) in the bank withdrawal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:44:49 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 00f7811e7c Terminal: two-column pages with table cells (holdings on the left, trading on the right)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:41:42 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 a2dc1dd2f6 Terminal: buy-average price per coin in Poinbase and Web3; drop the price line on the Poinbase card
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:35:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9441d4d1bd Terminal: home with three entries (Bank, Poinbase, Web3) plus report; entry icons
Bank shows real credits, rent and deposits/withdrawals; Poinbase is the
compliant exchange; Web3 is the wallet and decentralised trading.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:23:15 +08:00
ZHU MINCHAO ce95ec923d Terminal: fixed-width amount field 2026-11-03 15:15:19 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 0560c34231 Halving now raises the price anchor (pass-through 0.6, ramp the week before) with a pre-halving news item
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:13:57 +08:00
ZHU MINCHAO 967077a2db comment: 50% 2026-11-03 15:06:03 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 4b10c6ad7f Buyer spots each hidden flaw with 50% chance
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:06:00 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9165c9edf4 Reputation loss when a buyer catches a false label scales with the price gap (per weekly rent, 1 to 25); drop old-save model fallback
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:03:52 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 393dd8057a Seller claim lapses when the card is bought unless edited with a labeler; test helpers for sell and buy sides
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:57:28 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f564a895d0 Label bar: one model-authenticity row with a hint line; evidence listed after exposure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:30:47 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d6a456c986 Exposing the model reveals all four authenticity rows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:28:39 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 559a702ee3 Label bar: authenticity rows first, quality rows second; single-fan brand text moved off the fan
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:19:43 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 7f458b0139 Tiers drive physical traits and serial checks; guide item
Fans (1/2/3/3) and power connectors (6/8/8/8+8) follow the real chip's tier,
so a fake shows its real tier physically. Serial numbers (brand prefix, tier
code, factory code, four digits) are drawn on the sticker from card state;
the label bar gets power-and-fans and serial rows, eight in total, with a
framed panel. A graphics card appraisal guide item holds the rules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:18:01 +08:00
ZHU MINCHAO 9d19df407a Labels: eight rows (model, build, fan blades, contacts, bracket, print, serial, seal); serial text rules by brand and tier 2026-11-03 14:07:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 202bdbbfbc Four tiers per brand: RZX 60/70/80/90 and RZ 70/80/800/900
Model name is display only; performance and price follow brand and tier.
Tier physical traits: power pins 6/8/8/8+8 and fans 1/2/3/3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:05:47 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 0ba2845609 Inspection: hide the tray's own magnifier icon for cards; fill the gaps between back gold fingers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:55:22 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 fe72ec0577 Inspection tray: framed label panel with bright text, flip button at the bottom
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:53:56 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 b359a00789 Label bar in the magnifier tray: seller's claims per label, point out, edit with a labeler
Six labels (model, fan, contacts, bracket, back print, sticker). Dealers
state claims (honest tell the truth, middlemen hide 70%, scammers hide
everything and name the stamped model); the price follows claims until a
label is pointed out, then follows the real state. Pointing at a fake's
evidence exposes the model label too. A wrong accusation costs reputation.
Owned cards can have claims edited freely with a labeler present; a buyer
spots each hidden flaw with 35% chance, which exposes it and costs
reputation. Dimensions are additive percent adjustments on the model price.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:50:24 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f93dbc6eec Cards: appearance follows wear, hash depends on fan and contacts, seller confrontation
Fan, contact and scratch levels are recomputed from health and a per-card
seed, so a card looks older as it wears. Fan and contacts scale hashrate.
Each inspection-tray detail now maps to exactly one dimension value. Quoting
dealers price the card as if its condition were fine; the player points out
fan, contacts, bracket, print or sticker in the tray to take the matching
discount or expose a fake (a wrong accusation costs reputation like the
vanilla confrontation). Unowned cards show a coarse grade and rated specs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:37:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 68265095a8 Cards: coarse condition grade in the description until the card is owned
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:23:54 +08:00
ZHU MINCHAO 821eb1b960 Appraisal test helpers: put a client with appraisal cards in front of the counter 2026-11-03 13:20:11 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 2be4c90c8e GPU art: drop the diagonal stub at the end of the accent stripe; appraisal test helpers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:11:41 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 6b2d394023 Inspection overlays designed by agy; appearance dimensions tied to value, wear and death
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:04:45 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 dd95aad7aa Dedicated large inspection art for graphics cards
192x128 drawings made for the magnifier tray instead of enlarging the
48x32 item sprite: nine-blade fans, fin stack, heat pipes, power connector,
PCB parts, per-contact gold fingers; back with brushed backplate, QR and
barcode sticker, seal. Appearance overlays share coordinates with the base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:21:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 c05fe41ca9 Magnifier inspection view for graphics cards
Cards are inspectable. While the magnifier inspection tray shows a card, a
mod-owned panel under the tray draws the card enlarged with its appearance
overlays and a turn-over button. No Harmony patches: a resident component
watches the inspection manager.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:08:51 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e643332a41 GPU art: redraw front and back with real detail
Six-blade fans with rim, air duct and hub, graphite shroud with fin stack,
copper heat-pipe ends and an 8-pin connector, PCB with parts and per-contact
gold fingers; back has brushed backplate, vents, screws, recessed silkscreen
plate, QR/barcode sticker and a seal. Overlays realigned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:04:08 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 de7d5caafe Used cards: counter quote tag instead of client-temporary feature; item type Material
Client-temporary features are stripped when an item sits on the front
counter, so the scam markup never applied. The quote now lives in a tag and
counts only while the item is not owned. GPU item type ELECTRONIC has no
translation in the game, so cards are typed as Material.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:00:39 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9b48d7bf0e Used-hardware dealer customer and GPU appraisal registration
A scav-type seller visits now and then with three to five used cards. An
honest dealer sells at true value, a middleman adds 8%, a scammer mixes in
fakes (relabelled cheap chips) priced at 90% of the labelled model's value.
The markup is a client-temporary item feature, so a bought card is only
worth its real value. Cards are registered as appraisal items; the correct
value is the same function as the sale value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 11:40:10 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 42608a4acd Cards: 3x2 front/back sprites, appearance overlays, flip via the Toggle menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 11:26:11 +08:00
121 changed files with 3733 additions and 167 deletions

No files matched your search

+13
View File
@@ -2,6 +2,19 @@
## 未发布
### 第一阶段完成(入门、挖矿、显卡鉴定、终端)
以下条目取代下面较早条目里已过时的部分(型号、入口价、终端页面):
- 显卡改成每个牌子四档(低端、中端、高端、旗舰):Probidia RZX 60 / 70 / 80 / 90,PMD RZ 70 / 80 / 800 / 900;型号只是显示名,性能由牌子和档次定;档次对应供电口(6 / 8 / 8 / 8+8 针)和风扇数(1 / 2 / 3 / 3)。显卡 3×2 格,正反面各一张图;价格约为最初的 1.8 倍,出块奖励 90。
- 显卡外观:叶片、金手指、挡板、封条,随每晚损耗变差,影响价值、算力、损耗、暴毙。
- 二手卡贩子(老实人、中间商、骗子)、假卡(低档芯片套高档壳)、序列号(牌子前缀、档次码、工厂码、流水)、背面丝印、鉴定公会收显卡、显卡鉴定手册。
- 放大镜检视显卡:专门的大图和标签栏(型号真伪加叶片、金手指、挡板、封条四项成色),卖家说法与逐项指出、标签机改说法、买家验货与按价差扣信誉。
- 减半抬高价格锚(传导 0.6,减半前一周有预期行情,减半前 3 天上报纸)。
- 交易终端重做:首页三个入口(银行、Poinbase、Web3)加报告;左右分栏表格;买入均价;Web3 页的稳定币价格与状态;银行提取前的审查提示;金额输入框加宽。
- 入口升级价 400,开局送交易终端、矿架、RZX 60 和鉴定手册。
- 经济模拟 v7(运维库 research/sim/out_v7.md):电网电价下 RZX 70 挖矿第 30 天占总收入 18%、第 60 天 20%。
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
- mod 本体(第一阶段的第一部分):人脉网络"Web3 治理社区"(300,整个 mod 的入口)交付交易终端和一张 RZX 50;矿架向杰克逊博士购买;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSC;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSC 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。
Binary file not shown.
Binary file not shown.
Binary file not shown.
+439
View File
@@ -0,0 +1,439 @@
#!/usr/bin/env python3
"""显卡检视大图(192×128):放大镜检视台里用的细节图,不是小图的放大版。
正面:七叶风扇、鳍片、热管、供电口、PCB 元件与逐根金手指;背面:拉丝背板、丝印凹槽、二维码与条码贴纸、封条。
叠加图与底图同尺寸、坐标一致,只画变化的像素。"""
import math
import random
from sprites import Canvas, C, key, MODELS
BW, BH = 192, 128
# 风扇布局(按风扇数):中心与半径;FANS / FR 保留为双风扇的值,给叠加图模块当默认
FAN_LAYOUT = {1: ([(105, 55)], 34), 2: ([(68, 51), (142, 51)], 30), 3: ([(52, 51), (96, 51), (140, 51)], 21)}
FANS, FR = FAN_LAYOUT[2][0], FAN_LAYOUT[2][1]
SH0, SH1 = (24, 14), (186, 96) # 散热罩 左上 / 右下
FING_Y0, FING_Y1 = 109, 122
NOTCH_X = 112
# 5×7 像素字(丝印与品牌字)
F57 = {
'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
'Z': ['11111', '00001', '00010', '00100', '01000', '10000', '11111'],
'X': ['10001', '10001', '01010', '00100', '01010', '10001', '10001'],
'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
'i': ['00100', '00000', '01100', '00100', '00100', '00100', '01110'],
'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
'I': ['01110', '00100', '00100', '00100', '00100', '00100', '01110'],
'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
'0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
'1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
'2': ['01110', '10001', '00001', '00110', '01000', '10000', '11111'],
'3': ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
'5': ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
'6': ['00110', '01000', '10000', '11110', '10001', '10001', '01110'],
'7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
'8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
'9': ['01110', '10001', '10001', '01111', '00001', '00010', '01100'],
}
def text57(c, x, y, s, color, scale=1, skew=False, gap=1):
i = 0
for ch in s:
if ch == ' ':
x += 3 * scale
continue
dy = (i % 2) * scale if skew else 0
for r, row in enumerate(F57[ch]):
for k, bit in enumerate(row):
if bit == '1':
c.rect(x + k * scale, y + r * scale + dy, x + k * scale + scale - 1, y + r * scale + scale - 1 + dy, color)
x += (5 + gap) * scale + (scale if skew and i % 3 == 2 else 0)
i += 1
def outline(c, col='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, col)
def bracket(c):
"""左端 I/O 挡板:折边、三个接口(HDMI 与两个 DP,内腔与触片)、散热网孔、底部螺丝。"""
c.rect(2, 8, 22, 112, 'b')
c.rect(2, 8, 4, 112, 'l'); c.rect(2, 8, 2, 112, 'h') # 受光折边
c.rect(19, 8, 22, 112, 'd'); c.rect(21, 8, 22, 112, 'g') # 背光折边
for x in range(5, 19): # 拉丝
for y in range(9, 112, 2):
c.put(x, y, 'b' if (x + y) % 5 else 'l')
for i, y in enumerate((16, 36, 56)): # 三个接口
c.rect(6, y, 17, y + 13, 'k')
c.rect(6, y, 17, y, 'g'); c.rect(6, y, 6, y + 13, 'g')
c.rect(7, y + 1, 16, y + 12, 'n')
if i == 0: # HDMI:两侧斜角
for k in range(3):
c.put(7 + k, y + 12 - k, 'k'); c.put(16 - k, y + 12 - k, 'k')
c.rect(9, y + 4, 14, y + 6, 'z')
else: # DP:一边切角
c.put(16, y + 12, 'k'); c.put(15, y + 12, 'k'); c.put(16, y + 11, 'k')
c.rect(9, y + 4, 14, y + 5, 'z')
for x in range(8, 16, 2):
c.put(x, y + 9, 'Y')
for y in range(78, 104, 4): # 散热网孔
for x in range(7, 17, 3):
c.rect(x, y, x + 1, y + 2, 'k')
c.put(x, y, 'x')
c.rect(9, 106, 14, 110, 'm'); c.rect(10, 107, 13, 109, 'g') # 底部螺丝
c.put(11, 108, 'k'); c.put(12, 108, 'k'); c.put(11, 107, 'b')
c.rect(9, 10, 14, 12, 'k')
def fan(c, cx, cy, accent, accent_dark, R=None):
"""风扇:九片弯曲扇叶(前缘高光、叶面渐变、叶尖暗边),外圈带四个固定螺丝,轴心带品牌圆标。"""
R = R or FR
for y in range(cy - R - 1, cy + R + 2):
for x in range(cx - R - 1, cx + R + 2):
dx, dy = x - cx, y - cy
r = math.hypot(dx, dy)
if r > R + 0.5:
continue
ang = (math.atan2(dy, dx) / (2 * math.pi)) % 1.0
light = -(dx + dy) / (1.4 * R) # 左上为正
if r >= R - 3.2: # 外圈
c.put(x, y, 'h' if light > 0.5 else 'l' if light > 0.1 else 'b' if light > -0.4 else 'm')
elif r >= R - 5.5: # 风道阴影
c.put(x, y, 'n' if light < 0.2 else 'k')
elif r <= 6.5: # 轴心
if r <= 4.3:
c.put(x, y, accent if light < 0.3 else accent_dark if r > 3 else 'w')
else:
c.put(x, y, 'h' if light > 0.2 else 'm' if light > -0.3 else 'd')
else:
t = (ang * 9 + (r - 7) * 0.060) % 1.0
if t < 0.62:
if t < 0.06:
col = 'h'
elif t < 0.40:
col = 'l' if light > 0.25 else 'b' if light > -0.2 else 'm'
else:
col = 'b' if light > 0.2 else 'm' if light > -0.3 else 'g'
if r > R - 8:
col = 'g' if col in ('m', 'g') else 'm'
c.put(x, y, col)
else:
c.put(x, y, 'n' if t < 0.9 else 'k')
for k in range(4): # 外圈固定螺丝
a = math.pi / 4 + k * math.pi / 2
sx, sy = int(cx + math.cos(a) * (R - 1.5)), int(cy + math.sin(a) * (R - 1.5))
c.rect(sx - 1, sy - 1, sx + 1, sy + 1, 'k')
c.put(sx, sy, 'g')
def gpu_front(accent, accent_dark, brand_text, tier=1, burnt=False):
"""tier 决定实物特征:风扇数(1/2/3/3)与供电口(6 针 / 8 针 / 8 针 / 双 8 针)。"""
count = (1, 2, 3, 3)[tier]
fans, R = FAN_LAYOUT[count]
c = Canvas(BW, BH)
bracket(c)
x0, y0 = SH0
x1, y1 = SH1
# 散热罩:石墨底,顶部受光亮边,左缘高光,右下阴影;右上角切斜
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
if x > x1 - 8 and y < y0 + (x - (x1 - 8)): # 右上切角
continue
base = 'z'
if y < y0 + 3: base = 'h' if x < x1 - 40 else 'l'
elif y < y0 + 6: base = 'l' if x < x1 - 40 else 'b'
elif y < y0 + 9: base = 'x'
elif y > y1 - 4: base = 'n'
c.put(x, y, base)
for y in range(y0 + 3, y1): # 左缘
c.put(x0, y, 'l'); c.put(x0 + 1, y, 'b'); c.put(x0 + 2, y, 'x')
# 右端鳍片
for x in range(x1 - 18, x1 + 1):
for y in range(y0 + 10, y1 - 4):
if x > x1 - 8 and y < y0 + (x - (x1 - 8)) + 2:
continue
c.put(x, y, 'x' if (x - (x1 - 18)) % 3 == 0 else 'g' if (x - (x1 - 18)) % 3 == 1 else 'n')
c.rect(x1 - 20, y0 + 10, x1 - 19, y1 - 4, 'x')
# 风扇之间的中筋
for a, b in zip(fans, fans[1:]):
mx = (a[0] + b[0]) // 2 + 1
c.rect(mx - 1, y0 + 8, mx, y1 - 5, 'k'); c.rect(mx + 1, y0 + 8, mx + 1, y1 - 5, 'x')
for cx, cy in fans:
fan(c, cx, cy, accent, accent_dark, R)
# 前沿斜切装饰条:品牌色 + 品牌字
for x in range(x0 + 4, x1 - 20):
c.rect(x, y1 - 5, x, y1 - 3, accent if x % 11 else accent_dark)
text57(c, 30 if count == 1 else 112, 84, brand_text, 'h') # 单风扇的大风扇占了右边,品牌字改放左下
# 顶沿:四根铜热管端头与 8 针供电口
for i in range(4):
x = 38 + i * 9
c.rect(x, 7, x + 6, y0 - 1, 'p'); c.rect(x, 7, x + 1, y0 - 1, 'h' if i == 0 else 'p')
c.rect(x + 5, 7, x + 6, y0 - 1, 'q'); c.rect(x, 7, x + 6, 7, 'q')
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针
for left, pins in ((((118, 6),) if tier == 0 else ((112, 8),) if tier < 3 else ((78, 8), (126, 8)))):
right = left + pins * 5 + 2
c.rect(left, 5, right, y0 - 1, 'n'); c.rect(left, 5, right, 6, 'z')
for i in range(pins):
c.rect(left + 2 + i * 5, 7, left + 4 + i * 5, 9, 'k'); c.put(left + 3 + i * 5, 8, 'Y')
# PCB:阻焊绿、边缘高光、元件与走线
for y in range(y1 + 1, FING_Y0):
for x in range(x0, x1 + 1):
c.put(x, y, 'G' if y < y1 + 4 else 'g2')
for x in range(x0, x1 + 1):
c.put(x, y1 + 1, 'T') if False else None
random.seed(7)
for i in range(14):
sx = x0 + 6 + i * 11
c.rect(sx, y1 + 3, sx + 3, y1 + 5, 'k' if i % 3 else 'b')
c.rect(sx, y1 + 3, sx + 3, y1 + 3, 'x')
for k in range(4):
c.put(sx + k, y1 + 7, 'Y' if k % 2 == 0 else 'g2')
# 金手指:逐根(3 像素宽,1 像素缝),PCIe 键位缺口
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(FING_Y0, FING_Y1 + 1):
c.put(x, y, 'h' if y == FING_Y0 else 'Y' if (x - x0) % 4 != 0 else 'p')
c.put(x, FING_Y1, 'e')
c.rect(x0, FING_Y0, x1, FING_Y0, 'g2')
for y in range(FING_Y0 + 1, FING_Y1 + 1): # 缝里露出 PCB
for x in range(x0 + 2, x1 - 2):
if c.px[y][x] is None and not (NOTCH_X - 2 <= x <= NOTCH_X + 3):
c.put(x, y, 'g2')
if burnt:
for cx, cy in FANS:
for dx, dy in ((0, 0), (5, -5), (-6, 6), (8, 4), (-9, -3), (2, 10)):
c.rect(cx + dx - 2, cy + dy - 2, cx + dx + 2, cy + dy + 2, 'c')
for x in range(x0 + 4, x1 - 20):
c.rect(x, y1 - 5, x, y1 - 3, 'e')
for dx, dy in ((30, 70), (90, 40), (150, 80), (60, 30), (120, 88)):
c.rect(dx, dy, dx + 4, dy + 3, 'c')
outline(c)
return c
def screw(c, x, y):
c.rect(x - 3, y - 3, x + 3, y + 3, 'm')
c.rect(x - 3, y - 3, x - 1, y - 3, 'h')
c.rect(x - 2, y - 2, x + 2, y + 2, 'b')
c.rect(x - 2, y, x + 2, y, 'k'); c.rect(x, y - 2, x, y + 2, 'k')
c.rect(x + 2, y + 2, x + 3, y + 3, 'd')
def qr(c, x, y, seed):
rnd = random.Random(seed)
n = 21
c.rect(x - 1, y - 1, x + n, y + n, 'w')
for j in range(n):
for i in range(n):
on = rnd.random() < 0.5
for fx, fy in ((0, 0), (n - 7, 0), (0, n - 7)):
if fx <= i < fx + 7 and fy <= j < fy + 7:
li, lj = i - fx, j - fy
on = li in (0, 6) or lj in (0, 6) or (2 <= li <= 4 and 2 <= lj <= 4)
elif fx - 1 <= i <= fx + 7 and fy - 1 <= j <= fy + 7:
on = False
if on:
c.put(x + i, y + j, 'n')
def sticker(c, seed):
"""序列号贴纸(右下):二维码、条码、两行小字。"""
c.rect(86, 64, 172, 100, 'w')
c.rect(86, 64, 172, 65, 'h'); c.rect(86, 99, 172, 100, 'l')
c.rect(86, 64, 87, 100, 'h'); c.rect(171, 64, 172, 100, 'l')
qr(c, 91, 70, seed)
for x in range(118, 166):
w = 1 + ((x * 7 + seed) % 3)
if (x * 5 + seed) % 4 != 0:
c.rect(x, 70, x, 84, 'n')
def seal(c):
"""封条(红底白字):压住贴纸右边缘。"""
c.rect(174, 60, 182, 104, 'R')
c.rect(174, 60, 175, 104, 'r2'); c.rect(181, 60, 182, 104, 'r2')
for y in range(66, 100, 8):
c.rect(176, y, 180, y + 3, 'w')
c.put(178, y + 1, 'R')
def gpu_back(accent, accent_dark, burnt=False):
c = Canvas(BW, BH)
bracket(c)
x0, y0 = SH0
x1, y1 = SH1
for y in range(y0, y1 + 12):
for x in range(x0, x1 + 1):
c.put(x, y, 'd' if y % 2 else 'm' if (x // 3) % 4 else 'd')
for y in range(y0, y1 + 12):
c.put(x0, y, 'l'); c.put(x0 + 1, y, 'b'); c.put(x1, y, 'k'); c.put(x1 - 1, y, 'z')
for x in range(x0, x1 + 1):
c.put(x, y0, 'h'); c.put(x, y0 + 1, 'l'); c.put(x, y0 + 2, 'b')
# 丝印凹槽(型号叠加图写在这里)
c.rect(36, 24, 178, 52, 'z')
c.rect(36, 24, 178, 25, 'k'); c.rect(36, 24, 37, 52, 'k')
c.rect(36, 52, 178, 52, 'x'); c.rect(178, 24, 178, 52, 'x')
# 品牌色斜条
for x in range(x0 + 4, x1 - 4):
c.rect(x, 56, x, 59, accent)
c.put(x, 60, accent_dark)
# 通风槽(左下)
for y in range(70, 100, 6):
c.rect(x0 + 10, y, x0 + 50, y + 3, 'k')
c.rect(x0 + 10, y + 4, x0 + 50, y + 4, 'x')
# 四角螺丝
for sx, sy in ((x0 + 12, y0 + 10), (x1 - 12, y0 + 10), (x0 + 12, y1 - 2), (x1 - 12, y1 - 2)):
screw(c, sx, sy)
sticker(c, 5)
seal(c)
# 背面金手指(暗一档):触点之间是 PCB 的深绿,只有键位缺口是空的
for y in range(y1 + 12, FING_Y1 + 1):
for x in range(x0, x1 + 1):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 and y > y1 + 13:
continue
c.put(x, y, 'g2')
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(y1 + 14, FING_Y1 + 1):
c.put(x, y, 'Y' if (x - x0) % 4 else 'p')
if burnt:
for dx, dy in ((60, 30), (130, 40), (50, 80), (100, 70), (150, 90)):
c.rect(dx, dy, dx + 6, dy + 4, 'c')
outline(c)
return c
def new():
return Canvas(BW, BH)
# ---------- 叠加图 ----------
def ov_fan(kind):
c = new()
rnd = random.Random(11 + kind)
for cx, cy in FANS:
if kind == 1: # 积灰:叶片上一层灰点和灰絮
for _ in range(190):
a = rnd.random() * math.tau
r = 8 + rnd.random() * (FR - 12)
x, y = int(cx + math.cos(a) * r), int(cy + math.sin(a) * r)
c.put(x, y, 'l' if rnd.random() < 0.6 else 'h')
if rnd.random() < 0.3:
c.put(x + 1, y, 'l')
for _ in range(10): # 絮状长条
a = rnd.random() * math.tau
r = 10 + rnd.random() * (FR - 16)
x, y = int(cx + math.cos(a) * r), int(cy + math.sin(a) * r)
for k in range(5):
c.put(x + k, y + (k // 2), 'w' if k % 2 == 0 else 'l')
else: # 缺角:一片叶尖缺一块,露出风道黑底
a0 = 0.9 if cx < 100 else 3.2
for r in range(10, FR - 3):
for da in (-0.20, -0.17, -0.13, -0.10, -0.06, -0.02, 0.02, 0.06, 0.10, 0.14):
if r > FR - 14 + (da * 30) and r > 17:
c.put(int(cx + math.cos(a0 + da) * r), int(cy + math.sin(a0 + da) * r), 'n')
for r in range(17, FR - 3):
c.put(int(cx + math.cos(a0 - 0.22) * r), int(cy + math.sin(a0 - 0.22) * r), 'h')
return c
def ov_fingers(kind):
c = new()
rnd = random.Random(21 + kind)
x0, x1 = SH0[0], SH1[0]
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(FING_Y0 + 1, FING_Y1 + 1):
if kind == 1: # 发暗:触点变褐
c.put(x, y, 'e' if (x + y) % 3 else 'q')
else: # 氧化:发绿的斑块
if rnd.random() < 0.65:
c.put(x, y, 'g2' if rnd.random() < 0.5 else 'G')
return c
def ov_bracket(kind):
c = new()
rnd = random.Random(31)
if kind == 1: # 划痕
for _ in range(9):
x, y = rnd.randint(5, 17), rnd.randint(14, 100)
for k in range(rnd.randint(5, 12)):
c.put(x + k // 2, y + k, 'h' if k % 2 else 'k')
else: # 接口数不对:第三个接口被板子封死
c.rect(6, 56, 17, 69, 'b')
c.rect(6, 56, 17, 56, 'h'); c.rect(6, 56, 6, 69, 'l'); c.rect(17, 56, 17, 69, 'd')
for x in range(7, 17, 2):
c.put(x, 62, 'm')
return c
def ov_text(model, skew):
c = new()
text57(c, 46, 31, model, 'l' if skew else 'h', scale=2, skew=skew, gap=1)
return c
def ov_sticker(kind):
c = new()
if kind == 1: # 手写序列号
c.rect(86, 64, 172, 100, 'w')
c.rect(86, 64, 172, 65, 'h'); c.rect(86, 99, 172, 100, 'l')
c.rect(86, 64, 87, 100, 'h'); c.rect(171, 64, 172, 100, 'l')
rnd = random.Random(41)
for row in (74, 84, 92):
x = 92
while x < 164:
w = rnd.randint(3, 7)
for k in range(w):
c.put(x + k, row + int(math.sin(k + x) * 1.5), 'O')
c.put(x + k, row + 1 + int(math.sin(k + x) * 1.5), 'O')
x += w + rnd.randint(2, 4)
else: # 封条缺失:残留的胶痕与撕口
c.rect(174, 60, 182, 104, 'd')
for y in range(60, 105, 3):
c.rect(174, y, 178 + (y % 5), y + 1, 'm')
for y in range(62, 104, 6):
c.rect(179, y, 181, y + 1, 'x')
return c
BIG = {}
for _b, (_a, _ad, _t) in {'probidia': ('G', 'g2', 'PROBIDIA'), 'pmd': ('R', 'r2', 'PMD')}.items():
for _tier in range(4):
BIG['gpu_big_%s_f_t%d' % (_b, _tier)] = (lambda a=_a, ad=_ad, t=_t, k=_tier: gpu_front(a, ad, t, k))
BIG['gpu_big_%s_b' % _b] = (lambda a=_a, ad=_ad: gpu_back(a, ad))
BIG['gpu_big_burnt_f'] = lambda: gpu_front('e', 'c', 'PROBIDIA', 1, burnt=True)
BIG['gpu_big_burnt_b'] = lambda: gpu_back('e', 'c', burnt=True)
BIG_OV = {}
if __name__ == '__main__':
import sys, os
if len(sys.argv) == 3 and sys.argv[1] == '--preview':
d = sys.argv[2]
def stack(base, *ovs):
b = BIG[base]()
for o in ovs:
oc = BIG_OV[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
bg = (0x6A, 0x72, 0x70)
for t in range(4):
stack('gpu_big_probidia_f_t%d' % t).png(os.path.join(d, 'big_f_t%d.png' % t), scale=3, bg=bg)
+1159
View File
File diff suppressed because it is too large. Load diff
+446 -67
View File
@@ -30,6 +30,17 @@ C = {
'Y': (0xC8, 0xA4, 0x4A), # 金手指
'c': (0x2E, 0x2A, 0x2A), # 焦黑
'e': (0x6B, 0x4A, 0x3A), # 焦痕
'u': (0x4F, 0x86, 0xD0), # 蓝
'U': (0x2B, 0x4A, 0x82), # 深蓝
'v': (0x9B, 0x76, 0xD6), # 紫
'V': (0x5E, 0x45, 0x8F), # 深紫
'y': (0xF0, 0xCF, 0x64), # 亮金
'p': (0xC0, 0x7A, 0x4A), # 铜
'q': (0x8A, 0x4F, 0x34), # 暗铜
'w': (0xE6, 0xE2, 0xD0), # 白
'n': (0x1E, 0x1B, 0x25), # 近黑
'x': (0x4A, 0x4E, 0x58), # 石墨亮边
'z': (0x2D, 0x2F, 0x38), # 石墨暗
}
@@ -77,79 +88,300 @@ 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(2, 2, 2, 27, 'l'); c.rect(5, 2, 5, 27, 'd'); c.rect(4, 2, 4, 27, 'm')
for y in (5, 11, 17): # 三个接口:外框亮边、内腔深色、中间一条触片
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'h'); c.put(2, y + 1, 'g'); c.put(4, y + 1, 'g')
c.put(3, y + 2, 'd'); c.put(3, y + 3, 'g')
for y in range(20, 27, 2): # 散热网孔
c.put(2, y, 'k'); c.put(4, y, 'k')
c.put(3, y + 1, 'k')
c.put(3, 3, 'k'); c.put(3, 27, 'g') # 顶部卡扣与底部螺丝
c.put(2, 27, 'h'); c.put(4, 27, 'd')
def fan(c, cx, cy, accent, burnt=False, R=7):
"""风扇:深色风道里六片弯曲扇叶(前缘高光、叶面按受光渐变),外圈细边,轴心带品牌色点;光从左上来。"""
import math
k = 7.0 / R # 按半径折算到 R=7 的画法
for y in range(cy - R, cy + R + 1):
for x in range(cx - R, cx + R + 1):
dx, dy = x - cx, y - cy
r = math.hypot(dx, dy) * k
if r > 7.4:
continue
ang = (math.atan2(dy, dx) / (2 * math.pi)) % 1.0
lit = (dx + dy) < 0
if r >= 6.5: # 外圈细边
c.put(x, y, 'l' if dx + dy < -3 else ('b' if dx + dy < 3 else 'g'))
elif r >= 5.6: # 风道
c.put(x, y, 'n')
elif r <= 2.3: # 轴心
c.put(x, y, 'h' if (dx + dy) < -1 else 'b' if dx + dy < 2 else 'm')
else:
t = (ang * 6 + (r - 3) * 0.16) % 1.0
if t < 0.55:
c.put(x, y, 'h' if t < 0.09 else ('l' if lit else 'b') if t < 0.30 else ('b' if lit else 'm'))
else:
c.put(x, y, 'n' if t < 0.85 else 'k')
c.put(cx, cy, accent)
c.put(cx - 1, cy - 1, 'w')
FAN_LAYOUT_SMALL = {1: ([(26, 13)], 9), 2: ([(17, 13), (34, 13)], 7), 3: ([(14, 13), (26, 13), (38, 13)], 5)}
def gpu_front(accent, accent_dark, burnt=False, tier=1):
"""显卡正面(48×32,3×2 格),略微俯视:石墨色散热罩带斜切角与品牌色斜条,两个七叶风扇,
右端露出鳍片,顶沿露出铜热管和 8 针供电口,前沿是带元件的 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, 'x' if y < TOP0 + 3 else 'd' if (x - L) < 4 else 'z')
for x in range(L, R + 1):
c.put(x, TOP0, 'h' if x < R - 5 else 'l'); c.put(x, TOP0 + 1, 'l' if x < R - 8 else 'b')
for y in range(TOP0, TOP1 + 1): # 左缘受光
c.put(L, y, 'l'); c.put(L + 1, y, 'b')
# 右端鳍片(竖向细条)
for x in range(R - 4, R + 1):
for y in range(TOP0 + 2, TOP1):
c.put(x, y, 'g' if (x - (R - 4)) % 2 == 0 else 'k')
c.rect(R - 5, TOP0 + 2, R - 5, TOP1 - 1, 'x')
count = (1, 2, 3, 3)[tier]
fans, fr = FAN_LAYOUT_SMALL[count]
# 风扇之间的筋条
for a, b in zip(fans, fans[1:]):
mx = (a[0] + b[0]) // 2
for y in range(TOP0 + 3, TOP1 - 2):
c.put(mx, y, 'k'); c.put(mx + 1, y, 'x')
# 品牌色斜条(从前沿向左上延伸)与前沿色条
for x in range(L + 1, R - 5):
c.put(x, TOP1 - 1, accent if x % 9 else accent_dark)
c.put(x, TOP1, accent_dark)
for cx, cy in fans:
fan(c, cx, cy, accent, R=fr)
if count < 3:
for dx, dy in ((-7, -7), (7, -7), (-7, 7), (7, 7)): # 风扇四角的固定孔
c.put(cx + dx, cy + dy, 'n' if dx + dy else 'k')
# 顶沿:铜热管端头 + 8 针供电口(带针脚)
for x0 in (L + 6, L + 8, L + 10, L + 12):
c.put(x0, TOP0 - 1, 'p'); c.put(x0 + 1, TOP0 - 1, 'q')
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针(每针占两像素)
for left, pins in (((L + 22, 6),) if tier == 0 else ((L + 18, 8),) if tier < 3 else ((L + 8, 8), (L + 25, 8))):
for x in range(left, left + pins * 2):
c.put(x, TOP0 - 1, 'n')
for x in range(left, left + pins * 2, 2):
c.put(x, TOP0 - 2, 'k')
for x in range(left + 1, left + pins * 2, 2):
c.put(x, TOP0 - 2, 'Y')
# 前沿:PCB(绿)带元件,之后金手指
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'G' if x % 7 else 'g2'); c.put(x, TOP1 + 2, 'g2')
for x0 in (L + 4, L + 11, L + 22, L + 29):
c.put(x0, TOP1 + 1, 'b'); c.put(x0 + 1, TOP1 + 1, 'g')
for x in range(L, R + 1): # 金手指:每一根带深色分隔
if x in (L + 17, L + 18):
continue
c.put(x, TOP1 + 3, 'Y' if x % 2 == 0 else 'e'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'q')
c.put(x, TOP1 + 5, 'e' if x % 2 == 0 else '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), (19, 9), (33, 16)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 5):
c.put(x, TOP1 - 1, 'e')
for cx, cy in fans:
for dx, dy in ((0, 0), (1, 1), (-1, 1), (2, -2), (-2, -2)):
c.put(cx + dx, cy + dy, 'c')
outline(c)
return c
def screw(c, x, y):
c.put(x, y, 'b'); c.put(x + 1, y, 'g'); c.put(x, y + 1, 'g'); c.put(x + 1, y + 1, 'k')
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' if (y % 3) else 'm' if x % 2 else 'd')
c.put(R, y, 'k'); c.put(R - 1, y, 'z')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
for y in range(TOP0, 28):
c.put(L, y, 'b'); c.put(L + 1, y, 'x')
# 丝印凹槽
c.rect(9, 7, 43, 13, 'z')
c.rect(9, 7, 43, 7, 'k'); c.rect(9, 7, 9, 13, 'k')
c.rect(9, 13, 43, 13, 'x')
# 品牌色斜条与前沿色条
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 y in (19, 21, 23, 25):
for x in range(L + 4, L + 14):
c.put(x, y, 'k')
for x in range(L + 4, L + 14):
c.put(x, y + 1, 'x') if y + 1 < 27 else None
# 四角螺丝
for x, y in ((L + 2, TOP0 + 3), (R - 5, TOP0 + 3), (L + 2, 24), (R - 5, 24)):
screw(c, x, y)
# 序列号贴纸:左边二维码,右边条码与小字
c.rect(22, 17, 41, 23, 'w')
c.rect(22, 17, 41, 17, 'h'); c.rect(22, 23, 41, 23, 'l')
qr = ['1110101', '1010011', '1011101', '0001010', '1101111', '0100101', '1110011']
for j, row in enumerate(qr):
for i, ch in enumerate(row):
if ch == '1':
c.put(23 + i, 17 + j - (1 if j == 6 else 0) * 0, 'n') if j < 6 else None
for x in range(31, 41, 2):
c.rect(x, 18, x, 20, 'n')
for x in range(32, 41, 3):
c.put(x, 18, 'w'); c.put(x, 19, 'w')
for x in range(31, 41):
if x % 3 != 0:
c.put(x, 22, 'm')
# 封条(红底白字)
c.rect(43, 16, 44, 24, 'R')
c.rect(43, 16, 43, 24, 'r2')
for y in (18, 20, 22):
c.put(44, y, 'w')
# 背面金手指一行
for x in range(L, R + 1):
if x in (L + 17, L + 18):
continue
c.put(x, 27, 'Y' if x % 2 == 0 else 'e')
if burnt:
for x, y in [(14, 9), (15, 10), (30, 11), (36, 13), (20, 14), (26, 21), (12, 22)]:
c.put(x, y, 'c')
outline(c)
return c
def overlay():
return Canvas(W, H)
def ov_fan(kind, count=2):
"""风扇叠加:1 积灰(叶片上一层灰),2 叶片缺角(边缘缺一块并露出黑底)。"""
c = overlay()
for cx, cy in FAN_LAYOUT_SMALL[count][0]:
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(22, 17, 41, 23, 'w')
c.rect(22, 17, 41, 17, 'h'); c.rect(22, 23, 41, 23, 'l')
for x, y in ((24, 20), (25, 19), (26, 20), (27, 19), (29, 20), (30, 19), (30, 21), (32, 19), (33, 20), (34, 19), (35, 21), (36, 20), (37, 19), (38, 20), (39, 21), (24, 22), (26, 21), (28, 22), (31, 22), (35, 22)):
c.put(x, y, 'O')
else:
c.rect(43, 16, 44, 24, 'd')
for y in (16, 18, 20, 22, 24):
c.put(44, y, 'm')
c.put(43, 17, 'm'); c.put(43, 21, 'm'); c.put(43, 23, 'x')
return c
MODELS = ['RZX 60', 'RZX 70', 'RZX 80', 'RZX 90', 'RZ 70', 'RZ 80', 'RZ 800', 'RZ 900']
def rig():
"""矿架(48×48,3×3 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
@@ -283,13 +515,123 @@ def terminal():
return c
def key(model):
return model.replace(' ', '').lower()
def ui_bank():
"""银行图标(32×32):带山花的石柱建筑,门前一枚金币。"""
c = Canvas(32, 32)
for k in range(0, 14): # 山花(三角)
c.rect(16 - k, 4 + k // 2, 16 + k, 4 + k // 2, 'h' if k < 3 else 'l')
c.rect(2, 11, 29, 12, 'b'); c.rect(2, 13, 29, 13, 'd') # 檐口
for x0 in (4, 10, 16, 22): # 四根柱子
c.rect(x0, 14, x0 + 3, 24, 'l')
c.rect(x0, 14, x0, 24, 'h'); c.rect(x0 + 3, 14, x0 + 3, 24, 'd')
c.rect(x0 - 1, 14, x0 + 4, 14, 'b'); c.rect(x0 - 1, 24, x0 + 4, 24, 'b')
c.rect(1, 25, 30, 26, 'b'); c.rect(0, 27, 31, 28, 'm'); c.rect(0, 29, 31, 29, 'd')
c.rect(14, 8, 17, 9, 'y'); c.put(15, 8, 'w') # 山花里的小徽标
for yy in range(19, 27): # 金币
for xx in range(23, 31):
d = (xx - 26.5) ** 2 + (yy - 22.5) ** 2
if d <= 14:
c.put(xx, yy, 'e' if d > 9 else 'y')
c.put(25, 21, 'w'); c.put(26, 21, 'w'); c.put(26, 22, 'y')
outline(c)
return c
def ui_poinbase():
"""Poinbase 图标(32×32):蓝色盾牌,中间一枚金色 P 币,两侧一上一下的箭头。"""
c = Canvas(32, 32)
for y in range(3, 29): # 盾牌
half = 12 if y < 16 else max(0, 12 - (y - 16) * 12 // 12)
for x in range(16 - half, 16 + half + 1):
c.put(x, y, 'u' if x < 16 else 'U')
c.put(16 - half, y, 'h') if half else None
for x in range(4, 29):
c.put(x, 3, 'h'); c.put(x, 4, 'l')
for y in range(7, 21): # 币
for x in range(9, 24):
d = (x - 16) ** 2 + (y - 13.5) ** 2
if d <= 36:
c.put(x, y, 'e' if d > 28 else 'y')
c.put(12, 9, 'w'); c.put(13, 8, 'w'); c.put(12, 10, 'w')
c.rect(13, 9, 14, 18, 'U'); c.rect(15, 9, 18, 10, 'U'); c.rect(18, 11, 19, 13, 'U'); c.rect(15, 14, 18, 15, 'U') # P
for k in range(4): # 下方两个箭头:买入、卖出
c.put(9 + k, 24 - k, 'w') if k < 3 else None
for x in range(8, 13): c.put(x, 22, 'w')
for x in range(19, 24): c.put(x, 22, 'w')
c.put(19, 23, 'w'); c.put(20, 24, 'w'); c.put(23, 23, 'w'); c.put(22, 24, 'w')
outline(c)
return c
def ui_web3():
"""Web3 图标(32×32):六边形节点网络,紫色底。"""
c = Canvas(32, 32)
nodes = [(16, 7), (7, 14), (25, 14), (11, 24), (21, 24), (16, 16)]
edges = [(0, 1), (0, 2), (1, 3), (2, 4), (3, 4), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4)]
for (a, b) in edges:
(x0, y0), (x1, y1) = nodes[a], nodes[b]
n = max(abs(x1 - x0), abs(y1 - y0))
for i in range(n + 1):
px, py = x0 + (x1 - x0) * i // n, y0 + (y1 - y0) * i // n
c.put(px, py, 'v'); c.put(px + 1, py, 'v'); c.put(px, py + 1, 'v')
for i, (x, y) in enumerate(nodes):
r = 3 if i == 5 else 2
for dy in range(-r, r + 1):
for dx in range(-r, r + 1):
if dx * dx + dy * dy <= r * r + 1:
c.put(x + dx, y + dy, 'V' if (dx + dy) > 0 else 'h' if i != 5 else 'y')
c.put(x, y, 'w')
outline(c)
return c
def guide():
"""鉴定手册(16×16,一格):竖放的小册子,封面带品牌色条,书脊在左。"""
c = Canvas(16, 16)
c.box(2, 1, 13, 14, 'b')
c.rect(3, 2, 4, 13, 'd') # 书脊
c.rect(5, 2, 12, 2, 'h')
c.rect(6, 4, 11, 5, 'G') # 封面色块
c.rect(6, 4, 11, 4, 'g2')
for y in (7, 9, 11): # 文字行
c.rect(6, y, 11 if y != 11 else 9, y, 'm')
c.put(12, 13, 'd')
outline(c)
return c
SPRITES = {
'gpu_probidia': lambda: gpu('G', 'g2'),
'gpu_pmd': lambda: gpu('R', 'r2'),
'gpu_burnt': lambda: gpu('e', 'c', burnt=True),
'gpu_guide': guide,
'gpu_burnt_f': lambda: gpu_front('e', 'c', burnt=True),
'gpu_burnt_b': lambda: gpu_back('e', 'c', burnt=True),
'gpu_probidia_b': lambda: gpu_back('G', 'g2'),
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
'rig': rig,
'terminal': terminal,
}
for _tier in range(4):
SPRITES['gpu_probidia_f_t%d' % _tier] = (lambda t=_tier: gpu_front('G', 'g2', tier=t))
SPRITES['gpu_pmd_f_t%d' % _tier] = (lambda t=_tier: gpu_front('R', 'r2', tier=t))
UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3}
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
OVERLAYS = {
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
'gpu_ov_brk1': lambda: ov_bracket(1),
'gpu_ov_stk2': lambda: ov_sticker(2),
}
for _n in (1, 2, 3):
OVERLAYS['gpu_ov_fan1_%d' % _n] = (lambda n=_n: ov_fan(1, n))
OVERLAYS['gpu_ov_fan2_%d' % _n] = (lambda n=_n: ov_fan(2, n))
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 物品图每格的中心像素是否不透明来判断占格(行从图的下沿往上数)。有空格就报错。"""
@@ -306,11 +648,48 @@ 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
for name, make in UI_ICONS.items():
make().png(os.path.join(sys.argv[2], name + '.png'), scale=8, bg=(0x2a, 0x2c, 0x34))
stack('gpu_probidia_f_t1', 'gpu_ov_fan2_2', 'gpu_ov_fin1', 'gpu_ov_brk1').png(os.path.join(sys.argv[2], 'prev_front_worn.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 f in os.listdir(out): # 清掉旧的显卡图,避免留下不再使用的文件
if f.startswith('gpu_'):
os.remove(os.path.join(out, f))
for name, make in SPRITES.items():
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'))
for name, make in UI_ICONS.items():
make().png(os.path.join(out, name + '.png'))
import big, big_cases
for name, make in big.BIG.items():
make().png(os.path.join(out, name + '.png'))
for name, make in big_cases.CASES.items():
if name.startswith('gpu_big_ov_fan_'):
continue
if not name.startswith('gpu_big_ov_print_') and name in ('gpu_big_ov_fin_dim', 'gpu_big_ov_fin_oxide', 'gpu_big_ov_brk_scratch', 'gpu_big_ov_seal_missing'):
make().png(os.path.join(out, name + '.png'))
# 风扇叠加按风扇数各出一套:叠加模块里的风扇位置是模块级变量,换成对应布局后再画
for count, (centres, radius) in big.FAN_LAYOUT.items():
big_cases.FANS = list(centres) if len(centres) > 1 else [centres[0], centres[0]]
big_cases.FR = radius
for fname, short in (('fan_dust1', 'fan_dust1'), ('fan_dust2', 'fan_dust2'), ('fan_chip', 'fan_chip')):
getattr(big_cases, fname)().png(os.path.join(out, 'gpu_big_ov_%s_%d.png' % (short, count)))
for m in MODELS:
for style in ('normal', 'skew', 'stamp'):
big_cases.print_overlay(m, style).png(os.path.join(out, 'gpu_big_ov_print_%s_%s.png' % (style, key(m))))
print(out)
+180
View File
@@ -0,0 +1,180 @@
using System;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>卖家的说法:每个标签一项。型号是文字,其余取值与 <see cref="CardLook"/> 一致;「供电与风扇」由说法里的型号决定档次。</summary>
public struct Claim
{
public string Model;
public int Fan, Fingers, Bracket, Print, Sticker, Serial;
public int Tier => Hardware.Specs[Model].Tier;
public int Get(int row) => row == CardLabels.RowBuild ? Tier : row == CardLabels.RowFan ? Fan : row == CardLabels.RowFingers ? Fingers
: row == CardLabels.RowBracket ? Bracket : row == CardLabels.RowPrint ? Print : row == CardLabels.RowSerial ? Serial : Sticker;
public Claim With(int row, int value)
{
var c = this;
if (row == CardLabels.RowFan) c.Fan = value; else if (row == CardLabels.RowFingers) c.Fingers = value;
else if (row == CardLabels.RowBracket) c.Bracket = value; else if (row == CardLabels.RowPrint) c.Print = value;
else if (row == CardLabels.RowSerial) c.Serial = value; else if (row == CardLabels.RowSeal) c.Sticker = value;
return c;
}
public string Encode() => $"{Model}:{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}";
public static Claim Decode(string s)
{
var p = (s ?? "").Split(':');
if (p.Length != 2 || p[1].Length != 6 || !Hardware.Specs.ContainsKey(p[0])) return default;
int D(int i) => Math.Max(0, Math.Min(2, p[1][i] - '0'));
return new Claim { Model = p[0], Fan = D(0), Fingers = D(1), Bracket = D(2), Print = D(3), Sticker = D(4), Serial = D(5) };
}
/// <summary>按真实情况写出的说法(没有隐瞒)。</summary>
public static Claim Truth(Unit u)
{
var l = u.Appearance;
return new Claim { Model = u.FakeOf ?? u.Model, Fan = l.Fan, Fingers = l.Fingers, Bracket = l.Bracket, Print = l.Print, Sticker = l.Sticker, Serial = l.Serial };
}
}
/// <summary>
/// 标签栏的数据:五个标签(型号真伪、风扇叶片、金手指、挡板、封条),每个标签有卖家的说法和真实情况;
/// 供电与风扇、背面丝印、序列号三项是型号真伪的依据,只存状态,不单独成行。
/// 一个标签被指出(拆穿)之后,说法改回真实情况;价格按「已指出的标签取真实情况,其余取说法」算,
/// 所以卖家隐瞒的问题要玩家一项项指出来才能把价格拉回真实价值。指出型号有问题,型号自动改回真实型号。
/// </summary>
public static class CardLabels
{
public const int RowSpec = 1, RowBuild = 2, RowFan = 4, RowFingers = 8, RowBracket = 16, RowPrint = 32, RowSerial = 64, RowSeal = 128, AllRows = 255;
/// <summary>标签栏的顺序:第一行鉴定真伪(型号;依据是供电口与风扇数、序列号、背面丝印,玩家对着大图自己看),后四行鉴定成色。</summary>
public static readonly int[] Rows = { RowSpec, RowFan, RowFingers, RowBracket, RowSeal };
public const int AuthRows = 1;
/// <summary>各标签各取值对价格的影响(百分比,相加):风扇叶片积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失 −6%。
/// 型号、供电与风扇、丝印、序列号本身不扣价,它们决定的是这块卡是什么芯片。</summary>
public static double Pct(int row, int value)
{
switch (row)
{
case RowFan: return value == 1 ? -4 : value == 2 ? -12 : 0;
case RowFingers: return value == 1 ? -4 : value == 2 ? -12 : 0;
case RowBracket: return value == 1 ? -2 : 0;
case RowSeal: return value == 2 ? -6 : 0;
}
return 0;
}
/// <summary>这一行是不是假货的证据:说法与真实情况不同,并且真实情况本身不该出现在真卡上。</summary>
public static bool IsEvidence(int row, Claim claim, Claim real)
{
switch (row)
{
case RowBuild: return claim.Tier != real.Tier;
case RowPrint: return real.Print != 0 && claim.Print != real.Print;
case RowSerial: return real.Serial != 0 && claim.Serial != real.Serial;
}
return false;
}
/// <summary>
/// 价值:claim 为空表示没有人说过什么,一律按真实情况;exposed 里的位表示已指出的标签(取真实情况)。
/// 型号取「说法里的型号」,型号标签被拆穿之后取真实芯片。价值 = 型号定价 ×(0.25 + 0.75 × 健康度)×(1 + 各标签折扣之和)。
/// </summary>
public static long Value(Unit u, Claim? claim, int exposed)
{
var real = Claim.Truth(u);
Claim believed = claim == null ? real : claim.Value;
double sum = 0;
foreach (int r in new[] { RowFan, RowFingers, RowBracket, RowSeal })
{
int v = claim == null || (exposed & r) != 0 ? real.Get(r) : believed.Get(r);
sum += Pct(r, v);
}
string model = claim == null || (exposed & RowSpec) != 0 ? real.Model : believed.Model;
double value = CardValue.Of(model, u.Health) * Math.Max(0, 1 + sum / 100.0);
return Math.Max(1, (long)Math.Round(value));
}
/// <summary>指出一个标签:返回 0 没问题(说法属实)、1 隐瞒了对价格不利的情况、2 拆穿了假货(型号一并改回真实型号)。</summary>
public static int Point(Unit u, Claim claim, int row, ref int exposed)
{
var real = Claim.Truth(u);
bool wasExposed = (exposed & row) != 0;
exposed |= row;
if (row == RowSpec) return claim.Model != real.Model ? 2 : 0;
if (IsEvidence(row, claim, real))
{
exposed |= RowSpec | RowBuild;
return 2;
}
if (wasExposed) return 0;
return claim.Get(row) != real.Get(row) ? 1 : 0;
}
/// <summary>说法里隐瞒了什么:说法比真实情况价值高的标签,型号不符,以及假货的证据。</summary>
public static int Lies(Unit u, Claim claim, int exposed)
{
var real = Claim.Truth(u);
int lies = 0;
foreach (int r in Rows)
{
if ((exposed & r) != 0) continue;
if (r == RowSpec) { if (claim.Model != real.Model) lies |= r; }
else if (claim.Get(r) != real.Get(r) && Pct(r, claim.Get(r)) > Pct(r, real.Get(r))) lies |= r;
}
return lies;
}
/// <summary>卖家怎么说:老实人如实说;中间商每项有七成概率说成没问题;骗子全部说成没问题。假卡的型号一律说成标称型号。</summary>
public static Claim DealerClaim(Unit u, DealerKind kind, Rng rng)
{
var c = Claim.Truth(u);
if (kind == DealerKind.Honest) return c;
c.Model = u.Model;
double hide = kind == DealerKind.Scammer ? 1.0 : 0.7;
foreach (int r in new[] { RowFan, RowFingers, RowBracket, RowPrint, RowSerial, RowSeal })
if (rng.Chance(hide)) c = c.With(r, 0);
return c;
}
// ---------- 序列号 ----------
/// <summary>各牌子的序列号前缀与合法工厂码。序列号格式:前缀 + 档次码(1 到 4)+ "-" + 工厂码(两位)+ "-" + 四位流水。</summary>
public static string BrandCode(string brand) => brand == Hardware.Probidia ? "PB" : "MD";
public static readonly int[] ProbidiaFactories = { 11, 12, 21, 34, 47 }, PmdFactories = { 13, 22, 35, 41, 58 };
public static int[] Factories(string brand) => brand == Hardware.Probidia ? ProbidiaFactories : PmdFactories;
private static uint Mix(uint seed, int salt)
{
uint x = seed * 2654435761u + (uint)salt * 40503u + 777u;
x ^= x >> 15; x *= 2246822519u; x ^= x >> 13; x *= 3266489917u; x ^= x >> 16;
return x;
}
/// <summary>
/// 卡上贴纸的序列号。合规:前缀(牌子)+ 标称型号的档次码 + 合法工厂码 + 四位流水。
/// 不合规(贴纸是后补的):工厂码不在合法范围,或流水只有三位。档次码不符:贴的是原芯片的序列号,档次码是真实芯片的档次。
/// </summary>
public static string SerialText(Unit u)
{
var label = Hardware.Specs[u.Model];
var chip = Hardware.Specs[u.FakeOf ?? u.Model];
uint seed = u.Look.Seed == 0 ? 12345u : u.Look.Seed;
int kind = u.Appearance.Serial;
int[] ok = Factories(label.Brand);
int tier = (kind == 2 ? chip.Tier : label.Tier) + 1;
int factory = ok[(int)(Mix(seed, 1) % (uint)ok.Length)];
int number = 1000 + (int)(Mix(seed, 2) % 9000);
if (kind == 1)
{
if (Mix(seed, 3) % 2 == 0) { int[] bad = { 0, 9, 66, 77, 88, 99 }; factory = bad[(int)(Mix(seed, 4) % (uint)bad.Length)]; }
else number = 100 + (int)(Mix(seed, 5) % 900);
}
return $"{BrandCode(label.Brand)}{tier}-{factory:00}-{number}";
}
}
}
+64 -32
View File
@@ -13,17 +13,70 @@ namespace Decentralized.Domain
public int Fan;
/// <summary>金手指:0 光亮,1 发暗,2 氧化发绿(老化)。</summary>
public int Fingers;
/// <summary>挡板:0 完好,1 划痕(老化),2 接口数量不对(假卡)。</summary>
/// <summary>挡板:0 完好,1 划痕(老化)。</summary>
public int Bracket;
/// <summary>背面丝印:0 与标称型号一致,1 印的是真实芯片的型号(假卡),2 重新印过、字歪(假卡)。</summary>
public int Print;
/// <summary>背面贴纸与封条:0 印刷序列号、封条完好,1 手写序列号(假卡),2 封条缺失(旧卡或翻新卡)。</summary>
/// <summary>封条:0 完好,2 缺失(开过封的旧卡或翻新卡)。</summary>
public int Sticker;
/// <summary>序列号:0 合规(格式对、工厂码在合法范围、档次码与型号一致),1 不合规(工厂码不在合法范围或位数不对,贴纸是后补的),
/// 2 档次码与标称型号不符(贴的是原芯片的序列号)。后两种只出现在假卡上。</summary>
public int Serial;
public const int Variants = 3;
/// <summary>每张卡自己的随机种子:老化类维度(风扇、金手指、挡板划痕)由它和当前健康度决定,每晚损耗后重新算,
/// 所以越旧的卡看着越旧。为 0 时表示外观是写死的(测试用)。</summary>
public uint Seed;
private static double U(uint seed, int salt)
{
uint x = seed * 2654435761u + (uint)salt * 40503u + 12345u;
x ^= x >> 15; x *= 2246822519u; x ^= x >> 13; x *= 3266489917u; x ^= x >> 16;
return (x >> 8) * (1.0 / (1 << 24));
}
/// <summary>按当前健康度重新算出的外观:磨损度 = 1 − 健康度 / 100,再加上这张卡自己的偏差(±0.15)。
/// 风扇磨损 ≥ 0.25 积灰、≥ 0.55 缺角;金手指 ≥ 0.30 发暗、≥ 0.60 氧化;挡板磨损 ≥ 0.45 出划痕(接口数不对是假卡的固定特征,不变)。</summary>
public CardLook Appearance(double health)
{
if (Seed == 0) return this;
double worn = 1 - Math.Max(0, Math.Min(100, health)) / 100.0;
var l = this;
double fan = worn + (U(Seed, 1) - 0.5) * 0.3, fin = worn + (U(Seed, 2) - 0.5) * 0.3, brk = worn + (U(Seed, 3) - 0.5) * 0.3;
l.Fan = fan >= 0.55 ? 2 : fan >= 0.25 ? 1 : 0;
l.Fingers = fin >= 0.60 ? 2 : fin >= 0.30 ? 1 : 0;
l.Bracket = brk >= 0.45 ? 1 : 0;
return l;
}
private static readonly double[] FanHash = { 1.00, 0.97, 0.92 }, FingerHash = { 1.00, 0.98, 0.94 };
/// <summary>算力倍数:风扇积灰 ×0.97、缺角 ×0.92(降频);金手指发暗 ×0.98、氧化 ×0.94(接触不良掉速)。</summary>
public double HashFactor => FanHash[Fan] * FingerHash[Fingers];
/// <summary>六位数字,存在物品标签上:风扇、金手指、挡板、丝印、封条、序列号。</summary>
public string Encode() => $"{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}";
public static CardLook Decode(string s)
{
if (s == null || (s.Length != 5 && s.Length != 6)) return default;
int D(int i) => Math.Max(0, Math.Min(Variants - 1, s[i] - '0'));
return new CardLook { Fan = D(0), Fingers = D(1), Bracket = D(2), Print = D(3), Sticker = D(4), Serial = s.Length > 5 ? D(5) : 0 };
}
// 外观维度对应卡的真实状态:损耗、暴毙概率随之变化,价值走标签(见 CardLabels)
private static readonly double[] FanWear = { 1.00, 1.10, 1.30 }, FanDeath = { 1.00, 1.00, 1.20 };
private static readonly double[] FingerDeath = { 1.00, 1.10, 1.30 };
/// <summary>每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。</summary>
public double WearFactor => FanWear[Fan];
/// <summary>暴毙概率倍数:风扇缺角 ×1.2;金手指发暗 ×1.1、氧化 ×1.3(接触不良)。</summary>
public double DeathFactor => FanDeath[Fan] * FingerDeath[Fingers];
/// <summary>只在假卡上出现的取值:玩家一旦看到,就能肯定是假的。</summary>
public bool HasFakeSign => Bracket == 2 || Print != 0 || Sticker == 1;
public bool HasFakeSign => Print != 0 || Serial != 0;
/// <summary>
/// 生成外观。真卡:老化维度按健康度出现,封条缺失在二手卡上很常见,不会出现破绽类取值。
@@ -31,36 +84,15 @@ namespace Decentralized.Domain
/// </summary>
public static CardLook Generate(Rng rng, double health, bool fake)
{
double worn = 1 - Math.Max(0, Math.Min(100, health)) / 100.0;
var look = new CardLook
var look = new CardLook { Seed = rng.NextUInt() | 1, Sticker = rng.Chance(fake ? 0.35 : 0.15 + 0.30 * (1 - Math.Max(0, Math.Min(100, health)) / 100.0)) ? 2 : 0 };
if (fake)
{
Fan = Pick(rng, 0.10 + 0.55 * worn, 0.35 * worn),
Fingers = Pick(rng, 0.08 + 0.55 * worn, 0.40 * worn),
Bracket = rng.Chance(0.10 + 0.50 * worn) ? 1 : 0,
Sticker = rng.Chance(fake ? 0.35 : 0.15 + 0.30 * worn) ? 2 : 0,
};
if (!fake) return look;
int signs = rng.Chance(0.55) ? 1 : rng.Chance(0.6) ? 2 : 3;
var order = new[] { 0, 1, 2 };
for (int i = order.Length - 1; i > 0; i--)
{
int j = rng.Range(0, i);
(order[i], order[j]) = (order[j], order[i]);
// 假卡本身就带着实物上的破绽(低档次的板子:供电口和风扇数对不上),此外骗子还可能重印丝印、补贴序列号
if (rng.Chance(0.55)) look.Print = rng.Chance(0.6) ? 1 : 2;
if (rng.Chance(0.65)) look.Serial = rng.Chance(0.55) ? 2 : 1;
}
for (int k = 0; k < signs; k++)
{
switch (order[k])
{
case 0: look.Bracket = 2; break;
case 1: look.Print = rng.Chance(0.6) ? 1 : 2; break;
default: look.Sticker = 1; break;
}
}
return look;
return look.Appearance(health);
}
/// <summary>p1 的概率取 1,再以 p2 的概率升到 2。</summary>
private static int Pick(Rng rng, double p1, double p2) => rng.Chance(p1) ? (rng.Chance(p2 / Math.Max(p1, 1e-9)) ? 2 : 1) : 0;
}
/// <summary>
@@ -77,8 +109,8 @@ namespace Decentralized.Domain
return Math.Max(1, (long)Math.Round(spec.Price * f));
}
/// <summary>真实价值(芯片型号 + 健康度)。</summary>
public static long True(Unit u) => Of(u.FakeOf ?? u.Model, u.Health);
/// <summary>真实价值:全部标签都按真实情况算。</summary>
public static long True(Unit u) => CardLabels.Value(u, null, 0);
/// <summary>标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。</summary>
public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth);
+3 -3
View File
@@ -9,9 +9,9 @@ namespace Decentralized.Domain
{
public const string Rig = "RIG";
public static readonly string[] Basic = { "RZX50", "RZX60", "RZ470", "RZ580" };
public static readonly string[] Advanced = { "RZX70", "RZX80Ti", "RZ6800" };
public static readonly string[] Flagship = { "RZX90", "RZ7900" };
public static readonly string[] Basic = { "RZX60", "RZ70" };
public static readonly string[] Advanced = { "RZX70", "RZ80", "RZX80", "RZ800" };
public static readonly string[] Flagship = { "RZX90", "RZ900" };
public const int AdvancedFromDay = 10, FlagshipFromDay = 30, SpecialFlagshipFromDay = 20;
public const double RigRestockChance = 0.30, AdvancedChance = 0.60, AdvancedSecondChance = 0.30, FlagshipChance = 0.25;
+14 -1
View File
@@ -16,6 +16,8 @@ namespace Decentralized.Domain
public sealed class Market
{
public double Sentiment = 1.0;
/// <summary>供给因子:出块奖励减半抬高挖矿成本,价格锚随之上移;减半前一周有预期行情。每晚向目标靠拢。</summary>
public double Supply = 1.0;
public List<Pool> Pools = new List<Pool>();
/// <summary>开门时的参考价,夜间用它和收盘价算当日均价(合约的标记价)。</summary>
public double OpenPrice = Tuning.PscStartPrice;
@@ -42,9 +44,20 @@ namespace Decentralized.Domain
/// <summary>PSC 的参考价(信用点)。</summary>
public double Price => Main.Price;
/// <summary>供给因子的目标:奖励比起始奖励每少一半,锚上移 2^传导指数;减半前 7 天内再逐日加预期。</summary>
public static double SupplyTarget(int day, double reward)
{
double t = Math.Pow(Tuning.BlockReward / Math.Max(1e-9, reward), Tuning.HalvingPassThrough);
int toHalving = Tuning.HalvingDays - day % Tuning.HalvingDays; // 减半当晚 day % HalvingDays == 0,那天之后又是一个完整周期
if (toHalving <= Tuning.HalvingRampDays) t *= 1 + Tuning.HalvingRamp * (Tuning.HalvingRampDays - toHalving + 1) / Tuning.HalvingRampDays;
return t;
}
public void UpdateSupply(int day, double reward) => Supply += (SupplyTarget(day, reward) - Supply) * Tuning.SupplyAdjust;
public double Anchor(ICollection<MarketEvent> active)
{
double a = Tuning.PscStartPrice * Sentiment;
double a = Tuning.PscStartPrice * Sentiment * Supply;
if (active.Contains(MarketEvent.Blackout)) a *= 1.10;
if (active.Contains(MarketEvent.Crackdown)) a *= 0.92;
return a;
+20 -9
View File
@@ -13,6 +13,8 @@ namespace Decentralized.Domain
public double Kj;
public int Price;
/// <summary>暴毙概率的品牌系数。</summary>
/// <summary>档次:0 低端、1 中端、2 高端、3 旗舰(显卡)。实物特征(供电口、风扇数)只看档次。</summary>
public int Tier;
public double DeathFactor;
/// <summary>每天损耗的系数。</summary>
public double WearFactor;
@@ -25,20 +27,25 @@ namespace Decentralized.Domain
public static class Hardware
{
/// <summary>各档次的实物特征:供电口针数(旗舰是两个 8 针)与风扇数量。</summary>
public static readonly string[] PowerPins = { "6", "8", "8", "8+8" };
public static readonly int[] FanCount = { 1, 2, 3, 3 };
public const string Probidia = "Probidia", Pmd = "PMD", RatDigg = "RatDigg";
public static readonly IReadOnlyDictionary<string, HardwareSpec> Specs = new[]
{
Gpu("RZX50", Probidia, 1.0, 4, 220), Gpu("RZX60", Probidia, 1.5, 5, 330), Gpu("RZX70", Probidia, 5.0, 12, 800),
Gpu("RZX80Ti", Probidia, 8.0, 18, 1300), Gpu("RZX90", Probidia, 20.0, 30, 5000),
Gpu("RZ470", Pmd, 0.8, 2.5, 170), Gpu("RZ580", Pmd, 1.2, 3, 250), Gpu("RZ6800", Pmd, 4.0, 8, 680),
Gpu("RZ7900", Pmd, 15.0, 20, 4000),
// 四个档次(低端、中端、高端、旗舰),两个牌子通用:型号只是显示名,性能和价格由「牌子 + 档次」决定
Gpu("RZX60", Probidia, 0, 1.0, 4, 220), Gpu("RZX70", Probidia, 1, 5.0, 12, 800),
Gpu("RZX80", Probidia, 2, 8.0, 18, 1300), Gpu("RZX90", Probidia, 3, 20.0, 30, 5000),
Gpu("RZ70", Pmd, 0, 0.8, 2.5, 170), Gpu("RZ80", Pmd, 1, 4.0, 8, 680),
Gpu("RZ800", Pmd, 2, 8.0, 12, 1500), Gpu("RZ900", Pmd, 3, 15.0, 20, 4000),
Asic("M1", 50, 60, 11000), Asic("M2", 120, 110, 27000), Asic("M3", 300, 220, 72000),
}.ToDictionary(s => s.Model);
private static HardwareSpec Gpu(string model, string brand, double h, double kj, int price) => new HardwareSpec
private static HardwareSpec Gpu(string model, string brand, int tier, double h, double kj, int price) => new HardwareSpec
{
Model = model, Brand = brand, Hash = h, Kj = kj, Price = price,
Model = model, Brand = brand, Tier = tier, Hash = h, Kj = kj, Price = price,
DeathFactor = brand == Probidia ? 0.7 : 1.0, WearFactor = brand == Probidia ? 1.0 : 1.2,
};
@@ -84,6 +91,10 @@ namespace Decentralized.Domain
public double Health = 100;
public bool Dead;
public bool Refurbished;
/// <summary>外观;新卡(正规渠道)没有记录,按全新画。</summary>
public CardLook Look;
/// <summary>按当前健康度算出的外观。</summary>
public CardLook Appearance => Look.Appearance(Health);
public HardwareSpec Label => Hardware.Specs[Model];
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model];
@@ -92,7 +103,7 @@ namespace Decentralized.Domain
public double Hash(ModuleBonus b)
{
if (Dead) return 0;
return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash;
return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash * Appearance.HashFactor;
}
public double Kj(ModuleBonus b) => Dead ? 0 : Chip.Kj * b.Kj;
@@ -101,14 +112,14 @@ namespace Decentralized.Domain
public double DeathChance(ModuleBonus b)
{
double p = Tuning.DeathBase + Tuning.DeathWorn * Math.Pow(1 - Health / 100, 2);
return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death;
return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death * Appearance.DeathFactor;
}
/// <summary>一夜的损耗与暴毙判定;返回是否在今晚暴毙。</summary>
public bool Night(ModuleBonus b, Rng rng)
{
if (Dead) return false;
Health = Math.Max(0, Health - Label.WearFactor * b.Wear);
Health = Math.Max(0, Health - Label.WearFactor * b.Wear * Appearance.WearFactor);
if (rng.Chance(DeathChance(b)))
{
Dead = true;
+2 -2
View File
@@ -9,7 +9,7 @@ namespace Decentralized.Domain
public static class RigSlots
{
public const int Base = 2, Max = 5;
public const int CellWidth = 2, CellsPerSlot = 2;
public const int CellWidth = 3, CellsPerSlot = 2;
/// <summary>升级到第 n 个槽需要的电子元件(物品 id,件数)。n 从 3 开始。</summary>
public static (string Item, int Count) Cost(int slot)
@@ -45,7 +45,7 @@ namespace Decentralized.Domain
return take;
}
/// <summary>显卡格子的形状字符串:宽 2,高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。</summary>
/// <summary>显卡格子的形状字符串:宽 3(一张卡 3×2 格),高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。</summary>
public static string Shape(int slots)
{
var sb = new System.Text.StringBuilder();
+7
View File
@@ -9,6 +9,13 @@ namespace Decentralized.Domain
public const double PscStartPrice = 100;
public const double BlockReward = 90;
public const int HalvingDays = 90;
/// <summary>减半对币价的传导:奖励减半时,价格锚上移 2^传导指数 倍(0.6 即约 +52%),市场已部分预期到,所以不是翻倍。</summary>
public const double HalvingPassThrough = 0.6;
/// <summary>减半前一周的预期行情:锚逐日抬高,到减半当天为 +8%。</summary>
public const int HalvingRampDays = 7;
public const double HalvingRamp = 0.08;
/// <summary>价格锚向目标的逐晚靠拢比例(约 7 天完成大半)。</summary>
public const double SupplyAdjust = 0.15;
public const double NpcHash0 = 360;
public const double NpcKjPerH = 2.4;
public const double ElecPrice = 1.5;
+59
View File
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
public enum DealerKind { Honest, Middleman, Scammer }
/// <summary>贩子带来的一张卡:型号、健康度、是不是假卡。怎么说由 <see cref="CardLabels.DealerClaim"/> 决定,中间商另加 <see cref="MarkupPct"/>% 的价。</summary>
public sealed class UsedCard
{
public string Model;
public string FakeOf;
public double Health;
public int MarkupPct;
}
public static class UsedDealers
{
public const double MiddlemanMarkup = 0.08;
public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45;
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
public static DealerKind Kind(int day, Rng rng)
{
double scam = Math.Min(0.30, 0.10 + day * 0.004);
double r = rng.NextDouble();
return r < scam ? DealerKind.Scammer : r < scam + 0.30 ? DealerKind.Middleman : DealerKind.Honest;
}
/// <summary>能出现在二手市场上的型号:按天数放开,旗舰第 30 天起。</summary>
public static string[] Pool(int day)
{
var list = new List<string>(Dealer.Basic);
if (day >= Dealer.AdvancedFromDay) list.AddRange(Dealer.Advanced);
if (day >= Dealer.FlagshipFromDay) list.AddRange(Dealer.Flagship);
return list.ToArray();
}
public static List<UsedCard> Cards(DealerKind kind, int day, Rng rng)
{
var pool = Pool(day);
int count = rng.Range(3, 5);
var cards = new List<UsedCard>();
for (int i = 0; i < count; i++)
{
double share = kind == DealerKind.Scammer ? FakeShareScammer : kind == DealerKind.Middleman ? FakeShareMiddleman : 0;
var label = pool[rng.Range(0, pool.Length - 1)];
var card = new UsedCard { Model = label, Health = 25 + rng.NextDouble() * 60 };
var cheaper = Hardware.Specs.Values.Where(s => !s.Asic && s.Price * 2 <= Hardware.Specs[label].Price).Select(s => s.Model).ToArray();
if (cheaper.Length > 0 && rng.Chance(share)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0;
cards.Add(card);
}
return cards;
}
}
}
+19
View File
@@ -19,11 +19,30 @@ namespace Decentralized.Domain
/// <summary>被执法标记。</summary>
public bool Flagged;
/// <summary>买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。</summary>
public Dictionary<string, double> BuyQty = new Dictionary<string, double>(), BuyCost = new Dictionary<string, double>();
public void RecordBuy(string asset, double qty, double costCredits)
{
if (qty <= 0) return;
BuyQty[asset] = (BuyQty.TryGetValue(asset, out var q) ? q : 0) + qty;
BuyCost[asset] = (BuyCost.TryGetValue(asset, out var c) ? c : 0) + Math.Max(0, costCredits);
}
/// <summary>买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。</summary>
public double? AvgBuy(string asset) => BuyQty.TryGetValue(asset, out var q) && q > 1e-9 ? BuyCost[asset] / q : (double?)null;
public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0;
public void Add(string asset, double amount)
{
if (amount == 0) return;
if (amount < 0 && BuyQty.TryGetValue(asset, out var bq))
{
double before = Get(asset), keep = before > 1e-12 ? Math.Max(0, 1 + amount / before) : 0;
BuyQty[asset] = bq * keep; BuyCost[asset] *= keep;
if (BuyQty[asset] < 1e-9) { BuyQty.Remove(asset); BuyCost.Remove(asset); }
}
double v = Get(asset) + amount;
if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足");
if (Math.Abs(v) < 1e-12) Balances.Remove(asset);
+53
View File
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Decentralized.Domain;
namespace Decentralized
@@ -15,6 +16,58 @@ namespace Decentralized
ModHook.OnPlaceInventorInventoryItemLate += OnPlace;
// 开发者模式(F9)里的测试入口:像到了杰克逊店里一样,清空并扩大柜台后上货
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give rig x3 (test)", () => { for (int i = 0; i < 3; i++) Items.Give(ModHelper.SpawnItem(Items.Rig)); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used cards x4 (test)", () =>
{
var rng = Session.Data.Rng;
Items.Give(Items.NewCard("RZX80", 40, null, rng));
Items.Give(Items.NewCard("RZX80", 70, "RZX60", rng));
Items.Give(Items.NewCard("RZ800", 30, "RZ70", rng));
Items.Give(Items.NewCard("RZ900", 90, "RZ70", rng));
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dump inspection UI (test)", () => Log.Guard("检视界面结构", () =>
{
var sb = new System.Text.StringBuilder("\n");
void Walk(Transform t, int depth)
{
var rt = t as RectTransform;
var img = t.GetComponent<UnityEngine.UI.Image>();
sb.AppendLine($"{new string(' ', depth * 2)}{t.name} active={t.gameObject.activeSelf} pos={t.localPosition} size={(rt != null ? rt.sizeDelta.ToString() : "-")} scale={t.localScale}{(img != null ? " img=" + (img.sprite != null ? img.sprite.name : "null") : "")}");
if (depth < 5) foreach (Transform c in t) Walk(c, depth + 1);
}
var m = InspectionUIManager.Instance;
var ci = PlayerStore.Instance.currentClientInstance;
sb.AppendLine("当前顾客:" + (ci == null ? "无" : ci.GetClientBlueprint().identifier + " 意图 " + ci.GetClientBlueprint().clientIntent));
sb.AppendLine("检视物品:" + (m.currentInspectingItem?.identifier ?? "无") + " APPRAISAL=" + (m.currentInspectingItem?.IsTag("APPRAISAL_ITEM") ?? false));
Walk(m.panel.transform, 0);
Log.Info(sb.ToString());
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Edit claim + offer to buyer (test)", () => Log.Guard("卖出侧测试", () =>
{
var rng = Session.Data.Rng;
// 自己的一张真卡:标签机改成旗舰型号并把成色说成没问题,再摆给一位买家
var item = Items.NewCard("RZX70", 55, null, rng);
Items.Give(item);
var u = Items.ToUnit(item);
var c = Claim.Truth(u); c.Model = "RZX90"; c.Fan = 0; c.Fingers = 0; c.Bracket = 0; c.Sticker = 0;
item.EnableTag(Items.EditedTag);
Items.SetClaim(item, c, 0);
var client = new StoreClient { identifier = "pd_test_buyer", displayName = "买家", spriteName = SpriteDict.GetRandomScavSpriteName(), clientIntent = StoreClient.ClientIntent.BUY };
client.CompleteClientCreation();
PlayerStore.Instance.currentClientInstance = StoreClientInstance.CreateClientInstance(client);
Log.Info($"测试:卡 #{item.uniqueId} 说法 {c.Encode()},真实 {Claim.Truth(u).Encode()},价格 {item.unitValue},信誉前");
Items.OfferForTest(item);
Log.Info($"测试:验货后 Exposed={Items.Exposed(item)} 价格 {item.unitValue}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Buy scam card (test)", () => Log.Guard("买入侧测试", () =>
{
var rng = Session.Data.Rng;
var item = Items.NewCard("RZX90", 60, "RZX60", rng, DealerKind.Scammer, 0);
long before = item.unitValue;
GeneralHelper.SetItemOwned(item, true);
Items.Refresh(item);
Items.Give(item);
Log.Info($"买入测试:卡 #{item.uniqueId} 买前报价 {before},买下后价值 {item.unitValue},真实 {CardValue.True(Items.ToUnit(item))},说法 {(Items.GetClaim(item)?.Encode() ?? "无")}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{
EmporiumEntry.Instance.ClearFrontInv();
+329
View File
@@ -0,0 +1,329 @@
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Decentralized
{
/// <summary>
/// 放大镜检视显卡:和香烟一样把物品放大摆在检视台上,正反面各一张,下面是标签栏。
/// 标签栏每行一个标签(型号、风扇、金手指、挡板、丝印、贴纸):左边是卖家的说法,右边的按钮"指出"——
/// 说法属实就没事,隐瞒了问题价格回到真实情况,指错了掉信誉。归自己的卡,有标签机时按钮改成"改",随便改说法。
/// 游戏没有给 mod 开放检视界面,这里不打补丁,只在检视台(InspectionUIManager.panel)下面挂一个自己的界面,
/// 用一个常驻的 MonoBehaviour 每帧看一眼是否正在检视显卡。
/// </summary>
public sealed class GpuInspectView : MonoBehaviour
{
private const float RowH = 30f, CardScale = 3f, PanelTop = -112f, DetailH = 26f, GroupGap = 8f;
private const int RowCount = 5;
private TextMeshProUGUI detailText;
private static GpuInspectView instance;
private RectTransform root, cardRoot;
private Button flip;
private TextMeshProUGUI flipText;
private GameItem shown;
private string shownSig;
private bool hidGlass;
private Transform buttonTemplate;
private TMP_FontAsset font;
private readonly TextMeshProUGUI[] rowName = new TextMeshProUGUI[RowCount], rowText = new TextMeshProUGUI[RowCount];
private readonly Button[] rowButton = new Button[RowCount];
private readonly TextMeshProUGUI[] rowButtonText = new TextMeshProUGUI[RowCount];
private TextMeshProUGUI serialText;
public static void Ensure()
{
if (instance != null) return;
var go = new GameObject("pd_gpu_inspect_view");
DontDestroyOnLoad(go);
instance = go.AddComponent<GpuInspectView>();
}
public static void Remove()
{
if (instance != null) Destroy(instance.gameObject);
instance = null;
}
private void Update()
{
var m = InspectionUIManager.Instance;
var item = m != null && m.panel != null && m.panel.activeSelf ? m.currentInspectingItem : null;
if (item == null || !Items.IsGpu(item))
{
if (root != null && root.gameObject.activeSelf) root.gameObject.SetActive(false);
shown = null;
if (hidGlass && m != null && m.magnifyingGlass != null) { m.magnifyingGlass.SetActive(true); hidGlass = false; }
return;
}
Log.Guard("显卡检视大图", () =>
{
if (root == null) Build(m);
if (!root.gameObject.activeSelf) root.gameObject.SetActive(true);
// 检视台自带的放大镜图标会压在大图下面,显卡用不着
if (m.magnifyingGlass != null && m.magnifyingGlass.activeSelf) { m.magnifyingGlass.SetActive(false); hidGlass = true; }
var keys = new List<string>();
string sig = Items.LookKeys(item, keys) + "|" + string.Join(",", keys) + "|" + item.unitValue + "|" + Items.Exposed(item) + "|" + (Items.GetClaim(item)?.Encode() ?? "") + "|" + GeneralHelper.IsItemOwned(item) + "|" + HasLabeler();
if (item != shown || sig != shownSig)
{
shown = item;
shownSig = sig;
Redraw(item);
UpdateRows(item);
}
});
}
/// <summary>检视台上的叠加层:每个标签的每个取值对应一种细节,一一对应。</summary>
public static List<string> BigOverlays(GameItem item)
{
var o = new List<string>();
var u = Items.ToUnit(item);
if (u.Dead) return o;
var l = u.Appearance;
string P = "gpu_big_ov_";
if (!Items.IsBack(item))
{
int n = Items.FansOf(u);
if (l.Fan == 1) o.Add(P + "fan_dust2_" + n);
if (l.Fan == 2) { o.Add(P + "fan_dust1_" + n); o.Add(P + "fan_chip_" + n); }
if (l.Fingers == 1) o.Add(P + "fin_dim");
if (l.Fingers == 2) o.Add(P + "fin_oxide");
if (l.Bracket == 1) o.Add(P + "brk_scratch");
}
else
{
string model = (l.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model).ToLowerInvariant();
o.Add(P + "print_" + (l.Print == 2 ? "skew_" : l.Print == 1 ? "stamp_" : "normal_") + model);
if (l.Sticker == 2) o.Add(P + "seal_missing");
}
return o;
}
private static bool HasLabeler() => EmporiumEntry.Instance != null && EmporiumEntry.Instance.GetAllOwnedItemsByTag("LABELER_TAG").Count > 0;
private static void MakeRect(Transform parent, string name, Vector2 pos, Vector2 size, Color color)
{
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(parent, false);
rt.sizeDelta = size;
rt.localPosition = pos;
var img = go.GetComponent<Image>();
img.color = color;
img.raycastTarget = false;
}
private TextMeshProUGUI MakeText(Transform parent, string name, Vector2 pos, Vector2 size, TextAlignmentOptions align, float fontSize)
{
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(parent, false);
rt.sizeDelta = size;
rt.localPosition = pos;
var t = go.GetComponent<TextMeshProUGUI>();
if (font != null) t.font = font;
t.fontSize = fontSize;
t.alignment = align;
t.enableWordWrapping = false;
t.overflowMode = TextOverflowModes.Ellipsis;
t.raycastTarget = false;
return t;
}
private Button MakeButton(Transform parent, string name, Vector2 pos, Vector2 size, out TextMeshProUGUI label)
{
var go = Instantiate(buttonTemplate.gameObject, parent, false);
go.name = name;
var rt = (RectTransform)go.transform;
rt.sizeDelta = size;
rt.localPosition = pos;
var b = go.GetComponent<Button>();
b.onClick = new Button.ButtonClickedEvent();
label = go.GetComponentInChildren<TextMeshProUGUI>();
label.enableAutoSizing = true; label.fontSizeMin = 12; label.fontSizeMax = 24;
// 去掉复制来的按钮上驱动文字的本地化组件,文字由这里设置
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
string n = mb.GetType().Name;
if (mb is Button || mb is Image || mb is TextMeshProUGUI || mb is CanvasRenderer) continue;
if (n.Contains("Local") || n.Contains("Appraisal") || n.Contains("Send")) Destroy(mb);
}
return b;
}
private void Build(InspectionUIManager m)
{
root = new GameObject("_PdGpuInspection", typeof(RectTransform)).GetComponent<RectTransform>();
root.SetParent(m.panel.transform, false);
root.sizeDelta = new Vector2(1920, 1080);
root.localPosition = Vector3.zero;
cardRoot = new GameObject("Card", typeof(RectTransform)).GetComponent<RectTransform>();
cardRoot.SetParent(root, false);
cardRoot.localPosition = new Vector3(0, 85, 0);
buttonTemplate = m.appraisalPanel.transform.Find("buttonSend");
// 贴纸上的序列号:随卡变化,运行时用文字写在贴纸的空白处
serialText = MakeText(root, "Serial", Vector2.zero, new Vector2(48 * CardScale, 8 * CardScale), TextAlignmentOptions.Center, 20);
serialText.color = new Color(0.1f, 0.1f, 0.13f);
serialText.fontStyle = FontStyles.Bold;
serialText.overflowMode = TextOverflowModes.Overflow;
serialText.rectTransform.sizeDelta = new Vector2(48 * CardScale, 14 * CardScale);
if (buttonTemplate == null) return;
font = buttonTemplate.GetComponentInChildren<TextMeshProUGUI>()?.font;
if (font != null) serialText.font = font;
// 标签区:深色底加浅色边框,和上面的大图分开
float panelH = RowCount * RowH + 10f + DetailH + GroupGap, panelY = PanelTop - panelH / 2;
MakeRect(root, "LabelFrame", new Vector2(0, panelY), new Vector2(648, panelH + 8), new Color(0.78f, 0.68f, 0.45f, 1f));
MakeRect(root, "LabelBack", new Vector2(0, panelY), new Vector2(640, panelH), new Color(0.10f, 0.11f, 0.14f, 0.96f));
MakeRect(root, "LabelDivider", new Vector2(0, PanelTop - 5 - RowH * CardLabels.AuthRows - DetailH - GroupGap / 2), new Vector2(620, 2), new Color(0.78f, 0.68f, 0.45f, 0.8f));
detailText = MakeText(root, "Detail", new Vector2(-15, PanelTop - 5 - RowH * CardLabels.AuthRows - DetailH / 2), new Vector2(600, DetailH), TextAlignmentOptions.Left, 17);
flip = MakeButton(root, "Flip", new Vector2(0, PanelTop - panelH - 24), new Vector2(200, 30), out flipText);
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
for (int i = 0; i < RowCount; i++)
{
int row = CardLabels.Rows[i];
float y = PanelTop - 5 - RowH * (i + 0.5f) - (i >= CardLabels.AuthRows ? DetailH + GroupGap : 0f);
rowName[i] = MakeText(root, "RowName" + i, new Vector2(-255, y), new Vector2(110, RowH), TextAlignmentOptions.Left, 22);
rowText[i] = MakeText(root, "RowText" + i, new Vector2(-30, y), new Vector2(330, RowH), TextAlignmentOptions.Left, 22);
rowButton[i] = MakeButton(root, "RowButton" + i, new Vector2(245, y), new Vector2(110, RowH - 3), out rowButtonText[i]);
rowButton[i].onClick.AddListener(() => Log.Guard("标签栏", () => OnRowButton(row)));
}
}
private static string RowKey(int row) => row == CardLabels.RowSpec ? "spec" : row == CardLabels.RowBuild ? "build" : row == CardLabels.RowFan ? "fan" : row == CardLabels.RowFingers ? "fingers"
: row == CardLabels.RowBracket ? "bracket" : row == CardLabels.RowPrint ? "print" : row == CardLabels.RowSerial ? "serial" : "seal";
private static string ValueText(int row, int value) => row == CardLabels.RowSpec ? "" : L.T("lbl." + RowKey(row) + "." + (row == CardLabels.RowSeal && value == 1 ? 0 : value));
private static string RowName(int row) => L.T("lbl.row." + RowKey(row));
/// <summary>现场有没有卖家 / 鉴定客户在:决定"指出"按钮是否出现。</summary>
private static bool CanPoint(GameItem item)
{
var client = PlayerStore.Instance.currentClientInstance?.GetClientBlueprint();
if (client == null || GeneralHelper.IsItemOwned(item)) return false;
if (client.clientIntent == StoreClient.ClientIntent.APPRAISAL_SERVICE) return true;
return client.clientIntent == StoreClient.ClientIntent.SELL && !InspectionUIManager.IsConfrontLocked();
}
private void UpdateRows(GameItem item)
{
if (rowName[0] == null) return;
var u = Items.ToUnit(item);
var real = Claim.Truth(u);
var claim = Items.GetClaim(item);
int exposed = Items.Exposed(item);
bool owned = GeneralHelper.IsItemOwned(item);
bool canPoint = CanPoint(item), canEdit = owned && HasLabeler();
for (int i = 0; i < RowCount; i++)
{
int row = CardLabels.Rows[i];
rowName[i].text = RowName(row);
rowName[i].color = i < CardLabels.AuthRows ? new Color(1f, 0.85f, 0.45f) : new Color(0.6f, 0.85f, 1f); // 真伪行金黄,成色行浅蓝
rowName[i].fontSize = 22;
string text;
Color color = new Color(0.96f, 0.98f, 1f);
string Show(Claim c) => row == CardLabels.RowSpec ? DisplayModel(c.Model) : ValueText(row, c.Get(row));
if (claim == null) text = Show(real);
else if ((exposed & row) != 0)
{
text = Show(real);
bool lied = row == CardLabels.RowSpec ? claim.Value.Model != real.Model : claim.Value.Get(row) != real.Get(row);
if (lied) { color = new Color(1f, 0.45f, 0.4f); text += L.T("lbl.said", row == CardLabels.RowBuild ? L.T("lbl.tier." + claim.Value.Tier) : Show(claim.Value)); }
else color = new Color(0.5f, 1f, 0.55f);
}
else text = Show(claim.Value);
rowText[i].text = text;
rowText[i].color = color;
bool editable = row != CardLabels.RowBuild;
rowButton[i].gameObject.SetActive(canEdit ? editable : canPoint && (exposed & row) == 0);
rowButtonText[i].text = L.T(canEdit ? "lbl.edit" : "lbl.point");
}
// 型号真伪的依据:没拆穿之前提示去看哪里;拆穿之后列出实际查到的问题
if (detailText != null)
{
bool spec = (exposed & CardLabels.RowSpec) != 0;
string detail = claim == null ? "" : !spec ? L.T("lbl.hint") : Evidence(u, claim.Value, real);
detailText.text = detail;
detailText.color = spec ? new Color(1f, 0.7f, 0.6f) : new Color(0.75f, 0.78f, 0.85f);
}
if (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
// 贴纸上的序列号:只在背面显示
if (serialText != null)
{
serialText.gameObject.SetActive(Items.IsBack(item) && !u.Dead);
serialText.text = CardLabels.SerialText(u);
// 贴纸空白处中心在大图 (142, 91):卡片坐标以图中心为原点,y 向上
serialText.rectTransform.localPosition = new Vector3((142 - 96) * CardScale, 85 + (64 - 91) * CardScale, 0);
}
}
/// <summary>型号真伪的依据:实物与卖家说法对不上的地方(供电口与风扇数、序列号、背面丝印)。</summary>
private static string Evidence(Unit u, Claim claim, Claim real)
{
var parts = new List<string>();
if (claim.Tier != real.Tier) parts.Add(L.T("lbl.ev.build", L.T("lbl.tier." + real.Tier), Hardware.PowerPins[real.Tier], Hardware.FanCount[real.Tier]));
if (real.Serial != 0) parts.Add(L.T("lbl.ev.serial." + real.Serial));
if (real.Print != 0) parts.Add(L.T("lbl.ev.print." + real.Print));
return parts.Count == 0 ? L.T("lbl.ev.none") : L.T("lbl.ev.prefix") + string.Join(";", parts);
}
private static string DisplayModel(string model) => Items.DisplayModel(model);
private void OnRowButton(int row)
{
if (shown == null) return;
var item = shown;
if (GeneralHelper.IsItemOwned(item))
{
// 标签机:改说法,每按一次换下一个取值(型号是在八个型号里轮换)
var u = Items.ToUnit(item);
var cur = Items.GetClaim(item) ?? Claim.Truth(u);
if (row == CardLabels.RowSpec)
{
var models = Hardware.Specs.Values.Where(s => !s.Asic).Select(s => s.Model).ToList();
Items.EditClaim(item, row, 0, models[(models.IndexOf(cur.Model) + 1) % models.Count]);
}
else Items.EditClaim(item, row, row == CardLabels.RowSeal ? (cur.Get(row) == 0 ? 2 : 0) : row == CardLabels.RowBracket ? (cur.Get(row) + 1) % 2 : (cur.Get(row) + 1) % 3);
EmporiumAudioPlayer.Instance?.audioLabeler?.Play();
return;
}
int kind = Items.Point(item, row);
Log.Info($"指出 {row},结果 {kind},报价 {item.unitValue}");
if (kind == 0)
{
InspectionUIManager.Instance.OnConfrontFailed();
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T("point.none." + Random.Range(1, 3))));
}
else
{
InspectionUIManager.Instance.OnConfrontSuccess();
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T(kind == 2 ? "point.fake." + Random.Range(1, 3) : "point.worn." + Random.Range(1, 3))));
}
}
private void Redraw(GameItem item)
{
foreach (Transform c in cardRoot) Destroy(c.gameObject);
var keys = new List<string>();
string baseKey = Items.LookKeys(item, keys); // 小图键名只用来取底图(品牌与正反面)
var layers = new List<string> { "gpu_big_" + baseKey.Substring("gpu_".Length) };
layers.AddRange(BigOverlays(item));
foreach (var k in layers)
{
var sprite = Items.ModSprite(k);
if (sprite == null) continue;
var go = new GameObject(k, typeof(RectTransform), typeof(Image));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(cardRoot, false);
rt.sizeDelta = new Vector2(sprite.rect.width, sprite.rect.height);
rt.localScale = Vector3.one * CardScale;
var img = go.GetComponent<Image>();
img.sprite = sprite;
img.raycastTarget = false;
}
}
}
}
+251 -9
View File
@@ -15,12 +15,14 @@ namespace Decentralized
{
public const string Terminal = "pd_terminal";
public const string Rig = "pd_rig";
public const string Guide = "pd_gpu_guide";
public const string GpuPrefix = "pd_gpu_";
public const string HealthTag = "PD_HEALTH";
public const string FakeTag = "PD_FAKE_OF";
public const string RefurbTag = "PD_REFURBISHED";
public const string DeadTag = "PD_DEAD";
public const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", ClaimTag = "PD_CLAIM", ExposedTag = "PD_EXPOSED", MarkupTag = "PD_MARKUP", EditedTag = "PD_EDITED";
public static Action OpenTerminal;
@@ -35,6 +37,7 @@ namespace Decentralized
{
dir.Add(Terminal, CreateTerminal);
dir.Add(Rig, CreateRig);
dir.Add(Guide, CreateGuide);
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic))
{
var s = spec;
@@ -60,31 +63,211 @@ namespace Decentralized
return item;
}
/// <summary>显卡鉴定手册:档次对照、序列号规则、外观折扣。和游戏里的鉴定手册一样,是一件物品,说明里就是内容。</summary>
private static GameItem CreateGuide()
{
var item = ItemDirectory.CreateEmptyItem(Guide);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_guide");
item.name = L.T("item.guide.name");
item.shortDescription = L.T("item.guide.desc");
item.unitValue = 0;
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
// ---------- 显卡 ----------
/// <summary>RZX70 → "RZX 70",RZX80Ti → "RZX 80 Ti",RZ6800 → "RZ 6800"。</summary>
/// <summary>RZX70 → "RZX 70",RZ800 → "RZ 800"。</summary>
public static string DisplayModel(string model) => Regex.Replace(Regex.Replace(model, @"(?<=[A-Za-z])(?=\d)", " "), @"(?<=\d)(?=[A-Z])", " ");
private static GameItem CreateGpu(HardwareSpec spec)
{
var item = ItemDirectory.CreateEmptyItem(GpuId(spec.Model));
item.SetSpriteAndShapeFromMod(ModInfo.Id, spec.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd");
item.SetGameItemType("ELECTRONIC");
item.SetGameItemType("MATERIAL");
InspectableHelper.InitInspectableItem(item);
item.onLoaded = Refresh;
// 右键"切换"翻面;外观差异用叠加图画在同一张底图上
item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG);
item.mayToggleSlotItemFunc = (_, _, _) => true;
item.canToggleSlotItemFunc = (_, _, _) => true;
item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () =>
{
Flip(toggled);
});
item.getItemSpriteModifierForItemFunc = Overlays;
// 摆给顾客的时候,顾客可能看出说法与实物对不上
item.onSlotAddItemFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Combine(item.onSlotAddItemFunc,
(Action<GameItem, GameInventory, SlotMarker>)((it, _, __) => Log.Guard("顾客验货", () => OnOffered(it))));
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
Refresh(item);
return item;
}
/// <summary>按给定的状态造一张卡(二手卡、假卡):写入标签并生成外观。</summary>
public static GameItem NewCard(string model, double health, string fakeOf, Rng rng, DealerKind? seller = null, int markupPct = 0)
{
var item = ModHelper.SpawnItem(GpuId(model));
var u = new Unit { Model = model, FakeOf = fakeOf, Health = health, Look = CardLook.Generate(rng, health, fakeOf != null) };
Store(item, u);
if (seller != null)
{
GeneralHelper.SetItemOwned(item, false); // 卖家的货:买下之前不属于玩家,说法才有效
SetClaim(item, CardLabels.DealerClaim(u, seller.Value, rng), 0, markupPct);
}
return item;
}
// ---------- 标签栏:卖家的说法与指出 ----------
private static object checkedClient;
private static readonly HashSet<int> checkedItems = new HashSet<int>();
public const double CustomerSpotChance = 0.50;
public const int RepLossMax = 25;
/// <summary>被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。</summary>
public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
/// <summary>
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来;看出来就拆穿这一项(价格回到真实情况),
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
/// </summary>
public static void OfferForTest(GameItem item) { checkedItems.Clear(); OnOffered(item, true); }
private static void OnOffered(GameItem item, bool force = false)
{
if (item.IsTag(DeadTag) || !GeneralHelper.IsItemOwned(item) || (!force && !item.IsRootedInBackInv())) return;
var ci = PlayerStore.Instance.currentClientInstance;
var client = ci?.GetClientBlueprint();
if (client == null || (client.clientIntent != StoreClient.ClientIntent.BUY && client.clientIntent != StoreClient.ClientIntent.SELLNBUY)) return;
var claim = GetClaim(item);
if (claim == null) return;
if (!ReferenceEquals(checkedClient, ci)) { checkedClient = ci; checkedItems.Clear(); }
if (!checkedItems.Add(item.uniqueId)) return;
var u = ToUnit(item);
int exposed = Exposed(item);
int lies = CardLabels.Lies(u, claim.Value, exposed);
if (lies == 0) return;
var rng = Session.Data.Rng;
int found = 0;
foreach (int r in CardLabels.Rows)
if ((lies & r) != 0 && rng.Chance(CustomerSpotChance))
{
found |= r;
if (r != CardLabels.RowSpec && CardLabels.IsEvidence(r, claim.Value, Claim.Truth(u))) found |= CardLabels.RowSpec | CardLabels.RowBuild;
}
if (found == 0) return;
long before = item.unitValue;
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed | found));
Refresh(item);
int rep = RepLoss(before - item.unitValue);
string faction = string.IsNullOrEmpty(client.clientFaction) || StoreReputation.GetStoreReputation(client.clientFaction) == null ? "FACTION_LOWER_LEVEL" : client.clientFaction;
StoreReputation.ModReputation(faction, -rep);
EmporiumAudioPlayer.Instance?.audioFail?.Play();
Log.Info($"顾客看出标签不实:{found},价格 {item.unitValue},信誉 −{rep}");
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T((found & CardLabels.RowSpec) != 0 ? "cust.fake." + rng.Range(1, 2) : "cust.worn." + rng.Range(1, 2))));
}
/// <summary>标签上的说法;没有说法的卡(自己挖出来、正规渠道的新卡)一律按真实情况算。</summary>
public static Claim? GetClaim(GameItem item)
{
if (!item.IsTag(ClaimTag)) return null;
var c = Claim.Decode(item.GetTagReadonly(ClaimTag).GetString());
return string.IsNullOrEmpty(c.Model) ? (Claim?)null : c;
}
/// <summary>已指出的标签。型号一旦被拆穿,四个真伪行(型号、供电与风扇、序列号、丝印)一起亮出真实情况。</summary>
public static int Exposed(GameItem item)
{
int e = item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
if ((e & CardLabels.RowSpec) != 0) e |= CardLabels.RowSpec | CardLabels.RowBuild | CardLabels.RowSerial | CardLabels.RowPrint;
return e;
}
public static void SetClaim(GameItem item, Claim claim, int exposed, int markupPct = 0)
{
item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (markupPct != 0) item.ModifyTag(MarkupTag, t => t.Enable().SetInt(markupPct));
Refresh(item);
}
/// <summary>现在标签上写的型号:说法里的型号,被拆穿之后是真实芯片。</summary>
public static string BelievedModel(GameItem item, Unit u)
{
var c = GetClaim(item);
if (c == null) return u.Model;
return (Exposed(item) & CardLabels.RowSpec) != 0 ? (u.FakeOf ?? u.Model) : c.Value.Model;
}
/// <summary>玩家指出一个标签:记下来,说法改回真实情况;返回 0 说法属实、1 隐瞒了不利的情况、2 拆穿假货。</summary>
public static int Point(GameItem item, int row)
{
var u = ToUnit(item);
var claim = GetClaim(item) ?? Claim.Truth(u);
int exposed = Exposed(item);
int kind = CardLabels.Point(u, claim, row, ref exposed);
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (!item.IsTag(ClaimTag)) item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
Refresh(item);
if (item.IsTag("APPRAISAL_ITEM")) AppraisalHelper.SetDefaultAppraisalValue(item, (int)item.unitValue);
return kind;
}
/// <summary>用标签机改说法(随便改):这一项重新算作"没人指出过"。</summary>
public static void EditClaim(GameItem item, int row, int value, string model = null)
{
var u = ToUnit(item);
var claim = GetClaim(item) ?? Claim.Truth(u);
claim = row == CardLabels.RowSpec ? new Claim { Model = model ?? claim.Model, Fan = claim.Fan, Fingers = claim.Fingers, Bracket = claim.Bracket, Print = claim.Print, Sticker = claim.Sticker } : claim.With(row, value);
int exposed = Exposed(item) & ~row;
item.EnableTag(EditedTag);
SetClaim(item, claim, exposed);
}
/// <summary>说明:算力和耗电按标称写;成色在归玩家之前只给粗略等级(要看外观自己判断),归玩家之后给精确健康度。</summary>
private static string Desc(GameItem item, Unit u)
{
var label = Hardware.Specs[BelievedModel(item, u)];
string cond = GeneralHelper.IsItemOwned(item)
? Mathf.RoundToInt((float)u.Health) + "%"
: L.T(u.Health >= 85 ? "item.gpu.grade.1" : u.Health >= 60 ? "item.gpu.grade.2" : u.Health >= 35 ? "item.gpu.grade.3" : "item.gpu.grade.4");
return L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), cond);
}
private static void SyncValue(GameItem item)
{
if (item.IsTag(DeadTag)) return;
// 买下之后卖家的说法作废:卡回到真实情况,除非玩家用标签机自己改过
if (GeneralHelper.IsItemOwned(item) && item.IsTag(ClaimTag) && !item.IsTag(EditedTag))
{
item.ModifyTag(ClaimTag, t => t.Disable());
item.ModifyTag(ExposedTag, t => t.Disable());
item.ModifyTag(MarkupTag, t => t.Disable());
}
var u = ToUnit(item);
string desc = Desc(item, u);
if (item.shortDescription != desc) item.shortDescription = desc;
string name = L.T("item.gpu.name", Hardware.Specs[BelievedModel(item, u)].Brand, DisplayModel(BelievedModel(item, u)));
if (item.name != name) item.name = name;
long value = CardLabels.Value(u, GetClaim(item), Exposed(item));
if (!GeneralHelper.IsItemOwned(item) && item.IsTag(MarkupTag)) value = Math.Max(1, (long)Math.Round(value * (1 + item.GetTagReadonly(MarkupTag).GetInt() / 100.0)));
if (item.unitValue != value) item.unitValue = value;
}
public static Unit ToUnit(GameItem item)
{
string model = item.identifier.Substring(GpuPrefix.Length);
model = Hardware.Specs.Keys.First(k => k.ToLowerInvariant() == model);
model = Hardware.Specs.Keys.FirstOrDefault(k => k.ToLowerInvariant() == model) ?? "RZX60";
var u = new Unit { Model = model, Health = 100 };
if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString();
u.Refurbished = item.IsTag(RefurbTag);
u.Dead = item.IsTag(DeadTag);
if (item.IsTag(LookTag)) u.Look = CardLook.Decode(item.GetTagReadonly(LookTag).GetString());
// 外观种子:没有记录的卡(正规渠道的新卡)用物品编号当种子,之后同样随损耗变旧
u.Look.Seed = item.IsTag(SeedTag) ? (uint)item.GetTagReadonly(SeedTag).GetInt() : (uint)(item.uniqueId * 2654435761u) | 1u;
return u;
}
@@ -94,6 +277,8 @@ namespace Decentralized
if (u.FakeOf != null) item.ModifyTag(FakeTag, s => s.Enable().SetString(u.FakeOf));
if (u.Refurbished) item.EnableTag(RefurbTag);
if (u.Dead) item.EnableTag(DeadTag);
item.ModifyTag(LookTag, s => s.Enable().SetString(u.Look.Encode()));
item.ModifyTag(SeedTag, s => s.Enable().SetInt((int)u.Look.Seed));
Refresh(item);
}
@@ -107,12 +292,69 @@ namespace Decentralized
item.name = L.T("item.gpu.burnt.name", label.Brand, DisplayModel(label.Model));
item.shortDescription = L.T("item.gpu.burnt.desc");
item.unitValue = Math.Max(1, label.Price / 20);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_burnt");
item.SetSpriteAndShapeFromMod(ModInfo.Id, item.IsTag(BackTag) ? "gpu_burnt_b" : "gpu_burnt_f");
return;
}
item.name = L.T("item.gpu.name", label.Brand, DisplayModel(label.Model));
item.shortDescription = L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), Mathf.RoundToInt((float)u.Health));
item.unitValue = Math.Max(1, (long)Math.Round(label.Price * u.Health / 100));
item.SetSpriteAndShapeFromMod(ModInfo.Id, LookKeys(item, new List<string>()));
SyncValue(item);
}
public static bool IsBack(GameItem item) => item.IsTag(BackTag);
public static void Flip(GameItem item)
{
if (IsBack(item)) item.ModifyTag(BackTag, t => t.Disable()); else item.EnableTag(BackTag);
Refresh(item);
}
/// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
/// <summary>实物上的风扇数:由真实芯片的档次决定(低档次的板子套高档次的壳,风扇数露馅)。</summary>
public static int FansOf(Unit u) => Hardware.FanCount[Hardware.Specs[u.FakeOf ?? u.Model].Tier];
public static string LookKeys(GameItem item, List<string> overlays)
{
var u = ToUnit(item);
var l = u.Appearance;
bool back = IsBack(item);
string brand = u.Label.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd";
if (u.Dead) return back ? "gpu_burnt_b" : "gpu_burnt_f";
if (!back)
{
if (l.Fan > 0) overlays.Add($"gpu_ov_fan{l.Fan}_{FansOf(u)}");
if (l.Fingers > 0) overlays.Add("gpu_ov_fin" + l.Fingers);
if (l.Bracket > 0) overlays.Add("gpu_ov_brk1");
}
else
{
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = l.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model;
overlays.Add((l.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (l.Sticker == 2) overlays.Add("gpu_ov_stk2");
}
return brand + (back ? "_b" : "_f_t" + Hardware.Specs[u.FakeOf ?? u.Model].Tier);
}
public static Sprite ModSprite(string key)
{
var mod = ModLoader.Mods.Find(m => m.Manifest.ID.Equals(ModInfo.Id));
return mod != null && mod.Sprites.TryGetValue(key, out var sprite) ? sprite : null;
}
/// <summary>物品栏里的外观叠加图。</summary>
private static List<ItemSpriteModifier> Overlays(GameItem item)
{
var list = new List<ItemSpriteModifier>();
SyncValue(item); // 归属变化(买下)后价格随之变回真实价值
if (item.IsTag(DeadTag)) return list;
var keys = new List<string>();
LookKeys(item, keys);
int priority = 0;
foreach (var k in keys)
{
var sprite = ModSprite(k);
if (sprite != null) list.Add(new ItemSpriteModifier(sprite, Vector2.zero, 0, false, priority++, false, Color.white));
}
return list;
}
// ---------- 矿架 ----------
@@ -126,7 +368,7 @@ namespace Decentralized
var battery = new GameSlotInventory();
var upgrade = new GameSlotInventory();
var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7);
// 显卡每张 2×2 格:一列 2 格宽的格子,每个槽两行;未解锁的行不可放
// 显卡每张 3×2 格:3 格宽的格子,每张卡 3×2 格,每个槽两行;未解锁的行不可放
var cards = new GameGridInventory(RigSlots.Shape(RigSlots.Base), RigSlots.CellWidth);
var window = MachineHelper.CreateMachineryWindow(true,
MachineHelper.PowerPanel(battery),
+4
View File
@@ -15,6 +15,8 @@ namespace Decentralized
Unlock.Register();
Night.Subscribe();
DealerStock.Subscribe();
UsedDealerClient.Subscribe();
GpuInspectView.Ensure();
ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame;
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
@@ -25,6 +27,8 @@ namespace Decentralized
Items.Unsubscribe();
Night.Unsubscribe();
DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe();
GpuInspectView.Remove();
ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame;
}
+118 -4
View File
@@ -14,19 +14,16 @@ mat.common_electronic,Common electronics
mat.rare_electronic,Rare electronics
rig.label.cards,Graphics cards
item.gpu.name,{0} {1}
item.gpu.desc,"Hashrate {0} MH/s. Draws {1} kJ per day. Condition {2}%."
item.gpu.desc,"Rated hashrate {0} MH/s. Rated draw {1} kJ per day. Condition: {2}."
item.gpu.burnt.name,Burnt {0} {1}
item.gpu.burnt.desc,This card no longer works.
term.title,Trading Terminal
tab.wallet,Wallet
tab.trade,Trade
tab.report,Report
wallet.line,"{0}: {1} (about {2} credits)"
wallet.empty,The wallet is empty.
wallet.total,Total value: {0} credits
wallet.taint,Tainted value: {0} credits
trade.amount,Amount
trade.hint,Buy spends the amount in the quote coin. Sell sells the amount of the traded coin.
trade.deposit,Deposit credits as CRC
trade.withdraw,Withdraw CRC as credits
trade.pair,"{0}/{1} ({2})"
@@ -58,3 +55,120 @@ news.open.title,Poinbase Lists PSC
news.open.desc,The Upper Level exchange Poinbase now lists PSC against CRC. Graphics cards are in demand across the station.
news.halving.title,PSC Block Reward Halved
news.halving.desc,Miners now receive half as many coins per night.
appraisal.gpu,"Graphics card"
dealer.name,"Used-hardware dealer"
dealer.line1.1,"Cards from a mine clearance. Condition is written next to the name."
dealer.line1.2,"A few worked cards, going cheap."
dealer.line1.3,"Picked these up on the road. First-hand price."
dealer.line2.1,"See for yourself, turn them over and check the back."
dealer.line2.2,"No warranty. Look before you take."
dealer.line2.3,"Price is negotiable if you want them."
inspect.flip.back,"Turn over"
inspect.flip.front,"Front side"
item.gpu.grade.1,"like new"
item.gpu.grade.2,"decent"
item.gpu.grade.3,"clearly worn"
item.gpu.grade.4,"badly worn"
point.none.1,"There's nothing wrong with it. You misread it."
point.none.2,"Where? Don't make things up if you're not buying."
point.worn.1,"...Fine, it has that. I'll knock something off."
point.worn.2,"Sharp eyes. All right, this price."
point.fake.1,"...You caught it. It's a reflashed one. I'll price it for what it is."
point.fake.2,"Tch, not the original chip. Fine, the real chip's price."
lbl.fan.0,"Blades clean"
lbl.fan.1,"Blades dusty"
lbl.fan.2,"Blade chipped"
lbl.fingers.0,"Bright"
lbl.fingers.1,"Dull"
lbl.fingers.2,"Oxidised"
lbl.print.0,"Matches the model"
lbl.print.1,"Another model printed"
lbl.print.2,"Reprinted, crooked"
lbl.said," (seller said: {0})"
lbl.point,"Point out"
lbl.edit,"Edit"
cust.fake.1,"That's not what the label says. You trying to pass off a fake?"
cust.fake.2,"The model doesn't match. That isn't the chip it claims."
cust.worn.1,"That's not what you wrote. You left out the flaws."
cust.worn.2,"The condition on the label is false. Not at this price."
lbl.row.build,"Power & fans"
lbl.row.fan,"Blades"
lbl.row.fingers,"Contacts"
lbl.row.bracket,"Bracket"
lbl.row.print,"Back print"
lbl.row.serial,"Serial"
lbl.row.seal,"Seal"
lbl.build.0,"Entry: 6-pin, single fan"
lbl.build.1,"Mid: 8-pin, two fans"
lbl.build.2,"High: 8-pin, three fans"
lbl.build.3,"Flagship: dual 8-pin, three fans"
lbl.bracket.0,"Intact"
lbl.bracket.1,"Scratched"
lbl.serial.0,"Valid"
lbl.serial.1,"Bad factory code or length"
lbl.serial.2,"Tier code does not match"
lbl.seal.0,"Intact"
lbl.seal.2,"Missing"
lbl.tier.0,"Entry"
lbl.tier.1,"Mid"
lbl.tier.2,"High"
lbl.tier.3,"Flagship"
item.guide.name,"Graphics card appraisal guide"
item.guide.desc,"Tiers: entry has a 6-pin connector and one fan; mid 8-pin and two fans; high 8-pin and three fans; flagship dual 8-pin and three fans. Model names are display only: Probidia RZX 60 / 70 / 80 / 90, PMD RZ 70 / 80 / 800 / 900, lowest to highest tier. Serial: prefix PB (Probidia) or MD (PMD), tier code 1 to 4, a dash, a two-digit factory code, a dash, four digits. Valid factory codes: PB 11, 12, 21, 34, 47; MD 13, 22, 35, 41, 58. A tier code that does not match the model, or a bad factory code or length, means a fake. Appearance discounts: dusty blades -4%, chipped -12%; dull contacts -4%, oxidised -12%; scratched bracket -2%; missing seal -6%."
lbl.row.spec,"Model authenticity"
lbl.hint,"Check: power connector and fan count, serial, back print"
lbl.ev.prefix,"Evidence: "
lbl.ev.none,"Everything matches the model"
lbl.ev.build,"Hardware is {0} ({1}-pin, {2} fans)"
lbl.ev.serial.1,"Serial: bad factory code or length"
lbl.ev.serial.2,"Serial: tier code does not match"
lbl.ev.print.1,"Print shows another model"
lbl.ev.print.2,"Print was redone"
news.prehalving.title,"PSC Halving Ahead"
news.prehalving.desc,"Miners will receive half as many coins per night in three days. Holders expect the price to rise."
tab.home,"Home"
tab.bank,"Bank"
tab.poinbase,"Poinbase"
tab.web3,"Web3"
home.enter,"Enter"
home.summary,"Credits {0} | On-chain assets about {1} credits"
home.lastnight,"Night of day {0}: mined {1} PSC, {2} / {3} rigs powered"
entry.bank,"Bank"
entry.bank.l1,"Deposits, withdrawals, rent"
entry.bank.l2,"Credits {0}"
entry.poinbase,"Poinbase"
entry.poinbase.l1,"Compliant exchange"
entry.poinbase.l2,"PSC {0} credits"
entry.web3,"Web3"
entry.web3.l1,"Wallet and decentralised trading"
entry.web3.l2,"About {0} credits"
bank.cash,"Credits: {0}"
bank.rent,"Rent: {0} credits, due in {1} days"
bank.crc,"Poinbase account (CRC): {0}"
poin.account,"Poinbase account: CRC {0}, PSC {1}"
avg.line,"{0} average buy price: {1} credits"
avg.short,"avg. buy {0}"
avg.none,"—"
col.asset,"Asset"
col.amount,"Amount"
col.value,"Value"
col.avg,"Avg. buy"
bank.cash.k,"Credits"
bank.rent.k,"Rent"
bank.rent.v,"{0}, due in {1} days"
bank.crc.k,"Poinbase account (CRC)"
bank.ops,"Deposit / withdraw"
poin.account.h,"Poinbase account"
trade.h,"Trade"
web3.wallet.h,"On-chain wallet"
trade.hint,"Buying pays in the quote coin; |selling sells the traded coin."
bank.hint,"Deposit turns credits into CRC; |withdrawing turns CRC back into credits |and passes compliance review."
col.peg,"Price"
col.status,"Status"
stable.ok,"Pegged"
stable.depeg,"Depegged"
stable.warn,"Warning: staking subsidy pool low"
stable.spiral,"Collapsing"
aml.window,"Withdrawn in the last {0} days: {1} credits (cumulative line {2})"
aml.taint,"Wallet taint share: {0}%"
aml.chance,"At this amount, estimated review chance: {0}%"
1 # Probably Decentralized localization table: English (en)
14 mat.rare_electronic
15 rig.label.cards
16 item.gpu.name
17 item.gpu.desc
18 item.gpu.burnt.name
19 item.gpu.burnt.desc
20 term.title
tab.wallet
tab.trade
21 tab.report
22 wallet.line
23 wallet.empty
24 wallet.total
25 wallet.taint
26 trade.amount
trade.hint
27 trade.deposit
28 trade.withdraw
29 trade.pair
55 news.open.desc
56 news.halving.title
57 news.halving.desc
58 appraisal.gpu
59 dealer.name
60 dealer.line1.1
61 dealer.line1.2
62 dealer.line1.3
63 dealer.line2.1
64 dealer.line2.2
65 dealer.line2.3
66 inspect.flip.back
67 inspect.flip.front
68 item.gpu.grade.1
69 item.gpu.grade.2
70 item.gpu.grade.3
71 item.gpu.grade.4
72 point.none.1
73 point.none.2
74 point.worn.1
75 point.worn.2
76 point.fake.1
77 point.fake.2
78 lbl.fan.0
79 lbl.fan.1
80 lbl.fan.2
81 lbl.fingers.0
82 lbl.fingers.1
83 lbl.fingers.2
84 lbl.print.0
85 lbl.print.1
86 lbl.print.2
87 lbl.said
88 lbl.point
89 lbl.edit
90 cust.fake.1
91 cust.fake.2
92 cust.worn.1
93 cust.worn.2
94 lbl.row.build
95 lbl.row.fan
96 lbl.row.fingers
97 lbl.row.bracket
98 lbl.row.print
99 lbl.row.serial
100 lbl.row.seal
101 lbl.build.0
102 lbl.build.1
103 lbl.build.2
104 lbl.build.3
105 lbl.bracket.0
106 lbl.bracket.1
107 lbl.serial.0
108 lbl.serial.1
109 lbl.serial.2
110 lbl.seal.0
111 lbl.seal.2
112 lbl.tier.0
113 lbl.tier.1
114 lbl.tier.2
115 lbl.tier.3
116 item.guide.name
117 item.guide.desc
118 lbl.row.spec
119 lbl.hint
120 lbl.ev.prefix
121 lbl.ev.none
122 lbl.ev.build
123 lbl.ev.serial.1
124 lbl.ev.serial.2
125 lbl.ev.print.1
126 lbl.ev.print.2
127 news.prehalving.title
128 news.prehalving.desc
129 tab.home
130 tab.bank
131 tab.poinbase
132 tab.web3
133 home.enter
134 home.summary
135 home.lastnight
136 entry.bank
137 entry.bank.l1
138 entry.bank.l2
139 entry.poinbase
140 entry.poinbase.l1
141 entry.poinbase.l2
142 entry.web3
143 entry.web3.l1
144 entry.web3.l2
145 bank.cash
146 bank.rent
147 bank.crc
148 poin.account
149 avg.line
150 avg.short
151 avg.none
152 col.asset
153 col.amount
154 col.value
155 col.avg
156 bank.cash.k
157 bank.rent.k
158 bank.rent.v
159 bank.crc.k
160 bank.ops
161 poin.account.h
162 trade.h
163 web3.wallet.h
164 trade.hint
165 bank.hint
166 col.peg
167 col.status
168 stable.ok
169 stable.depeg
170 stable.warn
171 stable.spiral
172 aml.window
173 aml.taint
174 aml.chance
+118 -4
View File
@@ -13,19 +13,16 @@ mat.common_electronic,普通电子元件
mat.rare_electronic,稀有电子元件
rig.label.cards,显卡
item.gpu.name,{0} {1}
item.gpu.desc,算力 {0} MH/s。耗电 {1} kJ/天。健康度 {2}%。
item.gpu.desc,标称算力 {0} MH/s。标称耗电 {1} kJ/天。成色 {2}。
item.gpu.burnt.name,烧毁的 {0} {1}
item.gpu.burnt.desc,此显卡已无法工作。
term.title,交易终端
tab.wallet,钱包
tab.trade,交易
tab.report,报告
wallet.line,{0}:{1}(约 {2} 信用点)
wallet.empty,钱包为空。
wallet.total,总值:{0} 信用点
wallet.taint,污点:{0} 信用点
trade.amount,数量
trade.hint,买入按计价币的数量支付;卖出按所交易币的数量卖出。
trade.deposit,存入信用点(换成 CRC)
trade.withdraw,提取 CRC(换成信用点)
trade.pair,{0}/{1}({2})
@@ -57,3 +54,120 @@ news.open.title,Poinbase 上架 PSC
news.open.desc,上层交易所 Poinbase 上架 PSC/CRC 交易对。全站显卡需求上升。
news.halving.title,PSC 出块奖励减半
news.halving.desc,矿工每晚获得的币减半。
appraisal.gpu,"显卡"
dealer.name,"二手货贩子"
dealer.line1.1,"矿场清仓的卡,成色都在名字后面标着。"
dealer.line1.2,"几张挖过的卡,便宜出。"
dealer.line1.3,"路上收来的货,一手价。"
dealer.line2.1,"自己看,背面也翻过来看。"
dealer.line2.2,"不保修,看好了再拿。"
dealer.line2.3,"要的话价钱好商量。"
inspect.flip.back,"翻到背面"
inspect.flip.front,"翻到正面"
item.gpu.grade.1,"接近全新"
item.gpu.grade.2,"成色尚可"
item.gpu.grade.3,"明显旧了"
item.gpu.grade.4,"磨损严重"
point.none.1,"没毛病,你看错了吧。"
point.none.2,"这儿哪有问题?不买别瞎说。"
point.worn.1,"……行,这点毛病是有,算你便宜点。"
point.worn.2,"眼睛挺毒。好吧,这个价。"
point.fake.1,"……被你看出来了。是翻新的货,按真的价给你。"
point.fake.2,"啧,不是原装的。那就按芯片本来的价吧。"
lbl.fan.0,"叶片干净"
lbl.fan.1,"叶片积灰"
lbl.fan.2,"叶片缺角"
lbl.fingers.0,"光亮"
lbl.fingers.1,"发暗"
lbl.fingers.2,"氧化发绿"
lbl.print.0,"与型号一致"
lbl.print.1,"印着另一个型号"
lbl.print.2,"重印过,字歪"
lbl.said,"(卖家说:{0})"
lbl.point,"指出"
lbl.edit,"改"
cust.fake.1,"这不是它标的那块。你拿假货糊弄我?"
cust.fake.2,"型号对不上,里面的芯片根本不是这个。"
cust.worn.1,"跟你写的不一样,毛病你没说。"
cust.worn.2,"标签上的成色是假的吧,这个价我不要。"
lbl.row.build,"供电与风扇"
lbl.row.fan,"叶片"
lbl.row.fingers,"金手指"
lbl.row.bracket,"挡板"
lbl.row.print,"背面丝印"
lbl.row.serial,"序列号"
lbl.row.seal,"封条"
lbl.build.0,"低端:6针,单风扇"
lbl.build.1,"中端:8针,双风扇"
lbl.build.2,"高端:8针,三风扇"
lbl.build.3,"旗舰:双8针,三风扇"
lbl.bracket.0,"完好"
lbl.bracket.1,"有划痕"
lbl.serial.0,"合规"
lbl.serial.1,"工厂码或位数不合规"
lbl.serial.2,"档次码与型号不符"
lbl.seal.0,"完好"
lbl.seal.2,"已缺失"
lbl.tier.0,"低端"
lbl.tier.1,"中端"
lbl.tier.2,"高端"
lbl.tier.3,"旗舰"
item.guide.name,"显卡鉴定手册"
item.guide.desc,"档次:低端 6 针供电、单风扇;中端 8 针、双风扇;高端 8 针、三风扇;旗舰双 8 针、三风扇。型号只是显示名:Probidia 为 RZX 60 / 70 / 80 / 90,PMD 为 RZ 70 / 80 / 800 / 900,按档次从低到高。序列号:前缀 PB(Probidia)或 MD(PMD),接档次码 1 至 4,一道横杠,两位工厂码,一道横杠,四位流水。合法工厂码:PB 为 11、12、21、34、47;MD 为 13、22、35、41、58。档次码与型号不符,或工厂码、位数不对,都是假的。外观折扣:叶片积灰 -4%,缺角 -12%;金手指发暗 -4%,氧化 -12%;挡板划痕 -2%;封条缺失 -6%。"
lbl.row.spec,"型号真伪"
lbl.hint,"依据:供电口与风扇数、序列号、背面丝印"
lbl.ev.prefix,"依据:"
lbl.ev.none,"各项依据都与型号相符"
lbl.ev.build,"实物是{0}({1} 针,{2} 风扇)"
lbl.ev.serial.1,"序列号工厂码或位数不合规"
lbl.ev.serial.2,"序列号档次码与型号不符"
lbl.ev.print.1,"丝印印着另一个型号"
lbl.ev.print.2,"丝印重印过"
news.prehalving.title,"PSC 即将减半"
news.prehalving.desc,"三天后矿工每晚获得的币减半。持币者预期币价上行。"
tab.home,"首页"
tab.bank,"银行"
tab.poinbase,"Poinbase"
tab.web3,"Web3"
home.enter,"进入"
home.summary,"信用点 {0} | 链上资产约 {1} 信用点"
home.lastnight,"第 {0} 天夜间:挖出 {1} PSC,{2} / {3} 个矿架有电"
entry.bank,"银行"
entry.bank.l1,"信用点存取、房租"
entry.bank.l2,"信用点 {0}"
entry.poinbase,"Poinbase"
entry.poinbase.l1,"合规交易所"
entry.poinbase.l2,"PSC {0} 信用点"
entry.web3,"Web3"
entry.web3.l1,"钱包与去中心化交易"
entry.web3.l2,"约 {0} 信用点"
bank.cash,"信用点:{0}"
bank.rent,"房租:{0} 信用点,{1} 天后支付"
bank.crc,"Poinbase 账户(CRC):{0}"
poin.account,"Poinbase 账户:CRC {0},PSC {1}"
avg.line,"{0} 买入均价:{1} 信用点"
avg.short,"买入均价 {0}"
avg.none,"—"
col.asset,"币种"
col.amount,"数量"
col.value,"估值"
col.avg,"买入均价"
bank.cash.k,"信用点"
bank.rent.k,"房租"
bank.rent.v,"{0},{1} 天后支付"
bank.crc.k,"Poinbase 账户(CRC)"
bank.ops,"存取"
poin.account.h,"Poinbase 账户"
trade.h,"交易"
web3.wallet.h,"链上钱包"
trade.hint,"买入按计价币的数量支付;|卖出按所交易币的数量卖出。"
bank.hint,"存入把信用点换成 CRC;|提取把 CRC 换回信用点,|提取要过合规审查。"
col.peg,"价格"
col.status,"状态"
stable.ok,"锚定正常"
stable.depeg,"脱锚"
stable.warn,"预兆:质押补贴池告急"
stable.spiral,"崩盘中"
aml.window,"近 {0} 天已提取 {1} 信用点(累计线 {2})"
aml.taint,"钱包污点比例:{0}%"
aml.chance,"按这个数量,预计审查概率:{0}%"
1 # Probably Decentralized 本地化表:简体中文(zh)
13 mat.rare_electronic
14 rig.label.cards
15 item.gpu.name
16 item.gpu.desc
17 item.gpu.burnt.name
18 item.gpu.burnt.desc
19 term.title
tab.wallet
tab.trade
20 tab.report
21 wallet.line
22 wallet.empty
23 wallet.total
24 wallet.taint
25 trade.amount
trade.hint
26 trade.deposit
27 trade.withdraw
28 trade.pair
54 news.open.desc
55 news.halving.title
56 news.halving.desc
57 appraisal.gpu
58 dealer.name
59 dealer.line1.1
60 dealer.line1.2
61 dealer.line1.3
62 dealer.line2.1
63 dealer.line2.2
64 dealer.line2.3
65 inspect.flip.back
66 inspect.flip.front
67 item.gpu.grade.1
68 item.gpu.grade.2
69 item.gpu.grade.3
70 item.gpu.grade.4
71 point.none.1
72 point.none.2
73 point.worn.1
74 point.worn.2
75 point.fake.1
76 point.fake.2
77 lbl.fan.0
78 lbl.fan.1
79 lbl.fan.2
80 lbl.fingers.0
81 lbl.fingers.1
82 lbl.fingers.2
83 lbl.print.0
84 lbl.print.1
85 lbl.print.2
86 lbl.said
87 lbl.point
88 lbl.edit
89 cust.fake.1
90 cust.fake.2
91 cust.worn.1
92 cust.worn.2
93 lbl.row.build
94 lbl.row.fan
95 lbl.row.fingers
96 lbl.row.bracket
97 lbl.row.print
98 lbl.row.serial
99 lbl.row.seal
100 lbl.build.0
101 lbl.build.1
102 lbl.build.2
103 lbl.build.3
104 lbl.bracket.0
105 lbl.bracket.1
106 lbl.serial.0
107 lbl.serial.1
108 lbl.serial.2
109 lbl.seal.0
110 lbl.seal.2
111 lbl.tier.0
112 lbl.tier.1
113 lbl.tier.2
114 lbl.tier.3
115 item.guide.name
116 item.guide.desc
117 lbl.row.spec
118 lbl.hint
119 lbl.ev.prefix
120 lbl.ev.none
121 lbl.ev.build
122 lbl.ev.serial.1
123 lbl.ev.serial.2
124 lbl.ev.print.1
125 lbl.ev.print.2
126 news.prehalving.title
127 news.prehalving.desc
128 tab.home
129 tab.bank
130 tab.poinbase
131 tab.web3
132 home.enter
133 home.summary
134 home.lastnight
135 entry.bank
136 entry.bank.l1
137 entry.bank.l2
138 entry.poinbase
139 entry.poinbase.l1
140 entry.poinbase.l2
141 entry.web3
142 entry.web3.l1
143 entry.web3.l2
144 bank.cash
145 bank.rent
146 bank.crc
147 poin.account
148 avg.line
149 avg.short
150 avg.none
151 col.asset
152 col.amount
153 col.value
154 col.avg
155 bank.cash.k
156 bank.rent.k
157 bank.rent.v
158 bank.crc.k
159 bank.ops
160 poin.account.h
161 trade.h
162 web3.wallet.h
163 trade.hint
164 bank.hint
165 col.peg
166 col.status
167 stable.ok
168 stable.depeg
169 stable.warn
170 stable.spiral
171 aml.window
172 aml.taint
173 aml.chance
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more