Label bar in the magnifier tray: seller's claims per label, point out, edit with a labeler

Six labels (model, fan, contacts, bracket, back print, sticker). Dealers
state claims (honest tell the truth, middlemen hide 70%, scammers hide
everything and name the stamped model); the price follows claims until a
label is pointed out, then follows the real state. Pointing at a fake's
evidence exposes the model label too. A wrong accusation costs reputation.
Owned cards can have claims edited freely with a labeler present; a buyer
spots each hidden flaw with 35% chance, which exposes it and costs
reputation. Dimensions are additive percent adjustments on the model price.

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:50:24 +08:00
1 parent f93dbc6eec
commit b359a00789
9 files changed
+452 -174

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
using System;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>卖家的说法:每个标签一项。型号是文字,其余五项取值与 <see cref="CardLook"/> 的取值一致。</summary>
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) };
}
/// <summary>按真实情况写出的说法(没有隐瞒)。</summary>
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 };
}
}
/// <summary>
/// 标签栏的数据:六个标签(型号、风扇、金手指、挡板、丝印、贴纸),每个标签有卖家的说法和真实情况。
/// 一个标签被指出(拆穿)之后,说法改回真实情况;价格按「已指出的标签取真实情况,其余取说法」算,
/// 所以卖家隐瞒的问题要玩家一项项指出来才能把价格拉回真实价值。
/// </summary>
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 };
/// <summary>各标签各取值对价格的影响(百分比,相加):风扇积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失 −6%。
/// 接口数不对、丝印、手写序列号本身不扣价,它们是假货的证据。</summary>
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;
}
/// <summary>这一项的真实取值是不是假货的证据。</summary>
public static bool IsFakeSign(int row, int realValue) =>
(row == RowBracket && realValue == 2) || (row == RowPrint && realValue != 0) || (row == RowSticker && realValue == 1);
/// <summary>
/// 价值:claim 为空表示没有人说过什么,一律按真实情况;exposed 里的位表示已指出的标签(取真实情况)。
/// 型号取「说法里的型号」,型号标签被拆穿之后取真实芯片。价值 = 型号定价 ×(0.25 + 0.75 × 健康度)×(1 + 各标签折扣之和)。
/// </summary>
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));
}
/// <summary>指出一个标签:返回 0 没问题(说法属实)、1 隐瞒了对价格不利的情况、2 拆穿了假货。
/// 指出假货的证据(接口数、丝印、手写序列号)时,型号标签一并拆穿。</summary>
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;
}
/// <summary>说法里隐瞒了什么:说法比真实情况价值高的标签,以及型号不符。</summary>
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;
}
/// <summary>卖家怎么说:老实人如实说;中间商每项有三成概率隐瞒;骗子全部说成没问题。假卡的型号一律说成标称型号。</summary>
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;
}
}
}
+5 -30
View File
@@ -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 };
/// <summary>外观对价值的折扣:风扇积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失(开过封)−6%。</summary>
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);
// 外观维度对应卡的真实状态:损耗、暴毙概率随之变化,价值走标签(见 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 };
/// <summary>每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。</summary>
public double WearFactor => FanWear[Fan];
@@ -131,22 +120,8 @@ namespace Decentralized.Domain
return Math.Max(1, (long)Math.Round(spec.Price * f));
}
/// <summary>真实价值(芯片型号 + 健康度 + 外观折扣)。</summary>
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 True(Unit u) => CardLabels.Value(u, null, 0);
/// <summary>标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。</summary>
public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth);
+2 -5
View File
@@ -7,20 +7,18 @@ namespace Decentralized.Domain
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
public enum DealerKind { Honest, Middleman, Scammer }
/// <summary>贩子带来的一张卡。老实人不报价(按真实价值卖);其他贩子 <see cref="Quoted"/> 为真,按 <see cref="CardValue.Quote"/> 报价,
/// <see cref="MarkupPct"/> 是加价百分比,玩家指出外观问题或假货迹象就按维度扣回去。</summary>
/// <summary>贩子带来的一张卡:型号、健康度、是不是假卡。怎么说由 <see cref="CardLabels.DealerClaim"/> 决定,中间商另加 <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 = CardValue.ScamDiscount;
public const double MiddlemanMarkup = 0.08;
public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45;
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
@@ -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);
}
+140 -69
View File
@@ -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
{
/// <summary>
/// 放大镜检视显卡时的大图:和香烟一样把物品放大摆在检视台上,正反面各一张,看外观自己判断真假。
/// 放大镜检视显卡:和香烟一样把物品放大摆在检视台上,正反面各一张,下面是标签栏。
/// 标签栏每行一个标签(型号、风扇、金手指、挡板、丝印、贴纸):左边是卖家的说法,右边的按钮"指出"——
/// 说法属实就没事,隐瞒了问题价格回到真实情况,指错了掉信誉。归自己的卡,有标签机时按钮改成"改",随便改说法。
/// 游戏没有给 mod 开放检视界面,这里不打补丁,只在检视台(InspectionUIManager.panel)下面挂一个自己的界面,
/// 用一个常驻的 MonoBehaviour 每帧看一眼是否正在检视显卡。
/// </summary>
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>();
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);
}
});
}
/// <summary>检视大图的叠加层。五个维度各自对应一种或几种细节,组合时再加几处同源的小细节(磕碰、开过壳的螺丝、重新粘过的封条)。</summary>
/// <summary>检视台上的叠加层:每个标签的每个取值对应一种细节,一一对应。</summary>
public static List<string> BigOverlays(GameItem item)
{
var o = new List<string>();
@@ -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<RectTransform>();
rt.SetParent(parent, false);
rt.sizeDelta = size;
rt.localPosition = pos;
var t = go.GetComponent<TextMeshProUGUI>();
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<Button>();
b.onClick = new Button.ButtonClickedEvent();
label = go.GetComponentInChildren<TextMeshProUGUI>();
label.enableAutoSizing = true; label.fontSizeMin = 12; label.fontSizeMax = 24;
// 去掉复制来的按钮上驱动文字的本地化组件,文字由这里设置
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
string n = mb.GetType().Name;
if (mb is Button || mb is Image || mb is TextMeshProUGUI || mb is CanvasRenderer) continue;
if (n.Contains("Local") || n.Contains("Appraisal") || n.Contains("Send")) Destroy(mb);
}
return b;
}
private void Build(InspectionUIManager m)
{
root = new GameObject("_PdGpuInspection", typeof(RectTransform)).GetComponent<RectTransform>();
@@ -99,83 +144,110 @@ namespace Decentralized
root.localPosition = Vector3.zero;
cardRoot = new GameObject("Card", typeof(RectTransform)).GetComponent<RectTransform>();
cardRoot.SetParent(root, false);
cardRoot.localPosition = new Vector3(0, 0, 0);
// 翻面按钮:复制鉴定面板里"发送"按钮的外观
var template = m.appraisalPanel.transform.Find("buttonSend");
if (template != null)
cardRoot.localPosition = new Vector3(0, 85, 0);
buttonTemplate = m.appraisalPanel.transform.Find("buttonSend");
if (buttonTemplate == null) return;
font = buttonTemplate.GetComponentInChildren<TextMeshProUGUI>()?.font;
flip = MakeButton(root, "Flip", new Vector2(-230, 312), new Vector2(160, 34), out flipText);
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
for (int i = 0; i < 6; i++)
{
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, -290, 0);
flip = go.GetComponent<Button>();
flip.onClick = new Button.ButtonClickedEvent();
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
flipText = go.GetComponentInChildren<TextMeshProUGUI>();
// 去掉复制来的按钮上驱动文字的组件(本地化 / 提交逻辑),文字由这里设置
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
string n = mb.GetType().Name;
if (mb is Button || mb is Image || mb is TextMeshProUGUI || mb is CanvasRenderer) continue;
if (n.Contains("Local") || n.Contains("Appraisal") || n.Contains("Send")) Destroy(mb);
}
int row = CardLabels.Rows[i];
float y = -128 - i * RowH;
rowName[i] = MakeText(root, "RowName" + i, new Vector2(-262, y), new Vector2(110, RowH), TextAlignmentOptions.Left, 22);
rowText[i] = MakeText(root, "RowText" + i, new Vector2(-30, y), new Vector2(330, RowH), TextAlignmentOptions.Left, 22);
rowButton[i] = MakeButton(root, "RowButton" + i, new Vector2(245, y), new Vector2(110, RowH - 4), out rowButtonText[i]);
rowButton[i].onClick.AddListener(() => Log.Guard("标签栏", () => OnRowButton(row)));
}
}
/// <summary>对峙:卖家的报价装作成色没问题,玩家逐项指出问题;只在还没买下、卖家在场、卖家报价的卡上出现。</summary>
private void UpdatePointButtons(GameItem item)
private static string ValueText(int row, int value) =>
row == CardLabels.RowSpec ? "" : L.T("lbl." + (row == CardLabels.RowFan ? "fan" : row == CardLabels.RowFingers ? "fingers" : row == CardLabels.RowBracket ? "bracket" : row == CardLabels.RowPrint ? "print" : "sticker") + "." + value);
private static string RowName(int row) => L.T("lbl.row." + (row == CardLabels.RowSpec ? "spec" : row == CardLabels.RowFan ? "fan" : row == CardLabels.RowFingers ? "fingers" : row == CardLabels.RowBracket ? "bracket" : row == CardLabels.RowPrint ? "print" : "sticker"));
/// <summary>现场有没有卖家 / 鉴定客户在:决定"指出"按钮是否出现。</summary>
private static bool CanPoint(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);
if (client == null || GeneralHelper.IsItemOwned(item)) return false;
if (client.clientIntent == StoreClient.ClientIntent.APPRAISAL_SERVICE) return true;
return client.clientIntent == StoreClient.ClientIntent.SELL && !InspectionUIManager.IsConfrontLocked();
}
private void OnPoint(int bit)
private void UpdateRows(GameItem item)
{
if (rowName[0] == null) return;
var u = Items.ToUnit(item);
var real = Claim.Truth(u);
var claim = Items.GetClaim(item);
int exposed = Items.Exposed(item);
bool owned = GeneralHelper.IsItemOwned(item);
bool canPoint = CanPoint(item), canEdit = owned && HasLabeler();
for (int i = 0; i < 6; i++)
{
int row = CardLabels.Rows[i];
rowName[i].text = RowName(row);
string text;
Color color = Color.white;
if (claim == null)
{
text = row == CardLabels.RowSpec ? DisplayModel(real.Model) : ValueText(row, real.Get(row));
}
else if ((exposed & row) != 0)
{
int r = row == CardLabels.RowSpec ? 0 : real.Get(row);
text = row == CardLabels.RowSpec ? DisplayModel(real.Model) : ValueText(row, r);
bool lied = row == CardLabels.RowSpec ? claim.Value.Model != real.Model : claim.Value.Get(row) != r;
if (lied) { color = new Color(0.95f, 0.5f, 0.45f); text += L.T("lbl.said", row == CardLabels.RowSpec ? DisplayModel(claim.Value.Model) : ValueText(row, claim.Value.Get(row))); }
else color = new Color(0.55f, 0.85f, 0.55f);
}
else
{
text = row == CardLabels.RowSpec ? DisplayModel(claim.Value.Model) : ValueText(row, claim.Value.Get(row));
}
rowText[i].text = text;
rowText[i].color = color;
bool show = row != CardLabels.RowSpec && ((canPoint && claim != null && (exposed & row) == 0) || canEdit || (canPoint && claim == null));
bool point = canPoint && !canEdit;
rowButton[i].gameObject.SetActive(row != CardLabels.RowSpec ? ((canPoint && (exposed & row) == 0) || canEdit) : canEdit);
rowButtonText[i].text = L.T(canEdit ? "lbl.edit" : "lbl.point");
}
if (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
}
private static string DisplayModel(string model) => Items.DisplayModel(model);
private void OnRowButton(int row)
{
if (shown == null) return;
var item = shown;
int kind = Items.Point(item, bit);
var d = new Dialogue();
if (GeneralHelper.IsItemOwned(item))
{
// 标签机:改说法,每按一次换下一个取值(型号是在九个型号里轮换)
var u = Items.ToUnit(item);
var cur = Items.GetClaim(item) ?? Claim.Truth(u);
if (row == CardLabels.RowSpec)
{
var models = Hardware.Specs.Values.Where(s => !s.Asic).Select(s => s.Model).ToList();
Items.EditClaim(item, row, 0, models[(models.IndexOf(cur.Model) + 1) % models.Count]);
}
else Items.EditClaim(item, row, (cur.Get(row) + 1) % 3);
EmporiumAudioPlayer.Instance?.audioLabeler?.Play();
return;
}
int kind = Items.Point(item, row);
Log.Info($"指出 {row},结果 {kind},报价 {item.unitValue}");
if (kind == 0)
{
d.SetText("CLIENT", L.T("point.none." + Random.Range(1, 3)));
InspectionUIManager.Instance.OnConfrontFailed();
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T("point.none." + Random.Range(1, 3))));
}
else
{
d.SetText("CLIENT", L.T(kind == 2 ? "point.fake." + Random.Range(1, 3) : "point.worn." + Random.Range(1, 3)));
InspectionUIManager.Instance.OnConfrontSuccess();
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T(kind == 2 ? "point.fake." + Random.Range(1, 3) : "point.worn." + Random.Range(1, 3))));
}
Log.Info($"对峙:指出 {bit},结果 {kind},报价 {item.unitValue}");
DialogUIManager.Instance.StartDialogue(d);
InspectionUIManager.Instance.CloseUI();
}
private void Redraw(GameItem item)
@@ -193,12 +265,11 @@ namespace Decentralized
var rt = go.GetComponent<RectTransform>();
rt.SetParent(cardRoot, false);
rt.sizeDelta = new Vector2(sprite.rect.width, sprite.rect.height);
rt.localScale = Vector3.one * 4f;
rt.localScale = Vector3.one * CardScale;
var img = go.GetComponent<Image>();
img.sprite = sprite;
img.raycastTarget = false;
}
if (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
}
}
}
+93 -44
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", SeedTag = "PD_SEED", QuotedTag = "PD_QUOTED", MarkupTag = "PD_MARKUP", PointedTag = "PD_POINTED", ExposedTag = "PD_EXPOSED";
public const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", ClaimTag = "PD_CLAIM", ExposedTag = "PD_EXPOSED", MarkupTag = "PD_MARKUP";
public static Action OpenTerminal;
@@ -81,69 +81,120 @@ namespace Decentralized
Flip(toggled);
});
item.getItemSpriteModifierForItemFunc = Overlays;
// 摆给顾客的时候,顾客可能看出说法与实物对不上
item.onSlotAddItemFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Combine(item.onSlotAddItemFunc,
(Action<GameItem, GameInventory, SlotMarker>)((it, _, __) => Log.Guard("顾客验货", () => OnOffered(it))));
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)
public static GameItem NewCard(string model, double health, string fakeOf, Rng rng, DealerKind? seller = null, int markupPct = 0)
{
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);
if (seller != null) SetClaim(item, CardLabels.DealerClaim(u, seller.Value, rng), 0, markupPct);
return item;
}
/// <summary>贩子的柜台报价:在物品还不属于玩家时,按 <see cref="CardValue.Quote"/> 算(装作成色没问题,玩家指出一项才扣一项);
/// 归玩家之后立刻只值真实价值。老实人的卡不报价。</summary>
public static void SetQuote(GameItem item, int markupPct)
// ---------- 标签栏:卖家的说法与指出 ----------
private static object checkedClient;
private static readonly HashSet<int> checkedItems = new HashSet<int>();
public const double CustomerSpotChance = 0.35;
/// <summary>
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 35% 的概率看出来;看出来就拆穿这一项(价格回到真实情况),
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
/// </summary>
private static void OnOffered(GameItem item)
{
item.EnableTag(QuotedTag);
item.ModifyTag(MarkupTag, t => t.Enable().SetInt(markupPct));
if (item.IsTag(DeadTag) || !GeneralHelper.IsItemOwned(item) || !item.IsRootedInBackInv()) return;
var ci = PlayerStore.Instance.currentClientInstance;
var client = ci?.GetClientBlueprint();
if (client == null || (client.clientIntent != StoreClient.ClientIntent.BUY && client.clientIntent != StoreClient.ClientIntent.SELLNBUY)) return;
var claim = GetClaim(item);
if (claim == null) return;
if (!ReferenceEquals(checkedClient, ci)) { checkedClient = ci; checkedItems.Clear(); }
if (!checkedItems.Add(item.uniqueId)) return;
var u = ToUnit(item);
int exposed = Exposed(item);
int lies = CardLabels.Lies(u, claim.Value, exposed);
if (lies == 0) return;
var rng = Session.Data.Rng;
int found = 0;
foreach (int r in CardLabels.Rows)
if ((lies & r) != 0 && rng.Chance(CustomerSpotChance))
{
found |= r;
if (r != CardLabels.RowSpec && CardLabels.IsFakeSign(r, Claim.Truth(u).Get(r))) found |= CardLabels.RowSpec;
}
if (found == 0) return;
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed | found));
Refresh(item);
string faction = string.IsNullOrEmpty(client.clientFaction) || StoreReputation.GetStoreReputation(client.clientFaction) == null ? "FACTION_LOWER_LEVEL" : client.clientFaction;
StoreReputation.ModReputation(faction, -2);
EmporiumAudioPlayer.Instance?.audioFail?.Play();
Log.Info($"顾客看出标签不实:{found},价格 {item.unitValue},信誉 −2");
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T((found & CardLabels.RowSpec) != 0 ? "cust.fake." + rng.Range(1, 2) : "cust.worn." + rng.Range(1, 2))));
}
/// <summary>标签上的说法;没有说法的卡(自己挖出来、正规渠道的新卡)一律按真实情况算。</summary>
public static Claim? GetClaim(GameItem item)
{
if (!item.IsTag(ClaimTag)) return null;
var c = Claim.Decode(item.GetTagReadonly(ClaimTag).GetString());
return string.IsNullOrEmpty(c.Model) ? (Claim?)null : c;
}
public static int Exposed(GameItem item) => item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
public static void SetClaim(GameItem item, Claim claim, int exposed, int markupPct = 0)
{
item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (markupPct != 0) 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 static string BelievedModel(GameItem item, Unit u)
{
var c = GetClaim(item);
if (c == null) return u.Model;
return (Exposed(item) & CardLabels.RowSpec) != 0 ? (u.FakeOf ?? u.Model) : c.Value.Model;
}
/// <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)
/// <summary>玩家指出一个标签:记下来,说法改回真实情况;返回 0 说法属实、1 隐瞒了不利的情况、2 拆穿假货。</summary>
public static int Point(GameItem item, int row)
{
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;
var claim = GetClaim(item) ?? Claim.Truth(u);
int exposed = Exposed(item);
int kind = CardLabels.Point(u, claim, row, ref exposed);
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (!item.IsTag(ClaimTag)) item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
Refresh(item);
if (item.IsTag("APPRAISAL_ITEM")) AppraisalHelper.SetDefaultAppraisalValue(item, (int)item.unitValue);
return kind;
}
/// <summary>玩家指出一项:记下来,有问题就按维度扣价或拆穿假货;返回这一项的情况(0 没问题,1 扣价,2 拆穿)。</summary>
public static int Point(GameItem item, int point)
/// <summary>用标签机改说法(随便改):这一项重新算作"没人指出过"。</summary>
public static void EditClaim(GameItem item, int row, int value, string model = null)
{
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;
var u = ToUnit(item);
var claim = GetClaim(item) ?? Claim.Truth(u);
claim = row == CardLabels.RowSpec ? new Claim { Model = model ?? claim.Model, Fan = claim.Fan, Fingers = claim.Fingers, Bracket = claim.Bracket, Print = claim.Print, Sticker = claim.Sticker } : claim.With(row, value);
int exposed = Exposed(item) & ~row;
SetClaim(item, claim, exposed);
}
/// <summary>说明:算力和耗电按标称写;成色在归玩家之前只给粗略等级(要看外观自己判断),归玩家之后给精确健康度。</summary>
private static string Desc(GameItem item, Unit u)
{
var label = u.Label;
var label = Hardware.Specs[BelievedModel(item, u)];
string cond = GeneralHelper.IsItemOwned(item)
? Mathf.RoundToInt((float)u.Health) + "%"
: L.T(u.Health >= 85 ? "item.gpu.grade.1" : u.Health >= 60 ? "item.gpu.grade.2" : u.Health >= 35 ? "item.gpu.grade.3" : "item.gpu.grade.4");
@@ -153,12 +204,13 @@ namespace Decentralized
private static void SyncValue(GameItem item)
{
if (item.IsTag(DeadTag)) return;
string desc = Desc(item, ToUnit(item));
if (item.shortDescription != desc) item.shortDescription = desc;
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);
string desc = Desc(item, u);
if (item.shortDescription != desc) item.shortDescription = desc;
string name = L.T("item.gpu.name", Hardware.Specs[BelievedModel(item, u)].Brand, DisplayModel(BelievedModel(item, u)));
if (item.name != name) item.name = name;
long value = CardLabels.Value(u, GetClaim(item), Exposed(item));
if (!GeneralHelper.IsItemOwned(item) && item.IsTag(MarkupTag)) value = Math.Max(1, (long)Math.Round(value * (1 + item.GetTagReadonly(MarkupTag).GetInt() / 100.0)));
if (item.unitValue != value) item.unitValue = value;
}
@@ -202,9 +254,6 @@ namespace Decentralized
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 = Desc(item, u);
item.unitValue = CardValue.True(u);
SyncValue(item);
}
+28 -5
View File
@@ -72,14 +72,37 @@ 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."
lbl.row.spec,"Model"
lbl.row.fan,"Fans"
lbl.row.fingers,"Contacts"
lbl.row.bracket,"Bracket"
lbl.row.print,"Back print"
lbl.row.sticker,"Sticker"
lbl.fan.0,"Blades clean"
lbl.fan.1,"Blades dusty"
lbl.fan.2,"Blade chipped"
lbl.fingers.0,"Bright"
lbl.fingers.1,"Dull"
lbl.fingers.2,"Oxidised"
lbl.bracket.0,"Intact, three ports"
lbl.bracket.1,"Scratched"
lbl.bracket.2,"Wrong port count"
lbl.print.0,"Matches the model"
lbl.print.1,"Another model printed"
lbl.print.2,"Reprinted, crooked"
lbl.sticker.0,"Printed serial, seal intact"
lbl.sticker.1,"Handwritten serial"
lbl.sticker.2,"Seal missing"
lbl.said," (seller said: {0})"
lbl.point,"Point out"
lbl.edit,"Edit"
cust.fake.1,"That's not what the label says. You trying to pass off a fake?"
cust.fake.2,"The model doesn't match. That isn't the chip it claims."
cust.worn.1,"That's not what you wrote. You left out the flaws."
cust.worn.2,"The condition on the label is false. Not at this price."
1 # Probably Decentralized localization table: English (en)
72 item.gpu.grade.2
73 item.gpu.grade.3
74 item.gpu.grade.4
point.fan
point.fingers
point.bracket
point.print
point.sticker
75 point.none.1
76 point.none.2
77 point.worn.1
78 point.worn.2
79 point.fake.1
80 point.fake.2
81 lbl.row.spec
82 lbl.row.fan
83 lbl.row.fingers
84 lbl.row.bracket
85 lbl.row.print
86 lbl.row.sticker
87 lbl.fan.0
88 lbl.fan.1
89 lbl.fan.2
90 lbl.fingers.0
91 lbl.fingers.1
92 lbl.fingers.2
93 lbl.bracket.0
94 lbl.bracket.1
95 lbl.bracket.2
96 lbl.print.0
97 lbl.print.1
98 lbl.print.2
99 lbl.sticker.0
100 lbl.sticker.1
101 lbl.sticker.2
102 lbl.said
103 lbl.point
104 lbl.edit
105 cust.fake.1
106 cust.fake.2
107 cust.worn.1
108 cust.worn.2
+28 -5
View File
@@ -71,14 +71,37 @@ 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,"啧,不是原装的。那就按芯片本来的价吧。"
lbl.row.spec,"型号"
lbl.row.fan,"风扇"
lbl.row.fingers,"金手指"
lbl.row.bracket,"挡板"
lbl.row.print,"背面丝印"
lbl.row.sticker,"贴纸封条"
lbl.fan.0,"叶片干净"
lbl.fan.1,"叶片积灰"
lbl.fan.2,"叶片缺角"
lbl.fingers.0,"光亮"
lbl.fingers.1,"发暗"
lbl.fingers.2,"氧化发绿"
lbl.bracket.0,"完好,接口三个"
lbl.bracket.1,"有划痕"
lbl.bracket.2,"接口数不对"
lbl.print.0,"与型号一致"
lbl.print.1,"印着另一个型号"
lbl.print.2,"重印过,字歪"
lbl.sticker.0,"印刷序列号,封条完好"
lbl.sticker.1,"手写序列号"
lbl.sticker.2,"封条已缺失"
lbl.said,"(卖家说:{0})"
lbl.point,"指出"
lbl.edit,"改"
cust.fake.1,"这不是它标的那块。你拿假货糊弄我?"
cust.fake.2,"型号对不上,里面的芯片根本不是这个。"
cust.worn.1,"跟你写的不一样,毛病你没说。"
cust.worn.2,"标签上的成色是假的吧,这个价我不要。"
1 # Probably Decentralized 本地化表:简体中文(zh)
71 item.gpu.grade.2
72 item.gpu.grade.3
73 item.gpu.grade.4
point.fan
point.fingers
point.bracket
point.print
point.sticker
74 point.none.1
75 point.none.2
76 point.worn.1
77 point.worn.2
78 point.fake.1
79 point.fake.2
80 lbl.row.spec
81 lbl.row.fan
82 lbl.row.fingers
83 lbl.row.bracket
84 lbl.row.print
85 lbl.row.sticker
86 lbl.fan.0
87 lbl.fan.1
88 lbl.fan.2
89 lbl.fingers.0
90 lbl.fingers.1
91 lbl.fingers.2
92 lbl.bracket.0
93 lbl.bracket.1
94 lbl.bracket.2
95 lbl.print.0
96 lbl.print.1
97 lbl.print.2
98 lbl.sticker.0
99 lbl.sticker.1
100 lbl.sticker.2
101 lbl.said
102 lbl.point
103 lbl.edit
104 cust.fake.1
105 cust.fake.2
106 cust.worn.1
107 cust.worn.2
+4 -5
View File
@@ -68,8 +68,8 @@ namespace Decentralized
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));
var item = Items.NewCard(c.Model, c.Health, c.FakeOf, rng, DealerKind.Middleman); // 带一份说法:鉴定时玩家要指出哪些不实
AppraisalHelper.InitAppraisalItem(item, (int)item.unitValue);
return item;
}
@@ -99,10 +99,9 @@ namespace Decentralized
var placed = new List<string>();
foreach (var c in cards)
{
var item = Items.NewCard(c.Model, c.Health, c.FakeOf, rng);
if (c.Quoted) Items.SetQuote(item, c.MarkupPct);
var item = Items.NewCard(c.Model, c.Health, c.FakeOf, rng, kind, c.MarkupPct);
PlayerStore.Instance.AddDirectSellingItemToTable(item);
placed.Add($"{c.Model}{(c.FakeOf != null ? "←" + c.FakeOf : "")} {c.Health:0}% {(c.Quoted ? c.MarkupPct + "%" : "不报价")} 柜台价 {item.unitValue}(真实 {CardValue.True(Items.ToUnit(item))})");
placed.Add($"{c.Model}{(c.FakeOf != null ? "←" + c.FakeOf : "")} {c.Health:0}% 加价 {c.MarkupPct}% 柜台价 {item.unitValue}(真实 {CardValue.True(Items.ToUnit(item))})");
}
Log.Info($"二手贩子({kind}):{string.Join(",", placed)}");
});
+18 -11
View File
@@ -157,7 +157,7 @@ internal static class Program
Check(CardValue.True(u) == CardValue.Of("RZX60", 80) && CardValue.Quoted(u, 80) == CardValue.Of("RZX90", 80), "真实价值按芯片型号,标称价按标称型号");
Check(CardValue.Quoted(u, 80) > CardValue.True(u) * 8, $"假 RZX 90 的标称价 {CardValue.Quoted(u, 80)} 远高于真值 {CardValue.True(u)}");
var worn = new Unit { Model = "RZX70", Health = 100, Look = new CardLook { Fan = 2, Fingers = 2, Bracket = 1, Sticker = 2 } };
Check(Near(CardLook.Decode("22102").ValueFactor, 0.88 * 0.88 * 0.98 * 0.94, 1e-9) && CardValue.True(worn) == (long)Math.Round(800 * 0.88 * 0.88 * 0.98 * 0.94), "外观折扣乘进真实价值:风扇缺角 + 金手指氧化 + 划痕 + 封条缺失");
Check(CardValue.True(worn) == (long)Math.Round(800 * (1 - 0.12 - 0.12 - 0.02 - 0.06)), "各标签折扣相加进真实价值:风扇缺角 + 金手指氧化 + 划痕 + 封条缺失");
var clean = new Unit { Model = "RZX70", Health = 80 };
var dusty = new Unit { Model = "RZX70", Health = 80, Look = new CardLook { Fan = 2, Fingers = 2 } };
Check(dusty.DeathChance(ModuleBonus.None) > clean.DeathChance(ModuleBonus.None) * 1.5 && Near(CardLook.Decode("20000").WearFactor, 1.3, 1e-9), "风扇缺角、金手指氧化抬高暴毙概率与损耗");
@@ -346,7 +346,7 @@ internal static class Program
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.Quoted, "老实人的卡是真卡且不报价");
Check(c.FakeOf == null, "老实人的卡是真卡");
for (int i = 0; i < 300; i++)
foreach (var c in UsedDealers.Cards(DealerKind.Scammer, 40, rng))
{
@@ -357,15 +357,22 @@ internal static class Program
}
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%");
// 标签:假 RZX 70(芯片 RZX 50)、健康 60、风扇缺角、金手指氧化、丝印印着真实型号
var u = new Unit { Model = "RZX70", FakeOf = "RZX50", Health = 60, Look = new CardLook { Fan = 2, Fingers = 2, Print = 1 } };
var claim = CardLabels.DealerClaim(u, DealerKind.Scammer, new Rng(3));
Check(claim.Model == "RZX70" && claim.Fan == 0 && claim.Fingers == 0 && claim.Print == 0, "骗子的说法:标称型号,外观全是没问题");
long hidden = CardLabels.Value(u, claim, 0), real = CardValue.True(u);
Check(hidden == CardValue.Of("RZX70", 60) && real == (long)Math.Round(CardValue.Of("RZX50", 60) * 0.76), "没指出时按说法算(标称型号满价),真实价值按芯片和外观算");
int ex = 0;
Check(CardLabels.Point(u, claim, CardLabels.RowFan, ref ex) == 1 && CardLabels.Value(u, claim, ex) == (long)Math.Round(CardValue.Of("RZX70", 60) * 0.88), "指出风扇缺角:扣 12%");
Check(CardLabels.Point(u, claim, CardLabels.RowPrint, ref ex) == 2 && (ex & CardLabels.RowSpec) != 0, "指出丝印印着真实型号:拆穿假货,型号标签一并拆穿");
Check(CardLabels.Point(u, claim, CardLabels.RowFingers, ref ex) == 1 && CardLabels.Value(u, claim, ex) == real, "全部指出之后价格回到真实价值");
int ex2 = 0;
var honestUnit = new Unit { Model = "RZX70", Health = 60, Look = new CardLook { Fan = 1 } };
Check(CardLabels.Point(honestUnit, CardLabels.DealerClaim(honestUnit, DealerKind.Honest, new Rng(1)), CardLabels.RowFan, ref ex2) == 0, "老实人说的是实话,指出去没用");
Check(CardLabels.Lies(u, claim, 0) == (CardLabels.RowSpec | CardLabels.RowFan | CardLabels.RowFingers | CardLabels.RowPrint), "骗子隐瞒了型号、风扇、金手指、丝印");
Check(CardLabels.Lies(u, claim, ex) == 0, "指出之后不再算隐瞒");
Check(Claim.Decode(claim.Encode()).Encode() == claim.Encode(), "说法能存取");
});
Test("外观随损耗重算:越旧外观越差;种子固定;算力受风扇与金手指影响", () =>