diff --git a/src/Decentralized.Domain/CardLook.cs b/src/Decentralized.Domain/CardLook.cs
index e4efa64..d703c1b 100644
--- a/src/Decentralized.Domain/CardLook.cs
+++ b/src/Decentralized.Domain/CardLook.cs
@@ -22,6 +22,36 @@ namespace Decentralized.Domain
public const int Variants = 3;
+ /// 每张卡自己的随机种子:老化类维度(风扇、金手指、挡板划痕)由它和当前健康度决定,每晚损耗后重新算,
+ /// 所以越旧的卡看着越旧。为 0 时表示外观是写死的(测试用)。
+ 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));
+ }
+
+ /// 按当前健康度重新算出的外观:磨损度 = 1 − 健康度 / 100,再加上这张卡自己的偏差(±0.15)。
+ /// 风扇磨损 ≥ 0.25 积灰、≥ 0.55 缺角;金手指 ≥ 0.30 发暗、≥ 0.60 氧化;挡板磨损 ≥ 0.45 出划痕(接口数不对是假卡的固定特征,不变)。
+ 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 };
+
+ /// 算力倍数:风扇积灰 ×0.97、缺角 ×0.92(降频);金手指发暗 ×0.98、氧化 ×0.94(接触不良掉速)。
+ public double HashFactor => FanHash[Fan] * FingerHash[Fingers];
+
/// 五位数字,存在物品标签上:风扇、金手指、挡板、丝印、贴纸。
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 };
/// 外观对价值的折扣:风扇积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失(开过封)−6%。
- public double ValueFactor => FanValue[Fan] * FingerValue[Fingers] * (Bracket == 1 ? 0.98 : 1.0) * (Sticker == 2 ? 0.94 : 1.0);
+ public double ValueFactor => ValueFactorOf(AllDims);
+
+ /// 外观折扣的维度位:1 风扇、2 金手指、4 挡板划痕、8 封条缺失。
+ public const int DimFan = 1, DimFingers = 2, DimScratch = 4, DimSeal = 8, AllDims = 15;
+
+ /// 只计入 mask 里的维度的折扣:贩子报价时先装作这些问题都没有,玩家指出一项才扣一项。
+ 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);
/// 每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。
public double WearFactor => FanWear[Fan];
@@ -54,36 +92,29 @@ namespace Decentralized.Domain
///
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);
}
- /// p1 的概率取 1,再以 p2 的概率升到 2。
- private static int Pick(Rng rng, double p1, double p2) => rng.Chance(p1) ? (rng.Chance(p2 / Math.Max(p1, 1e-9)) ? 2 : 1) : 0;
}
///
@@ -101,7 +132,21 @@ namespace Decentralized.Domain
}
/// 真实价值(芯片型号 + 健康度 + 外观折扣)。
- 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;
+
+ ///
+ /// 贩子的报价:装作成色没问题(外观折扣只计玩家已指出的维度),在此之上加价 markupPct%。
+ /// 没拆穿的假卡按标称型号的价再打九折;拆穿之后按真实芯片算,和真卡一样。
+ ///
+ 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));
+ }
/// 标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。
public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth);
diff --git a/src/Decentralized.Domain/Mining.cs b/src/Decentralized.Domain/Mining.cs
index 0c2c8d2..9b82b86 100644
--- a/src/Decentralized.Domain/Mining.cs
+++ b/src/Decentralized.Domain/Mining.cs
@@ -86,6 +86,8 @@ namespace Decentralized.Domain
public bool Refurbished;
/// 外观;新卡(正规渠道)没有记录,按全新画。
public CardLook Look;
+ /// 按当前健康度算出的外观。
+ 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;
}
/// 一夜的损耗与暴毙判定;返回是否在今晚暴毙。
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;
diff --git a/src/Decentralized.Domain/UsedDealer.cs b/src/Decentralized.Domain/UsedDealer.cs
index c015c5b..291282e 100644
--- a/src/Decentralized.Domain/UsedDealer.cs
+++ b/src/Decentralized.Domain/UsedDealer.cs
@@ -7,18 +7,20 @@ namespace Decentralized.Domain
/// 二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。
public enum DealerKind { Honest, Middleman, Scammer }
- /// 贩子带来的一张卡: 是柜台上相对真实价值的加价百分比(骗子卖假卡时是正的,按标称型号报价再打九折)。
+ /// 贩子带来的一张卡。老实人不报价(按真实价值卖);其他贩子 为真,按 报价,
+ /// 是加价百分比,玩家指出外观问题或假货迹象就按维度扣回去。
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;
/// 今天来的贩子的类型:骗子越往后越多。
@@ -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;
diff --git a/src/Decentralized/GpuInspectView.cs b/src/Decentralized/GpuInspectView.cs
index f4499cd..f30c921 100644
--- a/src/Decentralized/GpuInspectView.cs
+++ b/src/Decentralized/GpuInspectView.cs
@@ -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 sig = Items.LookKeys(item, keys) + "|" + string.Join(",", keys);
if (item != shown || sig != shownSig)
@@ -64,24 +69,24 @@ namespace Decentralized
var o = new List();
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();
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();
+ 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