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:
1 parent
68265095a8
commit
f93dbc6eec
9 files changed
+266
-63
No files matched your search
@@ -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,15 +92,9 @@ 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--)
|
||||
@@ -79,11 +111,10 @@ namespace Decentralized.Domain
|
||||
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);
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
@@ -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,"啧,不是原装的。那就按芯片本来的价吧。"
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 天起必有旗舰", () =>
|
||||
|
||||
Reference in new issue
Block a user