Cards: appearance follows wear, hash depends on fan and contacts, seller confrontation

Fan, contact and scratch levels are recomputed from health and a per-card
seed, so a card looks older as it wears. Fan and contacts scale hashrate.
Each inspection-tray detail now maps to exactly one dimension value. Quoting
dealers price the card as if its condition were fine; the player points out
fan, contacts, bracket, print or sticker in the tray to take the matching
discount or expose a fake (a wrong accusation costs reputation like the
vanilla confrontation). Unowned cards show a coarse grade and rated specs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 13:37:48 +08:00
1 parent 68265095a8
commit f93dbc6eec
9 files changed
+279 -76

No files matched your search

+70 -8
View File
@@ -15,6 +15,10 @@ namespace Decentralized
private static GpuInspectView instance;
private RectTransform root, cardRoot;
private Button flip;
private readonly Button[] point = new Button[5];
private RectTransform pointRow;
private static readonly int[] PointBits = { Items.PointFan, Items.PointFingers, Items.PointBracket, Items.PointPrint, Items.PointSticker };
private static readonly string[] PointKeys = { "point.fan", "point.fingers", "point.bracket", "point.print", "point.sticker" };
private TextMeshProUGUI flipText;
private GameItem shown;
private string shownSig;
@@ -47,6 +51,7 @@ namespace Decentralized
{
if (root == null) Build(m);
if (!root.gameObject.activeSelf) root.gameObject.SetActive(true);
UpdatePointButtons(item);
var keys = new List<string>();
string sig = Items.LookKeys(item, keys) + "|" + string.Join(",", keys);
if (item != shown || sig != shownSig)
@@ -64,24 +69,24 @@ namespace Decentralized
var o = new List<string>();
var u = Items.ToUnit(item);
if (u.Dead) return o;
var l = u.Look;
var l = u.Appearance;
string P = "gpu_big_ov_";
// 每个维度的取值对应一种细节,一一对应,不靠组合触发
if (!Items.IsBack(item))
{
if (l.Fan == 1) o.Add(P + (l.Fingers > 0 ? "fan_dust2" : "fan_dust1"));
if (l.Fan == 2) { o.Add(P + "fan_chip"); if (l.Fingers == 2) o.Add(P + "fan_oil"); }
if (l.Fan == 1) o.Add(P + "fan_dust2");
if (l.Fan == 2) { o.Add(P + "fan_dust1"); o.Add(P + "fan_chip"); }
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"); if (l.Fan > 0) o.Add(P + "shroud_chip"); }
if (l.Bracket == 1) o.Add(P + "brk_scratch");
if (l.Bracket == 2) o.Add(P + "brk_port");
}
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.Print == 2) o.Add(P + "screw_opened"); // 重印过,说明拆开过
if (l.Sticker == 1) o.Add(P + "sticker_hand");
if (l.Sticker == 2) o.Add(P + (l.Print != 0 ? "seal_reglued" : "seal_missing"));
if (l.Sticker == 2) o.Add(P + "seal_missing");
}
return o;
}
@@ -94,14 +99,37 @@ namespace Decentralized
root.localPosition = Vector3.zero;
cardRoot = new GameObject("Card", typeof(RectTransform)).GetComponent<RectTransform>();
cardRoot.SetParent(root, false);
cardRoot.localPosition = new Vector3(0, 40, 0);
cardRoot.localPosition = new Vector3(0, 0, 0);
// 翻面按钮:复制鉴定面板里"发送"按钮的外观
var template = m.appraisalPanel.transform.Find("buttonSend");
if (template != null)
{
pointRow = new GameObject("PointRow", typeof(RectTransform)).GetComponent<RectTransform>();
pointRow.SetParent(root, false);
pointRow.localPosition = new Vector3(0, 312, 0);
for (int i = 0; i < point.Length; i++)
{
int bit = PointBits[i];
var pb = Instantiate(template.gameObject, pointRow, false);
pb.name = "Point" + i;
var prt = (RectTransform)pb.transform;
prt.sizeDelta = new Vector2(140, 40);
prt.localPosition = new Vector3((i - 2) * 148, 0, 0);
point[i] = pb.GetComponent<Button>();
point[i].onClick = new Button.ButtonClickedEvent();
point[i].onClick.AddListener(() => Log.Guard("指出问题", () => OnPoint(bit)));
var t = pb.GetComponentInChildren<TextMeshProUGUI>();
t.enableAutoSizing = true; t.fontSizeMin = 12; t.fontSizeMax = 26;
foreach (var mb in pb.GetComponentsInChildren<MonoBehaviour>(true))
{
string n = mb.GetType().Name;
if (n.Contains("Local") || n.Contains("TMPLocalizer")) Destroy(mb);
}
t.text = L.T(PointKeys[i]);
}
var go = Instantiate(template.gameObject, root, false);
go.name = "Flip";
go.transform.localPosition = new Vector3(0, -300, 0);
go.transform.localPosition = new Vector3(0, -290, 0);
flip = go.GetComponent<Button>();
flip.onClick = new Button.ButtonClickedEvent();
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
@@ -116,6 +144,40 @@ namespace Decentralized
}
}
/// <summary>对峙:卖家的报价装作成色没问题,玩家逐项指出问题;只在还没买下、卖家在场、卖家报价的卡上出现。</summary>
private void UpdatePointButtons(GameItem item)
{
if (pointRow == null) return;
var client = PlayerStore.Instance.currentClientInstance?.GetClientBlueprint();
bool show = client != null && client.clientIntent == StoreClient.ClientIntent.SELL && item.IsTag(Items.QuotedTag)
&& !GeneralHelper.IsItemOwned(item) && !InspectionUIManager.IsConfrontLocked();
pointRow.gameObject.SetActive(show);
if (!show) return;
for (int i = 0; i < point.Length; i++)
if (point[i] != null) point[i].gameObject.SetActive((Items.Pointed(item) & PointBits[i]) == 0);
}
private void OnPoint(int bit)
{
if (shown == null) return;
var item = shown;
int kind = Items.Point(item, bit);
var d = new Dialogue();
if (kind == 0)
{
d.SetText("CLIENT", L.T("point.none." + Random.Range(1, 3)));
InspectionUIManager.Instance.OnConfrontFailed();
}
else
{
d.SetText("CLIENT", L.T(kind == 2 ? "point.fake." + Random.Range(1, 3) : "point.worn." + Random.Range(1, 3)));
InspectionUIManager.Instance.OnConfrontSuccess();
}
Log.Info($"对峙:指出 {bit},结果 {kind},报价 {item.unitValue}");
DialogUIManager.Instance.StartDialogue(d);
InspectionUIManager.Instance.CloseUI();
}
private void Redraw(GameItem item)
{
foreach (Transform c in cardRoot) Destroy(c.gameObject);
+56 -11
View File
@@ -21,7 +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", QuoteTag = "PD_QUOTE";
public const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", QuotedTag = "PD_QUOTED", MarkupTag = "PD_MARKUP", PointedTag = "PD_POINTED", ExposedTag = "PD_EXPOSED";
public static Action OpenTerminal;
@@ -95,13 +95,51 @@ namespace Decentralized
return item;
}
/// <summary>贩子的柜台报价:只在物品还不属于玩家时按报价算,归玩家之后立刻只值真实价值。</summary>
public static void SetQuote(GameItem item, long quote)
/// <summary>贩子的柜台报价:在物品还不属于玩家时,按 <see cref="CardValue.Quote"/> 算(装作成色没问题,玩家指出一项才扣一项);
/// 归玩家之后立刻只值真实价值。老实人的卡不报价。</summary>
public static void SetQuote(GameItem item, int markupPct)
{
item.ModifyTag(QuoteTag, t => t.Enable().SetLong(quote));
item.EnableTag(QuotedTag);
item.ModifyTag(MarkupTag, t => t.Enable().SetInt(markupPct));
Refresh(item);
}
public static int Pointed(GameItem item) => item.IsTag(PointedTag) ? item.GetTagReadonly(PointedTag).GetInt() : 0;
/// <summary>玩家对峙时能指出的五项:风扇、金手指、挡板、丝印、贴纸。</summary>
public const int PointFan = 1, PointFingers = 2, PointBracket = 4, PointPrint = 8, PointSticker = 16;
/// <summary>指出过的项换算成报价里扣价的维度位(丝印、接口数、手写序列号不扣价,只拆穿假货)。</summary>
private static int PricedDims(int pointed, CardLook l) =>
((pointed & PointFan) != 0 ? CardLook.DimFan : 0) | ((pointed & PointFingers) != 0 ? CardLook.DimFingers : 0)
| ((pointed & PointBracket) != 0 ? CardLook.DimScratch : 0) | ((pointed & PointSticker) != 0 ? CardLook.DimSeal : 0);
/// <summary>这一项有没有问题;返回 0 没问题,1 老化类问题(扣价),2 假货迹象(拆穿假货)。</summary>
public static int Defect(GameItem item, int point)
{
var u = ToUnit(item);
var l = u.Appearance;
switch (point)
{
case PointFan: return l.Fan > 0 ? 1 : 0;
case PointFingers: return l.Fingers > 0 ? 1 : 0;
case PointBracket: return l.Bracket == 2 ? 2 : l.Bracket == 1 ? 1 : 0;
case PointPrint: return l.Print != 0 ? 2 : 0;
case PointSticker: return l.Sticker == 1 ? 2 : l.Sticker == 2 ? 1 : 0;
}
return 0;
}
/// <summary>玩家指出一项:记下来,有问题就按维度扣价或拆穿假货;返回这一项的情况(0 没问题,1 扣价,2 拆穿)。</summary>
public static int Point(GameItem item, int point)
{
int kind = Defect(item, point);
item.ModifyTag(PointedTag, t => t.Enable().SetInt(Pointed(item) | point));
if (kind == 2) item.EnableTag(ExposedTag);
Refresh(item);
return kind;
}
/// <summary>说明:算力和耗电按标称写;成色在归玩家之前只给粗略等级(要看外观自己判断),归玩家之后给精确健康度。</summary>
private static string Desc(GameItem item, Unit u)
{
@@ -117,7 +155,10 @@ namespace Decentralized
if (item.IsTag(DeadTag)) return;
string desc = Desc(item, ToUnit(item));
if (item.shortDescription != desc) item.shortDescription = desc;
long value = item.IsTag(QuoteTag) && !GeneralHelper.IsItemOwned(item) ? item.GetTagReadonly(QuoteTag).GetLong() : CardValue.True(ToUnit(item));
var u = ToUnit(item);
long value = item.IsTag(QuotedTag) && !GeneralHelper.IsItemOwned(item)
? CardValue.Quote(u, item.IsTag(MarkupTag) ? item.GetTagReadonly(MarkupTag).GetInt() : 0, PricedDims(Pointed(item), u.Appearance), item.IsTag(ExposedTag))
: CardValue.True(u);
if (item.unitValue != value) item.unitValue = value;
}
@@ -131,6 +172,8 @@ namespace Decentralized
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;
}
@@ -141,6 +184,7 @@ namespace Decentralized
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);
}
@@ -176,21 +220,22 @@ namespace Decentralized
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 (u.Look.Fan > 0) overlays.Add("gpu_ov_fan" + u.Look.Fan);
if (u.Look.Fingers > 0) overlays.Add("gpu_ov_fin" + u.Look.Fingers);
if (u.Look.Bracket > 0) overlays.Add("gpu_ov_brk" + u.Look.Bracket);
if (l.Fan > 0) overlays.Add("gpu_ov_fan" + l.Fan);
if (l.Fingers > 0) overlays.Add("gpu_ov_fin" + l.Fingers);
if (l.Bracket > 0) overlays.Add("gpu_ov_brk" + l.Bracket);
}
else
{
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = u.Look.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model;
overlays.Add((u.Look.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (u.Look.Sticker > 0) overlays.Add("gpu_ov_stk" + u.Look.Sticker);
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 > 0) overlays.Add("gpu_ov_stk" + l.Sticker);
}
return brand + (back ? "_b" : "_f");
}
+12 -1
View File
@@ -14,7 +14,7 @@ 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
@@ -72,3 +72,14 @@ item.gpu.grade.1,"like new"
item.gpu.grade.2,"decent"
item.gpu.grade.3,"clearly worn"
item.gpu.grade.4,"badly worn"
point.fan,"Point out fan"
point.fingers,"Point out contacts"
point.bracket,"Point out bracket"
point.print,"Point out print"
point.sticker,"Point out sticker"
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."
1 # Probably Decentralized localization table: English (en)
14 mat.rare_electronic
15 rig.label.cards
16 item.gpu.name
17 item.gpu.desc
18 item.gpu.burnt.name
19 item.gpu.burnt.desc
20 term.title
72 item.gpu.grade.2
73 item.gpu.grade.3
74 item.gpu.grade.4
75 point.fan
76 point.fingers
77 point.bracket
78 point.print
79 point.sticker
80 point.none.1
81 point.none.2
82 point.worn.1
83 point.worn.2
84 point.fake.1
85 point.fake.2
+12 -1
View File
@@ -13,7 +13,7 @@ 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,交易终端
@@ -71,3 +71,14 @@ item.gpu.grade.1,"接近全新"
item.gpu.grade.2,"成色尚可"
item.gpu.grade.3,"明显旧了"
item.gpu.grade.4,"磨损严重"
point.fan,"指出风扇"
point.fingers,"指出金手指"
point.bracket,"指出挡板"
point.print,"指出丝印"
point.sticker,"指出贴纸"
point.none.1,"没毛病,你看错了吧。"
point.none.2,"这儿哪有问题?不买别瞎说。"
point.worn.1,"……行,这点毛病是有,算你便宜点。"
point.worn.2,"眼睛挺毒。好吧,这个价。"
point.fake.1,"……被你看出来了。是翻新的货,按真的价给你。"
point.fake.2,"啧,不是原装的。那就按芯片本来的价吧。"
1 # Probably Decentralized 本地化表:简体中文(zh)
13 mat.rare_electronic
14 rig.label.cards
15 item.gpu.name
16 item.gpu.desc
17 item.gpu.burnt.name
18 item.gpu.burnt.desc
19 term.title
71 item.gpu.grade.2
72 item.gpu.grade.3
73 item.gpu.grade.4
74 point.fan
75 point.fingers
76 point.bracket
77 point.print
78 point.sticker
79 point.none.1
80 point.none.2
81 point.worn.1
82 point.worn.2
83 point.fake.1
84 point.fake.2
+13 -9
View File
@@ -36,7 +36,9 @@ namespace Decentralized
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used dealer visit (test)", () => Log.Guard("二手贩子(测试)", () =>
{
var m = PlayerStore.Instance.storeClientManager;
m.AddNextClient(Create(DealerKind.Scammer, 40));
var c = Create(DealerKind.Scammer, 40);
PlayerStore.Instance.currentClientInstance = StoreClientInstance.CreateClientInstance(c); // 测试:直接换成这位贩子
Place(DealerKind.Scammer, UsedDealers.Cards(DealerKind.Scammer, 40, Session.Data.Rng), Session.Data.Rng);
Log.Info($"二手贩子(测试)已排入,队列 {m.clientStack.Count} 人");
}));
}
@@ -85,22 +87,24 @@ namespace Decentralized
};
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("二手贩子上货", () =>
.SetEndAction(() => Place(kind, cards, rng));
client.AddBasicDialog();
client.CompleteClientCreation();
return client;
}
private static void Place(DealerKind kind, List<UsedCard> cards, Rng rng) => 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) Items.SetQuote(item, (long)Math.Round(CardValue.True(Items.ToUnit(item)) * (1 + c.MarkupPct / 100.0)));
if (c.Quoted) Items.SetQuote(item, c.MarkupPct);
PlayerStore.Instance.AddDirectSellingItemToTable(item);
placed.Add($"{c.Model}{(c.FakeOf != null ? "←" + c.FakeOf : "")} {c.Health:0}% {c.MarkupPct:+0;-0}% 柜台价 {item.unitValue}(真实 {CardValue.True(Items.ToUnit(item))})");
placed.Add($"{c.Model}{(c.FakeOf != null ? "←" + c.FakeOf : "")} {c.Health:0}% {(c.Quoted ? c.MarkupPct + "%" : "不报价")} 柜台价 {item.unitValue}(真实 {CardValue.True(Items.ToUnit(item))})");
}
Log.Info($"二手贩子({kind}):{string.Join(",", placed)}");
}));
client.AddBasicDialog();
client.CompleteClientCreation();
return client;
}
});
}
}