diff --git a/src/Decentralized.Domain/CardLabels.cs b/src/Decentralized.Domain/CardLabels.cs new file mode 100644 index 0000000..f756eb6 --- /dev/null +++ b/src/Decentralized.Domain/CardLabels.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; + +namespace Decentralized.Domain +{ + /// 卖家的说法:每个标签一项。型号是文字,其余五项取值与 的取值一致。 + public struct Claim + { + public string Model; + public int Fan, Fingers, Bracket, Print, Sticker; + + public int Get(int row) => row == CardLabels.RowFan ? Fan : row == CardLabels.RowFingers ? Fingers : row == CardLabels.RowBracket ? Bracket : row == CardLabels.RowPrint ? Print : 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 c.Sticker = value; + return c; + } + + public string Encode() => $"{Model}:{Fan}{Fingers}{Bracket}{Print}{Sticker}"; + + public static Claim Decode(string s) + { + var p = (s ?? "").Split(':'); + if (p.Length != 2 || p[1].Length != 5 || !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) }; + } + + /// 按真实情况写出的说法(没有隐瞒)。 + 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 }; + } + } + + /// + /// 标签栏的数据:六个标签(型号、风扇、金手指、挡板、丝印、贴纸),每个标签有卖家的说法和真实情况。 + /// 一个标签被指出(拆穿)之后,说法改回真实情况;价格按「已指出的标签取真实情况,其余取说法」算, + /// 所以卖家隐瞒的问题要玩家一项项指出来才能把价格拉回真实价值。 + /// + public static class CardLabels + { + public const int RowSpec = 1, RowFan = 2, RowFingers = 4, RowBracket = 8, RowPrint = 16, RowSticker = 32, AllRows = 63; + public static readonly int[] Rows = { RowSpec, RowFan, RowFingers, RowBracket, RowPrint, RowSticker }; + + /// 各标签各取值对价格的影响(百分比,相加):风扇积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失 −6%。 + /// 接口数不对、丝印、手写序列号本身不扣价,它们是假货的证据。 + 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 RowSticker: return value == 2 ? -6 : 0; + } + return 0; + } + + /// 这一项的真实取值是不是假货的证据。 + public static bool IsFakeSign(int row, int realValue) => + (row == RowBracket && realValue == 2) || (row == RowPrint && realValue != 0) || (row == RowSticker && realValue == 1); + + /// + /// 价值:claim 为空表示没有人说过什么,一律按真实情况;exposed 里的位表示已指出的标签(取真实情况)。 + /// 型号取「说法里的型号」,型号标签被拆穿之后取真实芯片。价值 = 型号定价 ×(0.25 + 0.75 × 健康度)×(1 + 各标签折扣之和)。 + /// + 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 Rows) + { + if (r == RowSpec) continue; + 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)); + } + + /// 指出一个标签:返回 0 没问题(说法属实)、1 隐瞒了对价格不利的情况、2 拆穿了假货。 + /// 指出假货的证据(接口数、丝印、手写序列号)时,型号标签一并拆穿。 + 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 (IsFakeSign(row, real.Get(row)) && claim.Get(row) != real.Get(row)) + { + exposed |= RowSpec; + return 2; + } + if (wasExposed) return 0; + return claim.Get(row) != real.Get(row) ? 1 : 0; + } + + /// 说法里隐瞒了什么:说法比真实情况价值高的标签,以及型号不符。 + 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)) || IsFakeSign(r, real.Get(r)))) lies |= r; + } + return lies; + } + + /// 卖家怎么说:老实人如实说;中间商每项有三成概率隐瞒;骗子全部说成没问题。假卡的型号一律说成标称型号。 + public static Claim DealerClaim(Unit u, DealerKind kind, Rng rng) + { + var c = Claim.Truth(u); + c.Model = u.Model; + if (kind == DealerKind.Honest) { c.Model = u.FakeOf ?? u.Model; return c; } + double hide = kind == DealerKind.Scammer ? 1.0 : 0.7; + foreach (int r in Rows) + { + if (r == RowSpec) continue; + if (rng.Chance(hide)) c = c.With(r, 0); + } + return c; + } + } +} diff --git a/src/Decentralized.Domain/CardLook.cs b/src/Decentralized.Domain/CardLook.cs index d703c1b..2d936d5 100644 --- a/src/Decentralized.Domain/CardLook.cs +++ b/src/Decentralized.Domain/CardLook.cs @@ -62,20 +62,9 @@ namespace Decentralized.Domain return new CardLook { Fan = D(0), Fingers = D(1), Bracket = D(2), Print = D(3), Sticker = D(4) }; } - // 外观维度对应卡的真实状态:价格、损耗、暴毙概率都随之变化,所以看外观就是在估价 - private static readonly double[] FanValue = { 1.00, 0.96, 0.88 }, FanWear = { 1.00, 1.10, 1.30 }, FanDeath = { 1.00, 1.00, 1.20 }; - 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 => 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); + // 外观维度对应卡的真实状态:损耗、暴毙概率随之变化,价值走标签(见 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 }; /// 每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。 public double WearFactor => FanWear[Fan]; @@ -131,22 +120,8 @@ namespace Decentralized.Domain return Math.Max(1, (long)Math.Round(spec.Price * f)); } - /// 真实价值(芯片型号 + 健康度 + 外观折扣)。 - 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 True(Unit u) => CardLabels.Value(u, null, 0); /// 标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。 public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth); diff --git a/src/Decentralized.Domain/UsedDealer.cs b/src/Decentralized.Domain/UsedDealer.cs index 291282e..fe6b5e8 100644 --- a/src/Decentralized.Domain/UsedDealer.cs +++ b/src/Decentralized.Domain/UsedDealer.cs @@ -7,20 +7,18 @@ 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 = CardValue.ScamDiscount; + public const double MiddlemanMarkup = 0.08; public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45; /// 今天来的贩子的类型:骗子越往后越多。 @@ -52,7 +50,6 @@ namespace Decentralized.Domain 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.Quoted = kind != DealerKind.Honest; card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0; cards.Add(card); } diff --git a/src/Decentralized/GpuInspectView.cs b/src/Decentralized/GpuInspectView.cs index f30c921..2b25382 100644 --- a/src/Decentralized/GpuInspectView.cs +++ b/src/Decentralized/GpuInspectView.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using Decentralized.Domain; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -6,22 +8,26 @@ using UnityEngine.UI; namespace Decentralized { /// - /// 放大镜检视显卡时的大图:和香烟一样把物品放大摆在检视台上,正反面各一张,看外观自己判断真假。 + /// 放大镜检视显卡:和香烟一样把物品放大摆在检视台上,正反面各一张,下面是标签栏。 + /// 标签栏每行一个标签(型号、风扇、金手指、挡板、丝印、贴纸):左边是卖家的说法,右边的按钮"指出"—— + /// 说法属实就没事,隐瞒了问题价格回到真实情况,指错了掉信誉。归自己的卡,有标签机时按钮改成"改",随便改说法。 /// 游戏没有给 mod 开放检视界面,这里不打补丁,只在检视台(InspectionUIManager.panel)下面挂一个自己的界面, /// 用一个常驻的 MonoBehaviour 每帧看一眼是否正在检视显卡。 /// public sealed class GpuInspectView : MonoBehaviour { + private const float RowH = 34f, CardScale = 3f; 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; + private Transform buttonTemplate; + private TMP_FontAsset font; + private readonly TextMeshProUGUI[] rowName = new TextMeshProUGUI[6], rowText = new TextMeshProUGUI[6]; + private readonly Button[] rowButton = new Button[6]; + private readonly TextMeshProUGUI[] rowButtonText = new TextMeshProUGUI[6]; public static void Ensure() { @@ -51,19 +57,19 @@ 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); + 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); } }); } - /// 检视大图的叠加层。五个维度各自对应一种或几种细节,组合时再加几处同源的小细节(磕碰、开过壳的螺丝、重新粘过的封条)。 + /// 检视台上的叠加层:每个标签的每个取值对应一种细节,一一对应。 public static List BigOverlays(GameItem item) { var o = new List(); @@ -71,7 +77,6 @@ namespace Decentralized if (u.Dead) return o; var l = u.Appearance; string P = "gpu_big_ov_"; - // 每个维度的取值对应一种细节,一一对应,不靠组合触发 if (!Items.IsBack(item)) { if (l.Fan == 1) o.Add(P + "fan_dust2"); @@ -91,6 +96,46 @@ namespace Decentralized return o; } + private static bool HasLabeler() => EmporiumEntry.Instance != null && EmporiumEntry.Instance.GetAllOwnedItemsByTag("LABELER_TAG").Count > 0; + + 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(); + rt.SetParent(parent, false); + rt.sizeDelta = size; + rt.localPosition = pos; + var t = go.GetComponent(); + 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