Used-hardware dealer customer and GPU appraisal registration

A scav-type seller visits now and then with three to five used cards. An
honest dealer sells at true value, a middleman adds 8%, a scammer mixes in
fakes (relabelled cheap chips) priced at 90% of the labelled model's value.
The markup is a client-temporary item feature, so a bought card is only
worth its real value. Cards are registered as appraisal items; the correct
value is the same function as the sale value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 11:40:10 +08:00
1 parent 42608a4acd
commit 9b48d7bf0e
6 files changed
+206

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
public enum DealerKind { Honest, Middleman, Scammer }
/// <summary>贩子带来的一张卡:<see cref="MarkupPct"/> 是柜台上相对真实价值的加价百分比(骗子卖假卡时是正的,按标称型号报价再打九折)。</summary>
public sealed class UsedCard
{
public string Model;
public string FakeOf;
public double Health;
public int MarkupPct;
}
public static class UsedDealers
{
public const double MiddlemanMarkup = 0.08, ScamDiscount = 0.90;
public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45;
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
public static DealerKind Kind(int day, Rng rng)
{
double scam = Math.Min(0.30, 0.10 + day * 0.004);
double r = rng.NextDouble();
return r < scam ? DealerKind.Scammer : r < scam + 0.30 ? DealerKind.Middleman : DealerKind.Honest;
}
/// <summary>能出现在二手市场上的型号:按天数放开,旗舰第 30 天起。</summary>
public static string[] Pool(int day)
{
var list = new List<string>(Dealer.Basic);
if (day >= Dealer.AdvancedFromDay) list.AddRange(Dealer.Advanced);
if (day >= Dealer.FlagshipFromDay) list.AddRange(Dealer.Flagship);
return list.ToArray();
}
public static List<UsedCard> Cards(DealerKind kind, int day, Rng rng)
{
var pool = Pool(day);
int count = rng.Range(3, 5);
var cards = new List<UsedCard>();
for (int i = 0; i < count; i++)
{
double share = kind == DealerKind.Scammer ? FakeShareScammer : kind == DealerKind.Middleman ? FakeShareMiddleman : 0;
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);
cards.Add(card);
}
return cards;
}
}
}
+2
View File
@@ -15,6 +15,7 @@ namespace Decentralized
Unlock.Register(); Unlock.Register();
Night.Subscribe(); Night.Subscribe();
DealerStock.Subscribe(); DealerStock.Subscribe();
UsedDealerClient.Subscribe();
ModHook.OnGameLoadedLate += Session.Load; ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame; ModHook.OnNewGameStarted += OnNewGame;
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用"); Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
@@ -25,6 +26,7 @@ namespace Decentralized
Items.Unsubscribe(); Items.Unsubscribe();
Night.Unsubscribe(); Night.Unsubscribe();
DealerStock.Unsubscribe(); DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe();
ModHook.OnGameLoadedLate -= Session.Load; ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame; ModHook.OnNewGameStarted -= OnNewGame;
} }
@@ -58,3 +58,12 @@ news.open.title,Poinbase Lists PSC
news.open.desc,The Upper Level exchange Poinbase now lists PSC against CRC. Graphics cards are in demand across the station. news.open.desc,The Upper Level exchange Poinbase now lists PSC against CRC. Graphics cards are in demand across the station.
news.halving.title,PSC Block Reward Halved news.halving.title,PSC Block Reward Halved
news.halving.desc,Miners now receive half as many coins per night. news.halving.desc,Miners now receive half as many coins per night.
appraisal.gpu,"Graphics card"
dealer.name,"Used-hardware dealer"
dealer.line1.1,"Cards from a mine clearance. Condition is written next to the name."
dealer.line1.2,"A few worked cards, going cheap."
dealer.line1.3,"Picked these up on the road. First-hand price."
dealer.line2.1,"See for yourself, turn them over and check the back."
dealer.line2.2,"No warranty. Look before you take."
dealer.line2.3,"Price is negotiable if you want them."
dealer.markup,"Used-goods markup"
1 # Probably Decentralized localization table: English (en)
58 news.open.desc
59 news.halving.title
60 news.halving.desc
61 appraisal.gpu
62 dealer.name
63 dealer.line1.1
64 dealer.line1.2
65 dealer.line1.3
66 dealer.line2.1
67 dealer.line2.2
68 dealer.line2.3
69 dealer.markup
@@ -57,3 +57,12 @@ news.open.title,Poinbase 上架 PSC
news.open.desc,上层交易所 Poinbase 上架 PSC/CRC 交易对。全站显卡需求上升。 news.open.desc,上层交易所 Poinbase 上架 PSC/CRC 交易对。全站显卡需求上升。
news.halving.title,PSC 出块奖励减半 news.halving.title,PSC 出块奖励减半
news.halving.desc,矿工每晚获得的币减半。 news.halving.desc,矿工每晚获得的币减半。
appraisal.gpu,"显卡"
dealer.name,"二手货贩子"
dealer.line1.1,"矿场清仓的卡,成色都在名字后面标着。"
dealer.line1.2,"几张挖过的卡,便宜出。"
dealer.line1.3,"路上收来的货,一手价。"
dealer.line2.1,"自己看,背面也翻过来看。"
dealer.line2.2,"不保修,看好了再拿。"
dealer.line2.3,"要的话价钱好商量。"
dealer.markup,"二手加价"
1 # Probably Decentralized 本地化表:简体中文(zh)
57 news.open.desc
58 news.halving.title
59 news.halving.desc
60 appraisal.gpu
61 dealer.name
62 dealer.line1.1
63 dealer.line1.2
64 dealer.line1.3
65 dealer.line2.1
66 dealer.line2.2
67 dealer.line2.3
68 dealer.markup
+99
View File
@@ -0,0 +1,99 @@
using System.Collections.Generic;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 二手显卡贩子:偶尔上门,把几张卡摆上柜台,玩家按外观自己判断(翻面看背面)。
/// 老实人按真实价值卖,中间商加价,骗子夹带假卡并按标称型号报价;价格是柜台上的临时加价,买下之后卡只值真实价值。
/// 另外把显卡登记为鉴定公会的鉴定物品,正确价值与实际售价是同一个数。
/// </summary>
public static class UsedDealerClient
{
public const string Appraisal = "pd_gpu_appraisal";
private const double VisitChance = 0.05;
private static int lastVisitDay = -1;
public static void Subscribe()
{
ModHook.OnGenerateCustomerNormal += OnGenerate;
ModHelper.AddAppraisalItem(ModInfo.Id, Appraisal, Items.GpuId("RZX50"), 2, CreateForAppraisal, u => (int)CardValue.True(Items.ToUnit(u)),
Items.IsGpu, null, () => L.T("appraisal.gpu"));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used dealer visit (test)", () => PlayerStore.Instance.storeClientManager?.AddClient(Create(DealerKind.Scammer)));
}
public static void Unsubscribe()
{
ModHook.OnGenerateCustomerNormal -= OnGenerate;
ModHelper.RemoveAppraisalItems(ModInfo.Id);
}
private static void OnGenerate(StoreClientManager manager)
{
if (Session.Data == null || !Unlock.IsUnlocked) return;
Log.Guard("二手贩子", () =>
{
int day = StoreStation.GetDayCounter();
if (day < 3 || day == lastVisitDay || !Session.Data.Rng.Chance(VisitChance)) return;
lastVisitDay = day;
manager.AddClient(Create(UsedDealers.Kind(day, Session.Data.Rng)));
});
}
/// <summary>鉴定公会送来的卡:一成是假卡;默认估价是按标称型号算的价。</summary>
private static GameItem CreateForAppraisal()
{
var rng = Session.Data?.Rng ?? new Rng(1);
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));
return item;
}
private static StoreClient Create(DealerKind kind)
{
int day = StoreStation.GetDayCounter();
var rng = Session.Data.Rng;
var cards = UsedDealers.Cards(kind, day, rng);
var client = new StoreClient
{
identifier = "pd_used_dealer",
displayName = L.T("dealer.name"),
spriteName = SpriteDict.GetRandomScavSpriteName(),
clientIntent = StoreClient.ClientIntent.SELL,
};
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("二手贩子上货", () =>
{
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) item.AddItemFeature(Markup(c.MarkupPct));
PlayerStore.Instance.AddDirectSellingItemToTable(item);
placed.Add($"{c.Model}{(c.FakeOf != null ? "←" + c.FakeOf : "")} {c.Health:0}% {c.MarkupPct:+0;-0}%");
}
Log.Info($"二手贩子({kind}):{string.Join(",", placed)}");
}));
client.AddBasicDialog();
client.CompleteClientCreation();
return client;
}
/// <summary>柜台上的临时加价:离开贩子之后自动去掉(游戏按客户临时特征处理)。</summary>
private static ItemFeature Markup(int pct)
{
var f = new ItemFeature(ItemFeature.FeatureType.ClientTemporary) { identifier = "pd_used_markup", category = "markup", valueStage = ItemFeature.ValueStage.Market };
string text = L.T("dealer.markup");
f.SetPublicDisplay(text);
f.SetActualDisplay(text);
f.isFeatureMatch = true;
f.SetPreExposedValueMod(pct);
return f;
}
}
}
@@ -335,6 +335,29 @@ internal static class Program
Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出"); Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出");
}); });
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, "老实人的卡是真卡且按真实价值");
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"), "型号按天数放开");
});
Test("杰克逊博士:没有矿架必上一个;基础卡每次两张;进阶第 10 天起、旗舰第 30 天起;特供货第 20 天起必有旗舰", () => Test("杰克逊博士:没有矿架必上一个;基础卡每次两张;进阶第 10 天起、旗舰第 30 天起;特供货第 20 天起必有旗舰", () =>
{ {
var rng = new Rng(11); var rng = new Rng(11);