Compare commits
No files matched your search
@@ -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 存档数据。中英双语。
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}%"
|
||||
|
@@ -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}%"
|
||||
|
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 397 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 340 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 706 B |
|
After Width: | Height: | Size: 402 B |
|
After Width: | Height: | Size: 388 B |
|
After Width: | Height: | Size: 405 B |
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 428 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 507 B |
|
After Width: | Height: | Size: 582 B |
|
After Width: | Height: | Size: 593 B |
|
After Width: | Height: | Size: 596 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 592 B |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 805 B |
|
After Width: | Height: | Size: 848 B |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 883 B |
|
After Width: | Height: | Size: 855 B |
|
After Width: | Height: | Size: 883 B |
|
After Width: | Height: | Size: 881 B |
|
After Width: | Height: | Size: 874 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 375 B |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 607 B |
|
After Width: | Height: | Size: 137 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 139 B |
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 113 B |
|
After Width: | Height: | Size: 116 B |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 129 B |
|
After Width: | Height: | Size: 126 B |
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 142 B |
|
After Width: | Height: | Size: 145 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 150 B |
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 136 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 139 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 146 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 142 B |