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

+71 -26
View File
@@ -22,6 +22,36 @@ namespace Decentralized.Domain
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;
if (Bracket != 2) 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}";
@@ -37,7 +67,15 @@ namespace Decentralized.Domain
private static readonly double[] FingerValue = { 1.00, 0.96, 0.88 }, FingerDeath = { 1.00, 1.10, 1.30 };
/// <summary>外观对价值的折扣:风扇积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失(开过封)−6%。</summary>
public double ValueFactor => FanValue[Fan] * FingerValue[Fingers] * (Bracket == 1 ? 0.98 : 1.0) * (Sticker == 2 ? 0.94 : 1.0);
public double ValueFactor => ValueFactorOf(AllDims);
/// <summary>外观折扣的维度位:1 风扇、2 金手指、4 挡板划痕、8 封条缺失。</summary>
public const int DimFan = 1, DimFingers = 2, DimScratch = 4, DimSeal = 8, AllDims = 15;
/// <summary>只计入 mask 里的维度的折扣:贩子报价时先装作这些问题都没有,玩家指出一项才扣一项。</summary>
public double ValueFactorOf(int mask) =>
((mask & DimFan) != 0 ? FanValue[Fan] : 1.0) * ((mask & DimFingers) != 0 ? FingerValue[Fingers] : 1.0)
* ((mask & DimScratch) != 0 && Bracket == 1 ? 0.98 : 1.0) * ((mask & DimSeal) != 0 && Sticker == 2 ? 0.94 : 1.0);
/// <summary>每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。</summary>
public double WearFactor => FanWear[Fan];
@@ -54,36 +92,29 @@ 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]);
}
for (int k = 0; k < signs; k++)
{
switch (order[k])
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--)
{
case 0: look.Bracket = 2; break;
case 1: look.Print = rng.Chance(0.6) ? 1 : 2; break;
default: look.Sticker = 1; break;
int j = rng.Range(0, i);
(order[i], order[j]) = (order[j], order[i]);
}
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>
@@ -101,7 +132,21 @@ namespace Decentralized.Domain
}
/// <summary>真实价值(芯片型号 + 健康度 + 外观折扣)。</summary>
public static long True(Unit u) => Math.Max(1, (long)Math.Round(Of(u.FakeOf ?? u.Model, u.Health) * u.Look.ValueFactor));
public static long True(Unit u) => Math.Max(1, (long)Math.Round(Of(u.FakeOf ?? u.Model, u.Health) * u.Appearance.ValueFactor));
public const double ScamDiscount = 0.90;
/// <summary>
/// 贩子的报价:装作成色没问题(外观折扣只计玩家已指出的维度),在此之上加价 markupPct%。
/// 没拆穿的假卡按标称型号的价再打九折;拆穿之后按真实芯片算,和真卡一样。
/// </summary>
public static long Quote(Unit u, int markupPct, int pointedDims, bool exposed)
{
bool fakeHidden = u.FakeOf != null && !exposed;
double v = Of(fakeHidden ? u.Model : (u.FakeOf ?? u.Model), u.Health) * u.Appearance.ValueFactorOf(pointedDims);
v *= fakeHidden ? ScamDiscount : 1 + markupPct / 100.0;
return Math.Max(1, (long)Math.Round(v));
}
/// <summary>标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。</summary>
public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth);
+5 -3
View File
@@ -86,6 +86,8 @@ namespace Decentralized.Domain
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];
@@ -94,7 +96,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;
@@ -103,14 +105,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 * Look.DeathFactor;
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 * Look.WearFactor);
Health = Math.Max(0, Health - Label.WearFactor * b.Wear * Appearance.WearFactor);
if (rng.Chance(DeathChance(b)))
{
Dead = true;
+7 -9
View File
@@ -7,18 +7,20 @@ namespace Decentralized.Domain
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
public enum DealerKind { Honest, Middleman, Scammer }
/// <summary>贩子带来的一张卡:<see cref="MarkupPct"/> 是柜台上相对真实价值的加价百分比(骗子卖假卡时是正的,按标称型号报价再打九折)。</summary>
/// <summary>贩子带来的一张卡。老实人不报价(按真实价值卖);其他贩子 <see cref="Quoted"/> 为真,按 <see cref="CardValue.Quote"/> 报价,
/// <see cref="MarkupPct"/> 是加价百分比,玩家指出外观问题或假货迹象就按维度扣回去。</summary>
public sealed class UsedCard
{
public string Model;
public string FakeOf;
public double Health;
public int MarkupPct;
public bool Quoted;
}
public static class UsedDealers
{
public const double MiddlemanMarkup = 0.08, ScamDiscount = 0.90;
public const double MiddlemanMarkup = 0.08, ScamDiscount = CardValue.ScamDiscount;
public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45;
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
@@ -49,13 +51,9 @@ namespace Decentralized.Domain
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);
if (cheaper.Length > 0 && rng.Chance(share)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
card.Quoted = kind != DealerKind.Honest;
card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0;
cards.Add(card);
}
return cards;
+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;
}
});
}
}
+33 -8
View File
@@ -137,8 +137,8 @@ internal static class Program
for (int i = 0; i < n; i++)
{
var real = CardLook.Generate(rng, 100, false);
var oldReal = CardLook.Generate(rng, 30, false);
var fake = CardLook.Generate(rng, 60, true);
var oldReal = CardLook.Generate(rng, 20 + (i % 40), false);
var fake = CardLook.Generate(rng, 20 + (i % 80), true);
if (real.HasFakeSign || oldReal.HasFakeSign) realSign++;
if (!fake.HasFakeSign) fakeNoSign++;
fakeSigns += (fake.Bracket == 2 ? 1 : 0) + (fake.Print != 0 ? 1 : 0) + (fake.Sticker == 1 ? 1 : 0);
@@ -340,27 +340,52 @@ internal static class Program
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
});
Test("二手贩子:老实人没有假卡;骗子按标称价九折报价;假卡芯片至少便宜一半;买下后只值真实价值", () =>
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, "老实人的卡是真卡且按真实价值");
Check(c.FakeOf == null && !c.Quoted, "老实人的卡是真卡且不报价");
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"), "型号按天数放开");
// 报价:假 RZX 70(芯片 RZX 50)、健康 60、风扇缺角、金手指氧化
var u = new Unit { Model = "RZX70", FakeOf = "RZX50", Health = 60, Look = new CardLook { Fan = 2, Fingers = 2 } };
long label = CardValue.Of("RZX70", 60), real = CardValue.True(u);
Check(CardValue.Quote(u, 0, 0, false) == (long)Math.Round(label * 0.9), "没拆穿的假卡:标称型号的价打九折,外观问题还没指出就不扣");
Check(CardValue.Quote(u, 0, CardLook.DimFan, false) == (long)Math.Round(label * 0.88 * 0.9), "指出风扇缺角:扣 12%");
Check(CardValue.Quote(u, 0, CardLook.AllDims, true) == real, "外观问题都指出并拆穿假货:报价等于真实价值");
var honest = new Unit { Model = "RZX70", Health = 60, Look = new CardLook { Fan = 1 } };
Check(CardValue.Quote(honest, 8, 0, false) == (long)Math.Round(CardValue.Of("RZX70", 60) * 1.08), "中间商:加价 8%,外观问题没指出就不扣");
Check(CardValue.Quote(honest, 8, CardLook.AllDims, false) == (long)Math.Round(CardValue.Of("RZX70", 60) * 0.96 * 1.08), "指出积灰:扣 4%");
});
Test("外观随损耗重算:越旧外观越差;种子固定;算力受风扇与金手指影响", () =>
{
var rng = new Rng(5);
int worse = 0, total = 400;
for (int i = 0; i < total; i++)
{
var look = CardLook.Generate(rng, 100, false);
var a = look.Appearance(100); var b = look.Appearance(20);
Check(b.Fan >= a.Fan && b.Fingers >= a.Fingers && b.Bracket >= a.Bracket, "健康度下降时外观只会变差");
if (b.Fan + b.Fingers + b.Bracket > a.Fan + a.Fingers + a.Bracket) worse++;
Check(look.Appearance(60).Encode() == look.Appearance(60).Encode(), "同一健康度外观固定");
}
Check(worse > total * 0.9, $"健康度从 100 掉到 20,九成以上的卡外观变差:{worse}/{total}");
var fake = CardLook.Generate(rng, 80, true);
Check(fake.HasFakeSign && fake.Appearance(10).HasFakeSign, "假卡的破绽不随健康度变化");
var clean = new Unit { Model = "RZX70", Health = 100 };
var bad = new Unit { Model = "RZX70", Health = 100, Look = new CardLook { Fan = 2, Fingers = 2 } };
Check(Near(bad.Hash(ModuleBonus.None) / clean.Hash(ModuleBonus.None), 0.92 * 0.94, 1e-9), "风扇缺角 + 金手指氧化:算力 ×0.92 ×0.94");
});
Test("杰克逊博士:没有矿架必上一个;基础卡每次两张;进阶第 10 天起、旗舰第 30 天起;特供货第 20 天起必有旗舰", () =>