From 090a973d2308fc2a90b66ed6f82acbd41fd95773 Mon Sep 17 00:00:00 2001 From: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:01:20 +0800 Subject: [PATCH] Phase 2: customers paying in coin (CRC/CRT/CRA/PSC), taint from stolen goods Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 + src/Decentralized.Domain/CoinPayment.cs | 47 +++++++++++ src/Decentralized.Domain/Tuning.cs | 2 + src/Decentralized/CoinCustomers.cs | 78 +++++++++++++++++++ src/Decentralized/ModEntry.cs | 2 + .../ModFiles/Localization/en.csv | 1 + .../ModFiles/Localization/zh.csv | 1 + src/Decentralized/Night.cs | 2 + src/Decentralized/Session.cs | 2 + tests/Decentralized.Domain.Tests/Program.cs | 10 +++ 10 files changed, 149 insertions(+) create mode 100644 src/Decentralized.Domain/CoinPayment.cs create mode 100644 src/Decentralized/CoinCustomers.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 96df71a..bcfb603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 未发布 +### 第二阶段(进行中) + +- 用币付款的顾客:排队的买家里,上层付 CRC(12%)、黑市付 CRT(25%)、革命派付 CRA(25%)、下层付 PSC(10%)。成交时扣回信用点,按当日币价换成币进热钱包;赃物款项不记销赃,改成币带污点(货值 × 赃物热度%);夜间日志汇总当天用币结算的额度。开发者模式 F9 有测试入口。游戏内成交流程尚未实测(测试存档里顾客不上柜台),机制本身在技术验证里通过。 + ### 第一阶段完成(入门、挖矿、显卡鉴定、终端) 以下条目取代下面较早条目里已过时的部分(型号、入口价、终端页面): diff --git a/src/Decentralized.Domain/CoinPayment.cs b/src/Decentralized.Domain/CoinPayment.cs new file mode 100644 index 0000000..b49a1f5 --- /dev/null +++ b/src/Decentralized.Domain/CoinPayment.cs @@ -0,0 +1,47 @@ +using System; + +namespace Decentralized.Domain +{ + /// + /// 顾客用币付款:成交的信用点在店里扣回,按当前币价换成币进热钱包。 + /// 收赃物的款项不再记销赃,改为币带污点:货值 × 赃物热度%。 + /// + public static class CoinPayment + { + /// 各派系用什么币付款(其余派系不用币)。 + public static string AssetFor(string faction) + { + switch (faction) + { + case "FACTION_UPPER_LEVEL": return Assets.CRC; + case "FACTION_BLACK_MARKET": return Assets.CRT; + case "FACTION_REVOLUTION": return Assets.CRA; + case "FACTION_LOWER_LEVEL": return Assets.PSC; + default: return null; + } + } + + /// 一位顾客用币付款的概率。 + public static double Chance(string asset) + { + switch (asset) + { + case Assets.CRC: return Tuning.CoinPayCrc; + case Assets.CRT: return Tuning.CoinPayCrt; + case Assets.CRA: return Tuning.CoinPayCra; + default: return Tuning.CoinPayPsc; + } + } + + /// 收款:返回收到的币数。heatPct 是物品的赃物热度(0–100)。 + public static double Receive(Wallet w, string asset, double credits, double price, double heatPct) + { + if (credits <= 0 || price <= 0) return 0; + double qty = credits / price; + w.Add(asset, qty); + w.Taint += credits * Math.Max(0, Math.Min(100, heatPct)) / 100.0; + w.Basis += credits; + return qty; + } + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 095839c..e0a18be 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -57,6 +57,8 @@ namespace Decentralized.Domain public const int AmlWindowDays = 7; public const double AmlTaintRatio = 0.10, AmlHeavyRatio = 0.30, AmlMixerBonus = 0.30, AmlMaxChance = 0.9; public const double TaxRate = 0.08; + /// 各派系顾客用币付款的概率。 + public const double CoinPayCrc = 0.12, CoinPayCrt = 0.25, CoinPayCra = 0.25, CoinPayPsc = 0.10; /// 单个金融违规事件记入的罪证上限。 public const double CrimeEventCap = 400; diff --git a/src/Decentralized/CoinCustomers.cs b/src/Decentralized/CoinCustomers.cs new file mode 100644 index 0000000..228c4d0 --- /dev/null +++ b/src/Decentralized/CoinCustomers.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using Decentralized.Domain; + +namespace Decentralized +{ + /// + /// 用币付款的顾客:每天排队的买家里,按派系(上层 CRC、黑市 CRT、革命派 CRA、下层 PSC)有一小部分改用币结算。 + /// 成交时在店里扣回刚入账的信用点,换成币进热钱包;赃物的款项不记销赃,改成币带污点(货值 × 赃物热度%)。 + /// + public static class CoinCustomers + { + private static readonly HashSet Marked = new HashSet(); + private static int markDay = -1; + + public static void Subscribe() + { + ModHook.OnGenerateCustomerLate += OnGenerate; + ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer (test)", () => Log.Guard("用币付款顾客(测试)", () => + { + var c = new StoreClient + { + identifier = "pd_coin_test", displayName = "Coin buyer", spriteName = SpriteDict.GetRandomScavSpriteName(), + clientFaction = "FACTION_BLACK_MARKET", useClientBudget = true, clientIntent = StoreClient.ClientIntent.BUY, + canClientExposeFeature = ClientCanExposeFunc.BasicClientExposeCapacity, + }; + c.SetBudget(5000); + c.clientBuyingTagList.Add("WATER"); c.clientBuyingTagList.Add("BREVAGE"); + c.mainDialogue.SetText(c.displayName, "CRT.").SetEndAction(() => { }); + c.AddBasicDialog(); + c.CompleteClientCreation(); + Mark(c, Assets.CRT); + PlayerStore.Instance.currentClientInstance = StoreClientInstance.CreateClientInstance(c); // 测试:直接换成这位顾客 + })); + } + public static void Unsubscribe() => ModHook.OnGenerateCustomerLate -= OnGenerate; + + private static void OnGenerate(StoreClientManager manager) + { + if (Session.Data == null || !Unlock.IsUnlocked) return; + Log.Guard("用币付款", () => + { + int day = StoreStation.GetDayCounter(); + if (day != markDay) { Marked.Clear(); markDay = day; } + foreach (var c in manager.clientStack) + { + if (c == null || c.isSecurity || Marked.Contains(c) || c.clientIntent != StoreClient.ClientIntent.BUY || c.isNoContributeToEvidence) continue; + Marked.Add(c); + string asset = CoinPayment.AssetFor(c.clientFaction); + if (asset == null || !Session.Data.Rng.Chance(CoinPayment.Chance(asset))) continue; + Mark(c, asset); + } + }); + } + + private static void Mark(StoreClient c, string asset) + { + var before = c.onItemBought; + c.isNoContributeToEvidence = true; + c.onItemBought = (item, cost) => + { + before?.Invoke(item, cost); + Log.Guard("用币付款成交", () => Pay(asset, item, cost)); + }; + } + + private static void Pay(string asset, GameItem item, int cost) + { + var d = Session.Data; + double price = Session.Price(asset); + if (cost <= 0 || price <= 0) return; + int heat = item.IsTag("STOLEN_VALUE_INT") ? item.GetTagReadonly("STOLEN_VALUE_INT").GetInt() : 0; + ModHelper.AddCredit(-cost, false); + double qty = CoinPayment.Receive(d.Hot, asset, cost, price, heat); + d.CoinSalesToday += cost; + Log.Info($"用币付款:{item.name} {cost} 信用点 → {qty:0.####} {asset},污点 +{cost * heat / 100.0:0}"); + } + } +} diff --git a/src/Decentralized/ModEntry.cs b/src/Decentralized/ModEntry.cs index 9c58c14..48d2fc5 100644 --- a/src/Decentralized/ModEntry.cs +++ b/src/Decentralized/ModEntry.cs @@ -16,6 +16,7 @@ namespace Decentralized Night.Subscribe(); DealerStock.Subscribe(); UsedDealerClient.Subscribe(); + CoinCustomers.Subscribe(); GpuInspectView.Ensure(); ModHook.OnGameLoadedLate += Session.Load; ModHook.OnNewGameStarted += OnNewGame; @@ -28,6 +29,7 @@ namespace Decentralized Night.Unsubscribe(); DealerStock.Unsubscribe(); UsedDealerClient.Unsubscribe(); + CoinCustomers.Unsubscribe(); GpuInspectView.Remove(); ModHook.OnGameLoadedLate -= Session.Load; ModHook.OnNewGameStarted -= OnNewGame; diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 7fb968a..a264b66 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -50,6 +50,7 @@ report.burnt,Cards burnt out: {0} report.price,PSC price: {0} credits nightlog.mined,Mining rigs produced {0} PSC. nightlog.burnt,A {0} burnt out. +nightlog.coinpay,Customers paying in coin settled {0} credits; the coins went to the hot wallet. nightlog.unpowered,{0} mining rig(s) had no power. 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. diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 3e4ae2d..3c44446 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -49,6 +49,7 @@ report.burnt,烧毁的显卡:{0} report.price,PSC 价格:{0} 信用点 nightlog.mined,矿架挖出 {0} PSC。 nightlog.burnt,一张 {0} 烧毁了。 +nightlog.coinpay,用币付款的顾客共结算 {0} 信用点,已换成币进了热钱包。 nightlog.unpowered,{0} 个矿架没有电。 news.open.title,Poinbase 上架 PSC news.open.desc,上层交易所 Poinbase 上架 PSC/CRC 交易对。全站显卡需求上升。 diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs index 17f5e6a..fe012e3 100644 --- a/src/Decentralized/Night.cs +++ b/src/Decentralized/Night.cs @@ -120,6 +120,8 @@ namespace Decentralized foreach (var name in report.Burnt) ps.AddNightLog(L.T("nightlog.burnt", name), "cc7755"); int unpowered = report.Rigs - report.PoweredRigs; if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755"); + if (d.CoinSalesToday > 0) ps.AddNightLog(L.T("nightlog.coinpay", Fmt.Credits(d.CoinSalesToday))); + d.CoinSalesToday = 0; Session.Save(); } } diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs index bb482e6..4bc7b1d 100644 --- a/src/Decentralized/Session.cs +++ b/src/Decentralized/Session.cs @@ -28,6 +28,8 @@ namespace Decentralized public List ActiveEvents = new List(); public NightReport LastNight; public bool OpeningNewsShown; + /// 当天用币付款的成交额(信用点),夜间清零。 + public double CoinSalesToday; } /// 当前这一局的状态:读档 / 新游戏时载入,睡觉结算后写回。 diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 54ec9ce..270531a 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -488,6 +488,16 @@ internal static class Program Check(Near(back.Market.Price, m.Price, 1e-9), "读回后继续推进,行情与原来完全相同"); }); + Test("用币付款:派系对应的币、按币价换算、污点与成本", () => + { + var w = new Wallet { Address = "t" }; + Check(CoinPayment.AssetFor("FACTION_BLACK_MARKET") == Assets.CRT && CoinPayment.AssetFor("FACTION_SECURITY") == null, "派系对应的币"); + double q = CoinPayment.Receive(w, Assets.CRT, 200, 0.5, 40); + Check(Near(q, 400, 1e-9) && Near(w.Get(Assets.CRT), 400, 1e-9), "按币价换算数量"); + Check(Near(w.Taint, 80, 1e-9) && Near(w.Basis, 200, 1e-9), "污点 = 货值 × 热度%,成本 = 货值"); + Check(CoinPayment.Receive(w, Assets.CRC, 0, 1, 10) == 0, "零金额不收"); + }); + Console.WriteLine($"\n{_passed} 通过,{_failed} 失败"); return _failed == 0 ? 0 : 1; }