2 Commits
Author SHA1 Message Date
ZHU MINCHAOandClaude Sonnet 5.5 9b48d7bf0e Used-hardware dealer customer and GPU appraisal registration
A scav-type seller visits now and then with three to five used cards. An
honest dealer sells at true value, a middleman adds 8%, a scammer mixes in
fakes (relabelled cheap chips) priced at 90% of the labelled model's value.
The markup is a client-temporary item feature, so a bought card is only
worth its real value. Cards are registered as appraisal items; the correct
value is the same function as the sale value.

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

No files matched your search

+231 -67
View File
@@ -77,79 +77,208 @@ class Canvas:
f.write(data)
def gpu(accent, accent_dark, burnt=False):
"""显卡(32×32,2×2 格),略微俯视:顶面是带两个风扇的散热罩,左端金属挡板(带接口),
前沿露出 PCB 和金手指,右端一道暗侧面,顶上一排供电口。每格中心都不透明。"""
c = Canvas(32, 32)
TOP0, TOP1 = 4, 22 # 顶面行范围
L, R = 6, 29 # 顶面左右(左边是挡板)
# 金属 I/O 挡板(左端竖条,带两个接口孔)
c.rect(1, 2, 5, 27, 'b')
c.rect(1, 2, 1, 27, 'h'); c.rect(5, 2, 5, 27, 'd')
for y in (6, 12): # 两个视频接口(深色矩形孔,带高光边)
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'g')
c.put(3, 20, 'k'); c.put(3, 22, 'k'); c.put(3, 24, 'k') # 下部散热孔
# 散热罩顶面
for y in range(TOP0, TOP1 + 1):
for x in range(L, R + 1):
c.put(x, y, 'm')
c.put(R, y, 'd'); c.put(R - 1, y, 'd')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
# 品牌色条(沿罩顶前沿)
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, accent)
c.put(L + 1, TOP1, accent_dark); c.put(R - 2, TOP1, accent_dark)
# 两个风扇(圆形,叶片用十字 + 斜线)
for cx in (11, 22):
cy = 13
for y in range(cy - 6, cy + 7):
for x in range(cx - 6, cx + 7):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= 36:
c.put(x, y, 'k')
if 20 <= d2 <= 30:
c.put(x, y, 'g')
for k in range(-4, 5):
if abs(k) >= 1:
c.put(cx + k, cy, 'g'); c.put(cx, cy + k, 'g')
c.put(cx - 2, cy - 2, 'g'); c.put(cx + 2, cy + 2, 'g'); c.put(cx + 2, cy - 2, 'g'); c.put(cx - 2, cy + 2, 'g')
c.rect(cx - 1, cy - 1, cx + 1, cy + 1, 'b')
c.put(cx, cy, 'h')
# 前沿:PCB 绿色薄边 + 金手指
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'g2')
c.put(x, TOP1 + 2, 'g2')
for x in range(L, R + 1): # 金手指:连续金色带,中间一道缺口(PCIe 键位),带深色触点线
c.put(x, TOP1 + 3, 'Y'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'e')
for y in (TOP1 + 3, TOP1 + 4):
c.put(L + 13, y, 'k'); c.put(L + 14, y, 'k')
# 顶上一排供电口(两个黑色插口)
for x0 in (L + 3, L + 9):
c.rect(x0, TOP0 - 1, x0 + 4, TOP0 - 1, 'k')
# 保证每格中心不透明:下半部(第二行格子,y 16..31 的中心 y=24)落在金手指/PCB 上
for x in range(L, R + 1):
for y in range(TOP1 + 1, 28):
if c.px[y][x] is None:
c.put(x, y, 'k')
if burnt:
for x, y in [(9, 8), (10, 9), (13, 17), (24, 8), (25, 9), (27, 18), (17, 12), (18, 11)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, 'e')
for x, y in [(15, 5), (16, 5), (20, 21), (26, 14)]:
c.put(x, y, 'c')
# 轮廓
FONT = { # 3×5 像素字(丝印用)
'R': '110101110101101', 'Z': '111001010100111', 'X': '101101010101101', 'P': '110101110100100',
'M': '101111111101101', 'D': '110101101101110', 'T': '111010010010010', 'i': '010000010010010',
'0': '111101101101111', '1': '010110010010111', '2': '110001010100111', '3': '110001010001110',
'4': '101101111001001', '5': '111100110001110', '6': '011100111101111', '7': '111001010010010',
'8': '111101111101111', '9': '111101111001110',
}
def text(c, x, y, s, color, skew=False):
"""在画布上写 3×5 像素字;skew 为重新印过的效果:字一高一低、间距不匀。"""
i = 0
for ch in s:
if ch == ' ':
x += 2
continue
dy = (i % 2) if skew else 0
for k, bit in enumerate(FONT[ch]):
if bit == '1':
c.put(x + k % 3, y + k // 3 + dy, color)
x += 4 + (1 if skew and i % 3 == 2 else 0)
i += 1
W, H = 48, 32
TOP0, TOP1 = 4, 22 # 顶面行范围
L, R = 6, 45 # 顶面左右(左边是挡板)
FANS = ((17, 13), (34, 13)) # 两个风扇中心
def outline(c):
solid = lambda v: v is not None
get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None
edge = [(x, y) for y in range(c.h) for x in range(c.w)
if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))]
for x, y in edge:
c.put(x, y, 'O')
def bracket(c):
"""左端金属挡板,三个接口孔。"""
c.rect(1, 2, 5, 27, 'b')
c.rect(1, 2, 1, 27, 'h'); c.rect(5, 2, 5, 27, 'd')
for y in (5, 11, 17):
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'g')
c.put(3, 22, 'k'); c.put(3, 24, 'k')
def gpu_front(accent, accent_dark, burnt=False):
"""显卡正面(48×32,3×2 格),略微俯视:顶面是带两个风扇的散热罩,左端金属挡板(三个接口),
前沿露出 PCB 和金手指,右端一道暗侧面。每格中心都不透明。"""
c = Canvas(W, H)
bracket(c)
for y in range(TOP0, TOP1 + 1):
for x in range(L, R + 1):
c.put(x, y, 'm')
c.put(R, y, 'd'); c.put(R - 1, y, 'd')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, accent)
c.put(L + 1, TOP1, accent_dark); c.put(R - 2, TOP1, accent_dark)
for cx, cy in FANS:
for y in range(cy - 7, cy + 8):
for x in range(cx - 7, cx + 8):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= 49:
c.put(x, y, 'k')
if 26 <= d2 <= 38:
c.put(x, y, 'g')
for k in range(-5, 6):
if abs(k) >= 1:
c.put(cx + k, cy, 'g'); c.put(cx, cy + k, 'g')
for dx, dy in ((-3, -3), (3, 3), (3, -3), (-3, 3)):
c.put(cx + dx, cy + dy, 'g')
c.rect(cx - 1, cy - 1, cx + 1, cy + 1, 'b')
c.put(cx, cy, 'h')
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'g2'); c.put(x, TOP1 + 2, 'g2')
for x in range(L, R + 1):
c.put(x, TOP1 + 3, 'Y'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'e')
for y in (TOP1 + 3, TOP1 + 4):
c.put(L + 17, y, 'k'); c.put(L + 18, y, 'k')
for x0 in (L + 4, L + 12): # 顶上供电口
c.rect(x0, TOP0 - 1, x0 + 4, TOP0 - 1, 'k')
for x in range(L, R + 1):
for y in range(TOP1 + 1, 28):
if c.px[y][x] is None:
c.put(x, y, 'k')
if burnt:
for x, y in [(12, 8), (13, 9), (18, 17), (35, 8), (36, 9), (40, 18), (24, 12), (25, 11)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, 'e')
outline(c)
return c
def gpu_back(accent, accent_dark, burnt=False):
"""显卡背面(翻面后):左端仍是挡板;黑色背板,上面一行丝印(型号用叠加图画),
右下一张序列号贴纸,贴纸边上一道封条。"""
c = Canvas(W, H)
bracket(c)
for y in range(TOP0, 28):
for x in range(L, R + 1):
c.put(x, y, 'd')
c.put(R, y, 'k'); c.put(R - 1, y, 'k')
for x in range(L, R + 1):
c.put(x, TOP0, 'm'); c.put(x, TOP0 + 1, 'm')
for x in range(L + 1, R - 1):
c.put(x, 16, accent)
c.put(L + 1, 17, accent_dark); c.put(R - 2, 17, accent_dark)
for x in range(L + 2, R - 2, 5): # 背板螺丝
for y in (22, 25):
c.put(x, y, 'b'); c.put(x + 1, y, 'k')
c.rect(26, 18, 41, 21, 'h') # 序列号贴纸(印刷条码)
for x in range(27, 41, 2):
c.rect(x, 19, x, 20, 'k')
c.rect(44, 17, 44, 22, 'R') # 封条(红色)
c.rect(43, 17, 43, 22, 'r2')
for x in range(L, R + 1):
for y in range(28, 28):
pass
if burnt:
for x, y in [(14, 9), (15, 10), (30, 11), (36, 13), (20, 14)]:
c.put(x, y, 'c')
outline(c)
return c
def overlay():
return Canvas(W, H)
def ov_fan(kind):
"""风扇叠加:1 积灰(叶片上一层灰),2 叶片缺角(边缘缺一块并露出黑底)。"""
c = overlay()
for cx, cy in FANS:
if kind == 1:
for dx, dy in ((-4, -2), (-2, -4), (3, -3), (4, 1), (-3, 3), (2, 4), (-5, 0), (0, 5), (1, -5), (-1, 2), (5, -1), (3, 2)):
c.put(cx + dx, cy + dy, 'l')
for dx, dy in ((-4, -1), (-1, -4), (4, -2), (2, 3), (-3, 4)):
c.put(cx + dx, cy + dy, 'h')
else:
for dx, dy in ((4, -4), (5, -4), (5, -3), (4, -3), (3, -4), (5, -5), (6, -3)):
c.put(cx + dx, cy + dy, 'k')
c.put(cx - 5, cy + 4, 'k'); c.put(cx - 4, cy + 5, 'k'); c.put(cx - 5, cy + 5, 'k')
return c
def ov_fingers(kind):
"""金手指叠加:1 发暗(触点变褐),2 氧化发绿。"""
c = overlay()
for x in range(L, R + 1):
for y in (TOP1 + 3, TOP1 + 4):
if x in (L + 17, L + 18):
continue
if kind == 1:
c.put(x, y, 'e' if (x + y) % 3 else 'Y')
else:
c.put(x, y, 'g2' if (x + y) % 2 else 'G')
return c
def ov_bracket(kind):
"""挡板叠加:1 划痕;2 接口数量不对(第三个接口被封成一块平板,只剩两个)。"""
c = overlay()
if kind == 1:
for x, y in ((2, 9), (3, 10), (4, 11), (2, 15), (3, 15), (3, 16), (2, 20), (3, 21), (4, 22), (4, 25), (3, 25)):
c.put(x, y, 'l' if (x + y) % 2 else 'd')
else:
c.rect(2, 17, 4, 20, 'b')
c.rect(2, 17, 4, 17, 'h')
return c
def ov_text(model, skew):
"""背面丝印叠加:写型号。skew 为重新印过的版本(字高低不齐)。"""
c = overlay()
text(c, 10, 8, model, 'l' if skew else 'h', skew)
return c
def ov_sticker(kind):
"""贴纸叠加:1 手写序列号(条码被盖住、写了歪歪扭扭的字);2 封条缺失(只剩撕过的残胶)。"""
c = overlay()
if kind == 1:
c.rect(26, 18, 41, 21, 'h')
for x, y in ((27, 20), (28, 19), (29, 20), (30, 19), (32, 20), (33, 19), (33, 21), (35, 19), (36, 20), (37, 19), (38, 21), (39, 20), (40, 19)):
c.put(x, y, 'O')
else:
c.rect(43, 17, 44, 22, 'd')
for y in (17, 19, 21, 22):
c.put(44, y, 'm')
c.put(43, 18, 'm'); c.put(43, 20, 'm')
return c
MODELS = ['RZX 50', 'RZX 60', 'RZX 70', 'RZX 80 Ti', 'RZX 90', 'RZ 470', 'RZ 580', 'RZ 6800', 'RZ 7900']
def rig():
"""矿架(48×48,3×3 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
@@ -283,14 +412,33 @@ def terminal():
return c
def key(model):
return model.replace(' ', '').lower()
SPRITES = {
'gpu_probidia': lambda: gpu('G', 'g2'),
'gpu_pmd': lambda: gpu('R', 'r2'),
'gpu_burnt': lambda: gpu('e', 'c', burnt=True),
'gpu_probidia_f': lambda: gpu_front('G', 'g2'),
'gpu_probidia_b': lambda: gpu_back('G', 'g2'),
'gpu_pmd_f': lambda: gpu_front('R', 'r2'),
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
'gpu_burnt_f': lambda: gpu_front('e', 'c', burnt=True),
'gpu_burnt_b': lambda: gpu_back('e', 'c', burnt=True),
'rig': rig,
'terminal': terminal,
}
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
OVERLAYS = {
'gpu_ov_fan1': lambda: ov_fan(1), 'gpu_ov_fan2': lambda: ov_fan(2),
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
'gpu_ov_brk1': lambda: ov_bracket(1), 'gpu_ov_brk2': lambda: ov_bracket(2),
'gpu_ov_stk1': lambda: ov_sticker(1), 'gpu_ov_stk2': lambda: ov_sticker(2),
}
for _m in MODELS:
OVERLAYS['gpu_ov_txt_' + key(_m)] = (lambda m=_m: ov_text(m, False))
OVERLAYS['gpu_ov_txs_' + key(_m)] = (lambda m=_m: ov_text(m, True))
def check_cells(canvas, name):
"""游戏按 mod 物品图每格的中心像素是否不透明来判断占格(行从图的下沿往上数)。有空格就报错。"""
cols, rows = canvas.w // 16, canvas.h // 16
@@ -306,6 +454,20 @@ if __name__ == '__main__':
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(sys.argv[2], name + '.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
# 预览:把各种叠加图叠在底图上
def stack(base, *ovs):
b = SPRITES[base]()
for o in ovs:
oc = OVERLAYS[o]()
for y in range(oc.h):
for x in range(oc.w):
if oc.px[y][x] is not None:
b.px[y][x] = oc.px[y][x]
return b
stack('gpu_probidia_f', 'gpu_ov_fan1', 'gpu_ov_fin1', 'gpu_ov_brk1').png(os.path.join(sys.argv[2], 'prev_front_worn.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
stack('gpu_pmd_f', 'gpu_ov_fan2', 'gpu_ov_fin2', 'gpu_ov_brk2').png(os.path.join(sys.argv[2], 'prev_front_fake.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
stack('gpu_probidia_b', 'gpu_ov_txt_rzx80ti').png(os.path.join(sys.argv[2], 'prev_back_ok.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
stack('gpu_pmd_b', 'gpu_ov_txs_rz6800', 'gpu_ov_stk1', 'gpu_ov_stk2').png(os.path.join(sys.argv[2], 'prev_back_fake.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
print(sys.argv[2])
else:
out = os.path.normpath(os.path.join(here, '..', 'src', 'Decentralized', 'ModFiles', 'Textures', 'Items'))
@@ -313,4 +475,6 @@ if __name__ == '__main__':
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(out, name + '.png'))
for name, make in OVERLAYS.items():
make().png(os.path.join(out, name + '.png'))
print(out)
+10
View File
@@ -22,6 +22,16 @@ namespace Decentralized.Domain
public const int Variants = 3;
/// <summary>五位数字,存在物品标签上:风扇、金手指、挡板、丝印、贴纸。</summary>
public string Encode() => $"{Fan}{Fingers}{Bracket}{Print}{Sticker}";
public static CardLook Decode(string s)
{
if (s == null || s.Length != 5) 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) };
}
/// <summary>只在假卡上出现的取值:玩家一旦看到,就能肯定是假的。</summary>
public bool HasFakeSign => Bracket == 2 || Print != 0 || Sticker == 1;
+2
View File
@@ -84,6 +84,8 @@ namespace Decentralized.Domain
public double Health = 100;
public bool Dead;
public bool Refurbished;
/// <summary>外观;新卡(正规渠道)没有记录,按全新画。</summary>
public CardLook Look;
public HardwareSpec Label => Hardware.Specs[Model];
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model];
+2 -2
View File
@@ -9,7 +9,7 @@ namespace Decentralized.Domain
public static class RigSlots
{
public const int Base = 2, Max = 5;
public const int CellWidth = 2, CellsPerSlot = 2;
public const int CellWidth = 3, CellsPerSlot = 2;
/// <summary>升级到第 n 个槽需要的电子元件(物品 id,件数)。n 从 3 开始。</summary>
public static (string Item, int Count) Cost(int slot)
@@ -45,7 +45,7 @@ namespace Decentralized.Domain
return take;
}
/// <summary>显卡格子的形状字符串:宽 2,高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。</summary>
/// <summary>显卡格子的形状字符串:宽 3(一张卡 3×2 格),高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。</summary>
public static string Shape(int slots)
{
var sb = new System.Text.StringBuilder();
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
public enum DealerKind { Honest, Middleman, Scammer }
/// <summary>贩子带来的一张卡:<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, ScamDiscount = 0.90;
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)];
var u = new Unit { Model = card.Model, FakeOf = card.FakeOf, Health = card.Health };
card.MarkupPct = (int)Math.Round((CardValue.Of(label, card.Health) * ScamDiscount / CardValue.True(u) - 1) * 100);
}
else if (kind == DealerKind.Middleman) card.MarkupPct = (int)Math.Round(MiddlemanMarkup * 100);
cards.Add(card);
}
return cards;
}
}
}
+8
View File
@@ -15,6 +15,14 @@ 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("RZX80Ti", 40, null, rng));
Items.Give(Items.NewCard("RZX80Ti", 70, "RZX60", rng));
Items.Give(Items.NewCard("RZ6800", 30, "RZ580", rng));
Items.Give(Items.NewCard("RZ7900", 90, "RZ470", rng));
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{
EmporiumEntry.Instance.ClearFrontInv();
+61 -4
View File
@@ -21,6 +21,7 @@ namespace Decentralized
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";
public static Action OpenTerminal;
@@ -68,14 +69,32 @@ namespace Decentralized
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.onLoaded = Refresh;
// 右键"切换"翻面;外观差异用叠加图画在同一张底图上
item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG);
item.mayToggleSlotItemFunc = (_, _, _) => true;
item.canToggleSlotItemFunc = (_, _, _) => true;
item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () =>
{
if (toggled.IsTag(BackTag)) toggled.ModifyTag(BackTag, t => t.Disable()); else toggled.EnableTag(BackTag);
Refresh(toggled);
});
item.getItemSpriteModifierForItemFunc = Overlays;
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)
{
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);
return item;
}
public static Unit ToUnit(GameItem item)
{
string model = item.identifier.Substring(GpuPrefix.Length);
@@ -85,6 +104,7 @@ namespace Decentralized
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());
return u;
}
@@ -94,6 +114,7 @@ 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()));
Refresh(item);
}
@@ -107,12 +128,48 @@ 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.SetSpriteAndShapeFromMod(ModInfo.Id, (label.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd") + (item.IsTag(BackTag) ? "_b" : "_f"));
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.unitValue = CardValue.True(u);
}
private 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>();
if (item.IsTag(DeadTag)) return list;
var u = ToUnit(item);
var keys = new List<string>();
if (!item.IsTag(BackTag))
{
if (u.Look.Fan > 0) keys.Add("gpu_ov_fan" + u.Look.Fan);
if (u.Look.Fingers > 0) keys.Add("gpu_ov_fin" + u.Look.Fingers);
if (u.Look.Bracket > 0) keys.Add("gpu_ov_brk" + u.Look.Bracket);
}
else
{
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = u.Look.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model;
keys.Add((u.Look.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (u.Look.Sticker > 0) keys.Add("gpu_ov_stk" + u.Look.Sticker);
}
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 +183,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),
+2
View File
@@ -15,6 +15,7 @@ namespace Decentralized
Unlock.Register();
Night.Subscribe();
DealerStock.Subscribe();
UsedDealerClient.Subscribe();
ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame;
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
@@ -25,6 +26,7 @@ namespace Decentralized
Items.Unsubscribe();
Night.Unsubscribe();
DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe();
ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame;
}
@@ -58,3 +58,12 @@ 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."
dealer.markup,"Used-goods markup"
1 # Probably Decentralized localization table: English (en)
58 news.open.desc
59 news.halving.title
60 news.halving.desc
61 appraisal.gpu
62 dealer.name
63 dealer.line1.1
64 dealer.line1.2
65 dealer.line1.3
66 dealer.line2.1
67 dealer.line2.2
68 dealer.line2.3
69 dealer.markup
@@ -57,3 +57,12 @@ 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,"要的话价钱好商量。"
dealer.markup,"二手加价"
1 # Probably Decentralized 本地化表:简体中文(zh)
57 news.open.desc
58 news.halving.title
59 news.halving.desc
60 appraisal.gpu
61 dealer.name
62 dealer.line1.1
63 dealer.line1.2
64 dealer.line1.3
65 dealer.line2.1
66 dealer.line2.2
67 dealer.line2.3
68 dealer.markup
Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

+99
View File
@@ -0,0 +1,99 @@
using System.Collections.Generic;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 二手显卡贩子:偶尔上门,把几张卡摆上柜台,玩家按外观自己判断(翻面看背面)。
/// 老实人按真实价值卖,中间商加价,骗子夹带假卡并按标称型号报价;价格是柜台上的临时加价,买下之后卡只值真实价值。
/// 另外把显卡登记为鉴定公会的鉴定物品,正确价值与实际售价是同一个数。
/// </summary>
public static class UsedDealerClient
{
public const string Appraisal = "pd_gpu_appraisal";
private const double VisitChance = 0.05;
private static int lastVisitDay = -1;
public static void Subscribe()
{
ModHook.OnGenerateCustomerNormal += OnGenerate;
ModHelper.AddAppraisalItem(ModInfo.Id, Appraisal, Items.GpuId("RZX50"), 2, CreateForAppraisal, u => (int)CardValue.True(Items.ToUnit(u)),
Items.IsGpu, null, () => L.T("appraisal.gpu"));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used dealer visit (test)", () => PlayerStore.Instance.storeClientManager?.AddClient(Create(DealerKind.Scammer)));
}
public static void Unsubscribe()
{
ModHook.OnGenerateCustomerNormal -= OnGenerate;
ModHelper.RemoveAppraisalItems(ModInfo.Id);
}
private static void OnGenerate(StoreClientManager manager)
{
if (Session.Data == null || !Unlock.IsUnlocked) return;
Log.Guard("二手贩子", () =>
{
int day = StoreStation.GetDayCounter();
if (day < 3 || day == lastVisitDay || !Session.Data.Rng.Chance(VisitChance)) return;
lastVisitDay = day;
manager.AddClient(Create(UsedDealers.Kind(day, Session.Data.Rng)));
});
}
/// <summary>鉴定公会送来的卡:一成是假卡;默认估价是按标称型号算的价。</summary>
private static GameItem CreateForAppraisal()
{
var rng = Session.Data?.Rng ?? new Rng(1);
int day = StoreStation.GetDayCounter();
var cards = UsedDealers.Cards(DealerKind.Middleman, day, rng);
var c = cards[0];
var item = Items.NewCard(c.Model, c.Health, c.FakeOf, rng);
AppraisalHelper.InitAppraisalItem(item, (int)CardValue.Of(c.Model, c.Health));
return item;
}
private static StoreClient Create(DealerKind kind)
{
int day = StoreStation.GetDayCounter();
var rng = Session.Data.Rng;
var cards = UsedDealers.Cards(kind, day, rng);
var client = new StoreClient
{
identifier = "pd_used_dealer",
displayName = L.T("dealer.name"),
spriteName = SpriteDict.GetRandomScavSpriteName(),
clientIntent = StoreClient.ClientIntent.SELL,
};
StoreClientFactionSetup.InitScav(client);
client.mainDialogue.SetText(client.displayName, L.T("dealer.line1." + rng.Range(1, 3))).NextDialogue().SetText(client.displayName, L.T("dealer.line2." + rng.Range(1, 3)))
.SetEndAction(() => Log.Guard("二手贩子上货", () =>
{
NegociationUIManager.Instance.OpenUI();
var placed = new List<string>();
foreach (var c in cards)
{
var item = Items.NewCard(c.Model, c.Health, c.FakeOf, rng);
if (c.MarkupPct != 0) item.AddItemFeature(Markup(c.MarkupPct));
PlayerStore.Instance.AddDirectSellingItemToTable(item);
placed.Add($"{c.Model}{(c.FakeOf != null ? "←" + c.FakeOf : "")} {c.Health:0}% {c.MarkupPct:+0;-0}%");
}
Log.Info($"二手贩子({kind}):{string.Join(",", placed)}");
}));
client.AddBasicDialog();
client.CompleteClientCreation();
return client;
}
/// <summary>柜台上的临时加价:离开贩子之后自动去掉(游戏按客户临时特征处理)。</summary>
private static ItemFeature Markup(int pct)
{
var f = new ItemFeature(ItemFeature.FeatureType.ClientTemporary) { identifier = "pd_used_markup", category = "markup", valueStage = ItemFeature.ValueStage.Market };
string text = L.T("dealer.markup");
f.SetPublicDisplay(text);
f.SetActualDisplay(text);
f.isFeatureMatch = true;
f.SetPreExposedValueMod(pct);
return f;
}
}
}
+24 -1
View File
@@ -170,7 +170,7 @@ internal static class Program
Check(RigSlots.Next(4) == ("rare_electronic", 2) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 4 && prog == 1, "第 5 槽:2 个稀有元件,先投 1 个进度 1/2");
Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 5, "投满解锁第 5 槽");
Check(RigSlots.Next(5) == null && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0, "满 5 槽后不再收");
Check(RigSlots.Shape(2) == "0000" + "0000" + new string('1', 12) && RigSlots.Shape(5) == new string('0', 20) && RigSlots.Shape(3).Length == 20, "形状:宽 2、共 10 行,前 2 × 槽数行可放");
Check(RigSlots.Shape(2) == "000000" + "000000" + new string('1', 18) && RigSlots.Shape(5) == new string('0', 30) && RigSlots.Shape(3).Length == 30, "形状:宽 3、共 10 行,前 2 × 槽数行可放");
});
Test("模块:折算系数 4,算力 +20%、省电 25%、损耗 −30% 封顶;性能加成提高损耗与暴毙", () =>
@@ -335,6 +335,29 @@ internal static class Program
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
});
Test("二手贩子:老实人没有假卡;骗子按标称价九折报价;假卡芯片至少便宜一半;买下后只值真实价值", () =>
{
var rng = new Rng(7);
int fakes = 0, cards = 0;
for (int i = 0; i < 300; i++)
foreach (var c in UsedDealers.Cards(DealerKind.Honest, 40, rng))
Check(c.FakeOf == null && c.MarkupPct == 0, "老实人的卡是真卡且按真实价值");
for (int i = 0; i < 300; i++)
foreach (var c in UsedDealers.Cards(DealerKind.Scammer, 40, rng))
{
cards++;
if (c.FakeOf == null) continue;
fakes++;
var u = new Unit { Model = c.Model, FakeOf = c.FakeOf, Health = c.Health };
Check(Hardware.Specs[c.FakeOf].Price * 2 <= Hardware.Specs[c.Model].Price, "假卡的芯片至少便宜一半");
long shown = (long)Math.Round(CardValue.True(u) * (1 + c.MarkupPct / 100.0));
Check(Math.Abs(shown - CardValue.Of(c.Model, c.Health) * UsedDealers.ScamDiscount) <= CardValue.True(u) * 0.01 + 1, "柜台价约等于标称价九折");
Check(c.MarkupPct > 0, "假卡的柜台价高于真实价值");
}
Check(fakes > cards * 0.2 && fakes < cards * 0.5, $"骗子的假卡占两成多(入门卡便宜到没法冒充):{fakes}/{cards}");
Check(UsedDealers.Pool(5).All(m => Dealer.Basic.Contains(m)) && UsedDealers.Pool(30).Contains("RZX90"), "型号按天数放开");
});
Test("杰克逊博士:没有矿架必上一个;基础卡每次两张;进阶第 10 天起、旗舰第 30 天起;特供货第 20 天起必有旗舰", () =>
{
var rng = new Rng(11);