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:
1 parent
42608a4acd
commit
9b48d7bf0e
6 files changed
+206
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace Decentralized
|
||||
Unlock.Register();
|
||||
Night.Subscribe();
|
||||
DealerStock.Subscribe();
|
||||
UsedDealerClient.Subscribe();
|
||||
ModHook.OnGameLoadedLate += Session.Load;
|
||||
ModHook.OnNewGameStarted += OnNewGame;
|
||||
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
|
||||
@@ -25,6 +26,7 @@ namespace Decentralized
|
||||
Items.Unsubscribe();
|
||||
Night.Unsubscribe();
|
||||
DealerStock.Unsubscribe();
|
||||
UsedDealerClient.Unsubscribe();
|
||||
ModHook.OnGameLoadedLate -= Session.Load;
|
||||
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.halving.title,PSC Block Reward Halved
|
||||
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"
|
||||
|
@@ -57,3 +57,12 @@ news.open.title,Poinbase 上架 PSC
|
||||
news.open.desc,上层交易所 Poinbase 上架 PSC/CRC 交易对。全站显卡需求上升。
|
||||
news.halving.title,PSC 出块奖励减半
|
||||
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,"二手加价"
|
||||
|
@@ -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 脱锚时按市价兑出");
|
||||
});
|
||||
|
||||
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 天起必有旗舰", () =>
|
||||
{
|
||||
var rng = new Rng(11);
|
||||
|
||||
Reference in new issue
Block a user