diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e085cd..7ff5d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,14 @@ - 货架重做:违禁品池只留能转手的货(枪械、钥匙卡、密码解序器芯片、爆炸物),弹药、毒品、药品、化学品、种子全部移除;高科技物品池换成矿机能用的东西(7 种系统模组、能量电池、能量阵列、高级模组提取器、矿架);显卡与高科技各占 30%,高科技不再需要中档声望。 - 玩家出售:黑市页新增"出售"子页。储藏区里的违禁品、高科技物品和显卡做成货架,选标价倍率 0.6–1.8(相对参考价;显卡按标称型号与成色)上架,收 5% 上架费。每晚按期望天数成交:≤0.9 倍 1 天,1.0 倍 2 天,1.8 倍 14 天,之间插值。成交扣 8% 抽成,货款托管 3 天后以 CRT 进钱包。货款带污点的概率和比例随倍率从 20%/20% 涨到 90%/80%。 - 出售的风险:假卡有 50% 被买家看穿退货,好评 −10(好评影响成交速度,每笔成交 +1);平台跑路时上架的货和未放款的货款都没了;蜜罐期间成交的货款和近期买家落网会被追查记罪证;镇压期间平台不成交。"我的订单与情报"页多出"我的上架",可以撤下。 +- 游戏内验证(全部商家逐家点开截图):21 家商家(18 类 + 大宗 + 尖端店铺 + 2 位个人卖家)的货架都显示正常;尖端店铺卖全新 RTX 90(5139)和超科技储藏区(10265);个人卖家混着药、模组、水培、显卡,价格各异。 - 游戏内验证:货架显示显卡、模组、能量电池;买一张卡后出售页列出储藏区里的货,上架(标价 400,上架费 20),过夜成交(货款 368),3 天后到账(CRT 2804 → 3172,这次随机没带污点)。没有在游戏里验证:假卡退货、跑路时的出售损失(只有单元测试)。 - 收尾核算(`research/sim/bm_econ.py`,按 Tuning 常量算期望):Ponero 来回 15.4%,与混币器洗掉 80% 污点的 15–18% 相当,但当天完成、清得彻底、不受制裁,所以手续费 8% → 10%(来回 19%),日额度 1000 → 600(日额度按钱包污点占比估,约够清掉稳态的污点流入,后期会成为瓶颈);出售的最优倍率取决于玩家的资金机会成本:每天 1% 时 ×1.8,2–3% 时 ×1.5,倍率不是越高越好;订单:托管期望净 +0.02、提前放款 +0.07(现货 −0.30),所以订单是便宜进货,不是赚钱路线;订单进货再挂高价出售的套利每件约 +0.13–0.18(20 天一轮,资金占用),量小,暂不限制上架数。 - 订单改成商家货架(用户意见):黑市页"订单"改名"商家",每周从 6 种类型里抽 5 家(全新矿卡经销商、二手矿卡贩子、模组高科技、军火、证件芯片、药物),另有 25% 的周出现一家大宗商家(库存 10–25 件、价格系数 0.55–0.75、送货 6 天)。每家有自己的价格系数(订单价 = 基础价 × 系数,一般 0.70–0.95,全新卡 0.85–1.00,二手卡 0.65–0.90),每家 4 种货、库存 3–6 件。一次订 5 件以上 95 折、10 件以上 9 折。界面:件数按钮 ×1 / ×5 / ×10,商家按钮,所选商家的货架。药物(药片、梦尘、管制化学品)只在药物商家和大宗商家出现,也能转手出售。每周货架总值在游戏里抽样 60 周平均 13.2k(目标 1.4 万)。 - 商家类型扩到 18 种 + 大宗(用户意见,对照游戏的物品目录和玩家职业):全新矿卡经销商、二手矿卡贩子、矿架与能源、模组、军火(枪、手雷、炸药、刀具、电击枪)、枪匠配件(枪械配件、3D 打印芯片与模组、枪械零件)、证件芯片、药物(药片、注射器、血袋、手术工具)、化学品(化学用品、助熔剂、化学设备模组、量筒试剂瓶)、酿酒(酵母、荧光莓、啤酒、酒瓶)、水培农场(种子、水培模组、植物分析仪)、养殖(鱼卵、鱼子酱、循环水养殖系统、血清、屠宰机)、矿工(矿石、金属锭、电子元件、拾荒扫描仪)、水务(滤水、净水、储水、检测)、仓储(走私暗格、储藏区扩建、军用背包)、工具(涡轮增压器、等离子切割机、X 射线扫描仪、心智重编程器)、奢侈品(香水、精华液、香烟)。每周 7 家:2 家从矿卡 / 矿架 / 模组 4 类里抽(保证矿工路线总有货),其余 5 家从其余类型里抽;每家 3 种货,库存 2–5 件,单件基础价超过 4000 的不上。游戏里抽样 60 周货架总值平均 13.2k(目标 1.4 万)。药物改成并入药物商家,现货柜台的货不变。所有货都能转手出售。 - 尖端店铺与个人卖家(用户意见):贵货(单件基础价 ≥ 2000:超科技涡轮增压器、超科技储藏区、心智重编程器、顶级显卡等)不再被一刀切掉,改由"尖端店铺"卖:每周只有 15% 的概率出现,2 种货、库存 1–2 件、价格系数 0.85–1.00、送货 5 天;顶级全新显卡也在这里。个人卖家:每周固定 2 位,什么都卖(可能是药、模组、显卡、仍有 10% 混进贵货),每件价格系数各自随机 0.50–1.20(捡漏或被宰),库存 1–2 件,是骗子的概率 25%,都是新号所以只接受托管。常规商家仍只卖基础价 ≤ 4000 的货。 +- 商家名用随机哈希匿名,前面用方括号标类型(用户意见,试过每类型 10 个主题店名后撤回):如 "[全新矿卡] df9ec7";选中的商家按钮前有 "> "。 - 每周货架总值放宽到 2 万左右(用户意见):库存范围 2–5 → 2–6,游戏里抽样 60 周平均 22.2k,尖端店铺出现概率保持 15% 不变,不靠它拉高周转量。 - 同时上架最多 10 件(用户意见),出售页显示"上架 n/10",超出时提示。 - 出售:货款污点随近 7 天成交额加重(成交额达 1.4 万时概率与比例各加 30%);出售页加"全部上架"按钮。套利核算(`bm_econ.py`,商家系数均值 0.825 再加批量折扣):普通商家提前放款进货 + ×1.5 出售每件约 +21%,大宗商家约 +33%,一周可买的货约 1.3 万,带污点的货款约够吃满 Ponero 日额度。 diff --git a/src/Decentralized.Domain/BmOrders.cs b/src/Decentralized.Domain/BmOrders.cs index 902f9b8..eeef7f3 100644 --- a/src/Decentralized.Domain/BmOrders.cs +++ b/src/Decentralized.Domain/BmOrders.cs @@ -87,8 +87,6 @@ namespace Decentralized.Domain public static double HoneypotChance(int secRep) => Math.Min(Tuning.BmHoneypotMax, Tuning.BmHoneypotBase + Math.Max(0, secRep) * Tuning.BmHoneypotPerSecRep); - private static readonly string[] NameA = { "rat", "grey", "static", "null", "dust", "iron", "pale", "wire", "salt", "ghost" }; - private static readonly string[] NameB = { "king", "mule", "broker", "hand", "cat", "fox", "tooth", "lamp", "nine", "dealer" }; public bool Closed(int day) => day <= ClosedUntil; @@ -166,14 +164,18 @@ namespace Decentralized.Domain } /// 每周重新生成商家、货架和论坛情报(与现货同一天刷新):从 6 种类型里抽 BmMerchantCount 家,另有概率出现一家大宗商家。 - public void RefreshOrders(int day, int rep, Rng rng, Func value) + public void RefreshOrders(int day, int rep, Rng rng, Func value, bool allTypes = false) { Sellers.Clear(); Catalog.Clear(); Forum.Clear(); var types = Core.OrderBy(_ => rng.NextDouble()).Take(Tuning.BmCoreMerchants).ToList(); types.AddRange(Others.OrderBy(_ => rng.NextDouble()).Take(Tuning.BmMerchantCount - Tuning.BmCoreMerchants)); + if (allTypes) { types = ((MerchantType[])Enum.GetValues(typeof(MerchantType))).ToList(); types.Add(MerchantType.Personal); } + else + { if (rng.Chance(Tuning.BmBulkChance)) types.Add(MerchantType.Bulk); if (rng.Chance(Tuning.BmEliteChance)) types.Add(MerchantType.Elite); for (int i = 0; i < Tuning.BmPersonalCount; i++) types.Add(MerchantType.Personal); + } foreach (var t in types) { bool personal = t == MerchantType.Personal; @@ -184,7 +186,7 @@ namespace Decentralized.Domain double hi = t == MerchantType.Elite ? Tuning.BmFactorEliteMax : t == MerchantType.GpuNew ? Tuning.BmFactorNewMax : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMax : t == MerchantType.Bulk ? Tuning.BmFactorBulkMax : Tuning.BmFactorMax; var s = new BmSeller { - Name = NameA[rng.Range(0, NameA.Length - 1)] + "_" + NameB[rng.Range(0, NameB.Length - 1)] + rng.Range(1, 99), + Name = rng.NextUInt().ToString("x8").Substring(0, 6), Scam = scam, Type = t, Factor = personal ? 1.0 : F(rng, lo, hi), Trades = fresh ? rng.Range(0, 12) : rng.Range(25, 220), Rating = scam && !fresh ? rng.Range(94, 99) : fresh ? rng.Range(70, 100) : rng.Range(90, 99), diff --git a/src/Decentralized/BlackDelivery.cs b/src/Decentralized/BlackDelivery.cs index 8368a5a..d48dd53 100644 --- a/src/Decentralized/BlackDelivery.cs +++ b/src/Decentralized/BlackDelivery.cs @@ -42,6 +42,9 @@ namespace Decentralized return ModHelper.SpawnItem(id); } + /// 商家名:随机的匿名哈希,类型另外标注。 + public static string DisplayName(string name) => name; + public static string ItemName(string id) { var it = ModHelper.SpawnItem(GameId(id)); @@ -115,8 +118,8 @@ namespace Decentralized switch (e.Kind) { case "ready": ps.AddNightLog(L.T("nightlog.bm.ready", name), "99bb66"); break; - case "scam_refund": ps.AddNightLog(L.T("nightlog.bm.scam_refund", e.Order.Seller, name), "cc7755"); break; - case "scam_loss": ps.AddNightLog(L.T("nightlog.bm.scam_loss", e.Order.Seller, name, Fmt.Num(e.Amount)), "cc7755"); break; + case "scam_refund": ps.AddNightLog(L.T("nightlog.bm.scam_refund", DisplayName(e.Order.Seller), name), "cc7755"); break; + case "scam_loss": ps.AddNightLog(L.T("nightlog.bm.scam_loss", DisplayName(e.Order.Seller), name, Fmt.Num(e.Amount)), "cc7755"); break; case "intercept": ps.AddNightLog(L.T("nightlog.bm.intercept", name, Fmt.Num(e.Amount)), "cc7755"); break; case "rug": ps.AddNightLog(L.T("nightlog.bm.rug", name, Fmt.Num(e.Amount)), "cc7755"); break; case "trap": diff --git a/src/Decentralized/CoinCustomers.cs b/src/Decentralized/CoinCustomers.cs index 1ad0d02..3bb2502 100644 --- a/src/Decentralized/CoinCustomers.cs +++ b/src/Decentralized/CoinCustomers.cs @@ -75,6 +75,12 @@ namespace Decentralized Log.Info($"黑市抽样:无法造出或价格为 0 的货 {missing.Count} 个:{string.Join(", ", missing)};基础价中位 {costs[costs.Count / 2]},最高 {costs.Last()}"); Log.Info($"黑市抽样:60 周货架总值 平均 {vals.Average():0},最小 {vals.Min():0},最大 {vals.Max():0}"); })); + ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market all merchants (test)", () => Log.Guard("黑市全部商家(测试)", () => + { + var d = Session.Data; + d.Black.RefreshOrders(StoreStation.GetDayCounter(), StoreReputation.GetBMReputation(), d.Rng, BlackDelivery.Value, true); + Log.Info($"黑市全部商家(测试):{d.Black.Sellers.Count} 家,货架 {d.Black.Catalog.Count} 种,{string.Join("/", d.Black.Sellers.Select(x => x.Type))}"); + })); ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market events (test)", () => Log.Guard("黑市事件(测试)", () => { var d = Session.Data; diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index de84183..1de9efb 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -419,6 +419,8 @@ namespace Decentralized Open(); } + private static string MerchantLabel(BmSeller s) => "[" + L.T("merchant." + s.Type) + "] " + s.Name; + private static int _merchant; private static int _qty = 1; private static readonly int[] Qtys = { 1, 5, 10 }; @@ -442,13 +444,13 @@ namespace Decentralized for (int k = i; k < Math.Min(sellers.Count, i + 3); k++) { int idx = k; - string n = L.T("merchant." + sellers[k].Type) + " " + sellers[k].Name; - b.AddButton(idx == _merchant ? $"[{n}]" : n, () => { _merchant = idx; Open(); }); + string n = MerchantLabel(sellers[k]); + b.AddButton(idx == _merchant ? "> " + n : n, () => { _merchant = idx; Open(); }); } b.End(); } var seller = sellers[_merchant]; - b.AddLabel(L.T("black.seller", seller.Name, seller.Trades, seller.Rating) + " " + L.T("black.days", seller.Days)); + b.AddLabel(L.T("black.seller", MerchantLabel(seller), seller.Trades, seller.Rating) + " " + L.T("black.days", seller.Days)); var goods = d.Black.Catalog.Where(l => l.Seller == _merchant).Cast().ToList(); if (goods.Count == 0) { b.AddLabel(L.T("black.empty")); return; } Shelf(b, goods, (c, g) => @@ -541,13 +543,13 @@ namespace Decentralized if (sale.State == SaleState.Listed) l.AddButton(L.T("black.sell.delist"), () => Delist(sale)); } foreach (var o in d.Black.Orders.OrderByDescending(x => x.PlacedDay)) - l.AddLabel(L.T("black.mine.line", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), o.Seller, L.T("black.mode." + o.Mode), L.T("black.state." + o.State + (o.Why != null && o.State != OrderState.Done ? "." + o.Why : "")), o.DueDay)); + l.AddLabel(L.T("black.mine.line", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), BlackDelivery.DisplayName(o.Seller), L.T("black.mode." + o.Mode), L.T("black.state." + o.State + (o.Why != null && o.State != OrderState.Done ? "." + o.Why : "")), o.DueDay)); }, r => { r.AddHeader(L.T("black.forum.h")); if (d.Black.Forum.Count == 0) r.AddLabel(L.T("black.forum.quiet")); foreach (var f in d.Black.Forum) - r.AddLabel(f.StartsWith("scam:") ? L.T("black.forum.scam", f.Substring(5)) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f)); + r.AddLabel(f.StartsWith("scam:") ? L.T("black.forum.scam", BlackDelivery.DisplayName(f.Substring(5))) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f)); }); }