From e44a4439ed17ce011c2106198ac23f78d1e53293 Mon Sep 17 00:00:00 2001 From: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:06:32 +0800 Subject: [PATCH] Black market in Web3 page, shelf UI, no dead drop, tech and GPU goods; rules moved to Web3 handbook Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 +- src/Decentralized.Domain/BlackMarket.cs | 91 ++++++-- src/Decentralized.Domain/BmOrders.cs | 40 ++-- src/Decentralized.Domain/Tuning.cs | 12 +- src/Decentralized.Domain/UsedDealer.cs | 17 +- src/Decentralized/BlackDelivery.cs | 51 ++--- src/Decentralized/Items.cs | 16 ++ .../ModFiles/Localization/en.csv | 42 ++-- .../ModFiles/Localization/zh.csv | 42 ++-- src/Decentralized/TerminalWindow.cs | 200 ++++++++++-------- src/Decentralized/Unlock.cs | 1 + tests/Decentralized.Domain.Tests/Program.cs | 24 +-- 12 files changed, 296 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 104e00d..a32c298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 第四阶段:Web3 黑市与 Ponero(进行中) -- 人脉网络新增"Web3 黑市"(前置:Web3 治理社区,1500):黑市派系在链上的实体;终端多一页"黑市",需要黑市声望 ≥ 10,镇压期间下线。 +- 人脉网络新增"Web3 黑市"(前置:Web3 治理社区,1500):黑市派系在链上的实体;放在终端 Web3 页的"黑市"子页里,需要黑市声望 ≥ 10,镇压期间下线。 - 现货柜台:每周刷新一批违禁品(游戏已有的 32 种,低 / 中 / 高三档随声望 10 / 40 / 80 放开;件数 4 + 声望/40),价格 = 游戏基础价 × 1.3,只收 PSC / CRT / CRA / Ponero,不收 CRC。付出去的币连同按比例的污点留在卖家那边,货放进储藏区,不带赃物标签。 - Ponero 兑换处(声望 ≥ 30):任意币兑入 Ponero 时这部分价值的污点留在原地,兑入、兑出各收 8%,每天额度 1000 信用点;Ponero 价 = PSC × 0.5。从 Ponero 兑出的币在出金审查里按"刚出混币器"加重。 - 游戏内验证:声望 75 时上架 5 种;用 CRT 买一把左轮 312 点,余额 3000 → 2688,污点 2000 → 1875(按付款占钱包的比例);5 PSC 兑入得 9.2 Ponero(500 × 0.92 / 50),当天额度剩 500。 @@ -12,6 +12,9 @@ - 配送:送货上门 3 天,到货后开店时快递员作为顾客上门,对话结束把货放进储藏区;死投点 2 天,到货后晚上去垃圾场,货进外出背包。上门被截获 8%,死投 3%,死投另有 12% 被别人先挖走;镇压期间到期顺延、截获 +15%。 - 平台跑路:每晚概率 0.2% + 在途款项每万信用点 0.4%(封顶 5%),跑路后在途款项归零、目录与现货清空、入口关闭 10 天。论坛情报:骗子卖家有一半概率被人点名,跑路风险升高时出现提币变慢的帖子。 - 游戏内验证:下单后在途显示在"我的订单";过夜到期,两单到货,一单骗子卖家托管退款(CRT +143,污点 +57);快递员对话走完,货进储藏区,快递员自动离开。没有在游戏里验证:死投点取货(要去垃圾场)、平台跑路与截获的实际触发(只有单元测试)。 +- 商城改成货架式:每行 4 张商品卡(游戏物品贴图、名称、类别、库存或卖家、价格、按钮);删掉死投点,只剩送货上门;货架上除违禁品外偶尔有高科技物品(声望 ≥ 40,加价 50%)和显卡(加价 20%,20% 是假卡)。 +- 界面不放说明文字:银行、交易、混币器、黑市、Ponero 的规则说明都移进新物品"Web3 手册"(购买 Web3 治理社区时一并交付)。 +- 游戏内验证:货架显示贴图与成色,用 CRT 买一张 RTX 60(232)进储藏区。 - 还没做:蜜罐、受控交付(第三批)。 ### 第三阶段:数值调优(进行中) diff --git a/src/Decentralized.Domain/BlackMarket.cs b/src/Decentralized.Domain/BlackMarket.cs index b35185d..2f106cf 100644 --- a/src/Decentralized.Domain/BlackMarket.cs +++ b/src/Decentralized.Domain/BlackMarket.cs @@ -4,18 +4,29 @@ using System.Linq; namespace Decentralized.Domain { - /// 黑市现货柜台上的一件货:游戏物品 id、剩余件数、含溢价的单价(信用点)。 - public sealed class BmOffer + /// 货的种类:违禁品、高科技物品、显卡。 + public enum GoodKind { Contraband, Tech, Gpu } + + /// 黑市货架上的一种货。显卡的 Item 是 "gpu:型号",Model 是标称型号,FakeOf 非空表示背后是别的芯片(买家看不到)。 + public class BmGood { public string Item; - public int Qty; - public int Price; + public GoodKind Kind; public int Level; + public int Price; + public string Model, FakeOf; + public double Health; + } + + /// 现货柜台上的一件货:剩余件数、含溢价的单价(信用点)。 + public sealed class BmOffer : BmGood + { + public int Qty; } /// - /// Web3 黑市:黑市派系在链上的实体。现货柜台每周刷新一批违禁品(档次随黑市声望放开),只收币,脏币也收, - /// 付出去的币连同按比例的污点一起留在卖家那边;Ponero 兑换处把任意币兑成无法追踪的 Ponero(污点留在原地),有每日额度和手续费。 + /// Web3 黑市:黑市派系在链上的实体。货架上主要是违禁品(档次随黑市声望放开),偶尔有高科技物品和显卡(加价), + /// 现货只收币,脏币也收,付出去的币连同按比例的污点一起留在卖家那边;Ponero 兑换处把任意币兑成无法追踪的 Ponero(这部分价值的污点被清除),有每日额度和手续费。 /// 镇压期间整个平台下线。 /// public sealed partial class BlackMarket @@ -26,7 +37,7 @@ namespace Decentralized.Domain public double DeskToday; public int DeskDay = -1; - /// 现货池:游戏物品 id 与违禁等级(1 低、2 中、3 高)。全部来自游戏已有的违禁品。 + /// 违禁品池:游戏物品 id 与违禁等级(1 低、2 中、3 高)。全部来自游戏已有的违禁品。 public static readonly KeyValuePair[] Pool = { P("small_pistol_ammo", 1), P("heavy_pistol_ammo", 1), P("handmade_pistol", 1), P("printed_gun", 1), P("stun_baton", 1), @@ -38,6 +49,13 @@ namespace Decentralized.Domain P("shotgun", 3), P("smg", 3), P("c4", 3), P("fanta_pill", 3), P("nightmare_dust", 3), P("cmd_keycard", 3), P("crypto_module_sec", 3), }; + /// 高科技物品池(不是违禁品,黑市上要加价)。 + public static readonly string[] TechPool = + { + "hand", "aug_chip", "energy_credit_hyp", "energy_credit_breeder", "makeshift_reactor", "mirage_projector", "printer", + "system_module_overclock", "system_module_performance", "system_module_quality", "module_extractor_advanced", "powerblock", + }; + private static KeyValuePair P(string id, int level) => new KeyValuePair(id, level); public static bool Open(int rep) => rep >= Tuning.BmOpenRep; @@ -45,26 +63,55 @@ namespace Decentralized.Domain public static bool DeskOpen(int rep) => rep >= Tuning.PoneroOpenRep; public static int StockCount(int rep) => Tuning.BmStockBase + (int)Math.Max(0, rep / Tuning.BmStockPerRep); - /// 溢价后的单价(信用点)。 - public static int PriceOf(long baseValue) => (int)Math.Max(1, Math.Round(baseValue * (1 + Tuning.BmPremium))); + /// 货的加价:违禁品 0,高科技、显卡各有额外加价(在现货溢价 / 订单折扣之外)。 + public static double Extra(GoodKind k) => k == GoodKind.Tech ? Tuning.BmTechMarkup : k == GoodKind.Gpu ? Tuning.BmGpuMarkup : 0; - /// 每周刷新一次(或首次开放时):按声望开放的档次抽 StockCount 种货,每种 1–3 件。value 给出物品的游戏基础价。 + /// 现货单价(信用点):基础价 ×(1 + 溢价 + 货的加价)。 + public static int PriceOf(long baseValue, GoodKind k = GoodKind.Contraband) => (int)Math.Max(1, Math.Round(baseValue * (1 + Tuning.BmPremium + Extra(k)))); + + /// 抽 count 种货:高科技(中档声望起)和显卡按概率换掉违禁品。value 给出游戏物品的基础价;显卡的基础价按标称型号与成色算。 + private List Roll(int count, int day, int rep, Rng rng, Func value, Func price) + { + var goods = new List(); + var pool = Pool.Where(x => x.Value <= MaxLevel(rep)).ToList(); + var tech = TechPool.ToList(); + var gpus = UsedDealers.Pool(day); + for (int i = 0; i < count; i++) + { + double r = rng.NextDouble(); + if (r < Tuning.BmGpuChance) + { + var c = UsedDealers.Card(gpus, Tuning.BmGpuFake, 0, rng); + goods.Add(new BmGood { Item = "gpu:" + c.Model, Kind = GoodKind.Gpu, Level = 2, Model = c.Model, FakeOf = c.FakeOf, Health = c.Health, Price = price(CardValue.Of(c.Model, c.Health), GoodKind.Gpu) }); + } + else if (r < Tuning.BmGpuChance + Tuning.BmTechChance && rep >= Tuning.BmMidRep && tech.Count > 0) + { + int k = rng.Range(0, tech.Count - 1); + string id = tech[k]; + tech.RemoveAt(k); + long v = value(id); + if (v > 0) goods.Add(new BmGood { Item = id, Kind = GoodKind.Tech, Level = 3, Price = price(v, GoodKind.Tech) }); + } + else if (pool.Count > 0) + { + int k = rng.Range(0, pool.Count - 1); + var pick = pool[k]; + pool.RemoveAt(k); + long v = value(pick.Key); + if (v > 0) goods.Add(new BmGood { Item = pick.Key, Kind = GoodKind.Contraband, Level = pick.Value, Price = price(v, GoodKind.Contraband) }); + } + } + return goods; + } + + /// 每周刷新一次(或首次开放时):按声望开放的档次抽 StockCount 种货,每种 1–3 件(高档与显卡、高科技 1 件)。 public bool Refresh(int day, int rep, Rng rng, Func value) { if (!Open(rep) || day - RefreshedDay < Tuning.BmRefreshDays) return false; RefreshedDay = day; Offers.Clear(); - var pool = Pool.Where(x => x.Value <= MaxLevel(rep)).ToList(); - int n = Math.Min(StockCount(rep), pool.Count); - for (int i = 0; i < n; i++) - { - int k = rng.Range(0, pool.Count - 1); - var pick = pool[k]; - pool.RemoveAt(k); - long v = value(pick.Key); - if (v <= 0) continue; - Offers.Add(new BmOffer { Item = pick.Key, Level = pick.Value, Qty = pick.Value == 3 ? 1 : rng.Range(1, 3), Price = PriceOf(v) }); - } + foreach (var g in Roll(StockCount(rep), day, rep, rng, value, PriceOf)) + Offers.Add(new BmOffer { Item = g.Item, Kind = g.Kind, Level = g.Level, Price = g.Price, Model = g.Model, FakeOf = g.FakeOf, Health = g.Health, Qty = g.Kind != GoodKind.Contraband || g.Level == 3 ? 1 : rng.Range(1, 3) }); return true; } @@ -91,7 +138,7 @@ namespace Decentralized.Domain public double DeskLeft(int day) => Math.Max(0, Tuning.PoneroDailyCap - (DeskDay == day ? DeskToday : 0)); - /// 兑入 Ponero:from 币 amount 个,扣手续费后按 Ponero 价到账;这部分价值带的污点留在原地(不跟着 Ponero)。返回到账数量,额度或余额不足返回 -1。 + /// 兑入 Ponero:from 币 amount 个,扣手续费后按 Ponero 价到账;这部分价值带的污点被清除(不跟着 Ponero)。返回到账数量,额度或余额不足返回 -1。 public double ToPonero(Wallet w, string from, double amount, int day, Func price) { double value = amount * price(from); diff --git a/src/Decentralized.Domain/BmOrders.cs b/src/Decentralized.Domain/BmOrders.cs index b717122..530c33d 100644 --- a/src/Decentralized.Domain/BmOrders.cs +++ b/src/Decentralized.Domain/BmOrders.cs @@ -5,7 +5,6 @@ using System.Linq; namespace Decentralized.Domain { public enum PayMode { Escrow, Advance } - public enum Delivery { Door, Drop } public enum OrderState { Pending, Ready, Done, Refunded, Lost } /// 黑市上的卖家:成交数与好评率公开;是不是骗子是隐藏的。 @@ -20,12 +19,9 @@ namespace Decentralized.Domain } /// 订单目录里的一件货:卖家、货、价格(订单价,已折扣)。 - public sealed class BmListing + public sealed class BmListing : BmGood { public int Seller; - public string Item; - public int Level; - public int Price; } public sealed class BmOrder @@ -34,7 +30,8 @@ namespace Decentralized.Domain public string Seller; public bool SellerScam; public PayMode Mode; - public Delivery Delivery; + public string Model, FakeOf; + public double Health; public int Price; public string Asset; public double Qty; @@ -49,7 +46,7 @@ namespace Decentralized.Domain /// 夜间发生的订单事件,供日志与新闻使用。 public sealed class BmEvent { - public string Kind; // ready / scam_refund / scam_loss / rug / intercept / theft / reopen + public string Kind; // ready / scam_refund / scam_loss / rug / intercept / reopen public BmOrder Order; public double Amount; } @@ -71,16 +68,15 @@ namespace Decentralized.Domain /// 托管中和已提前放款但尚未交付的资金(信用点),平台跑路的概率随它增加。 public double Held => Orders.Where(o => o.State == OrderState.Pending).Sum(o => (double)o.Price); - public static int OrderPrice(long baseValue) => (int)Math.Max(1, Math.Round(baseValue * (1 - Tuning.BmOrderDiscount))); + public static int OrderPrice(long baseValue, GoodKind k = GoodKind.Contraband) => (int)Math.Max(1, Math.Round(baseValue * (1 + Extra(k)) * (1 - Tuning.BmOrderDiscount))); public static int EscrowFee(int price) => (int)Math.Ceiling(price * Tuning.BmEscrowFee); /// 下单实付(信用点):托管 = 订单价 + 托管费;提前放款 = 订单价打折。 public static int Pay(int price, PayMode m) => m == PayMode.Escrow ? price + EscrowFee(price) : (int)Math.Round(price * (1 - Tuning.BmAdvanceDiscount)); - public static int Days(Delivery d) => d == Delivery.Door ? Tuning.BmDoorDays : Tuning.BmDropDays; public double RugChance => Math.Min(Tuning.BmRugMax, Tuning.BmRugBase + Held / 10000.0 * Tuning.BmRugPerTenK); /// 每周重新生成卖家、目录和论坛情报(与现货同一天刷新)。 - public void RefreshOrders(int rep, Rng rng, Func value) + public void RefreshOrders(int day, int rep, Rng rng, Func value) { Sellers.Clear(); Catalog.Clear(); Forum.Clear(); for (int i = 0; i < Tuning.BmSellerCount; i++) @@ -98,21 +94,14 @@ namespace Decentralized.Domain Sellers.Add(s); if (scam && rng.Chance(0.5)) Forum.Add("scam:" + s.Name); } - var pool = Pool.Where(x => x.Value <= MaxLevel(rep)).ToList(); - for (int i = 0; i < Tuning.BmSellerCount * 2 && pool.Count > 0; i++) - { - int k = rng.Range(0, pool.Count - 1); - var pick = pool[k]; - pool.RemoveAt(k); - long v = value(pick.Key); - if (v <= 0) continue; - Catalog.Add(new BmListing { Seller = i % Sellers.Count, Item = pick.Key, Level = pick.Value, Price = OrderPrice(v) }); - } + int n = 0; + foreach (var g in Roll(Tuning.BmSellerCount * 2, day, rep, rng, value, OrderPrice)) + Catalog.Add(new BmListing { Seller = n++ % Sellers.Count, Item = g.Item, Kind = g.Kind, Level = g.Level, Price = g.Price, Model = g.Model, FakeOf = g.FakeOf, Health = g.Health }); if (RugChance > Tuning.BmRugBase * 3) Forum.Add("rug"); } /// 下单:钱包里的币按实付折算扣除,污点与成本按比例留在卖家那边(记在订单上)。成功返回订单。 - public BmOrder Place(BmListing l, PayMode mode, Delivery del, Wallet w, string asset, int day, Func price) + public BmOrder Place(BmListing l, PayMode mode, Wallet w, string asset, int day, Func price) { if (l == null || asset == Assets.CRC || Closed(day)) return null; var seller = Sellers[l.Seller]; @@ -129,8 +118,8 @@ namespace Decentralized.Domain w.Add(asset, -qty); var o = new BmOrder { - Item = l.Item, Seller = seller.Name, SellerScam = seller.Scam, Mode = mode, Delivery = del, Price = credits, Asset = asset, Qty = qty, - Taint = taint, Basis = basis, PlacedDay = day, DueDay = day + Days(del), + Item = l.Item, Seller = seller.Name, SellerScam = seller.Scam, Mode = mode, Model = l.Model, FakeOf = l.FakeOf, Health = l.Health, Price = credits, Asset = asset, Qty = qty, + Taint = taint, Basis = basis, PlacedDay = day, DueDay = day + Tuning.BmDoorDays, }; Orders.Add(o); return o; @@ -168,9 +157,8 @@ namespace Decentralized.Domain else { o.State = OrderState.Lost; o.Why = "scam"; ev.Add(new BmEvent { Kind = "scam_loss", Order = o, Amount = o.Price }); } continue; } - double ic = (o.Delivery == Delivery.Door ? Tuning.BmInterceptDoor : Tuning.BmInterceptDrop) + (crackdown ? Tuning.BmInterceptCrackdown : 0); + double ic = Tuning.BmInterceptDoor + (crackdown ? Tuning.BmInterceptCrackdown : 0); if (rng.Chance(ic)) { o.State = OrderState.Lost; o.Why = "intercept"; ev.Add(new BmEvent { Kind = "intercept", Order = o, Amount = o.Price }); continue; } - if (o.Delivery == Delivery.Drop && rng.Chance(Tuning.BmDropTheft)) { o.State = OrderState.Lost; o.Why = "theft"; ev.Add(new BmEvent { Kind = "theft", Order = o, Amount = o.Price }); continue; } o.State = OrderState.Ready; ev.Add(new BmEvent { Kind = "ready", Order = o }); } @@ -180,6 +168,6 @@ namespace Decentralized.Domain public void Deliver(BmOrder o) { o.State = OrderState.Done; o.Why = "done"; } - public IEnumerable ReadyFor(Delivery d) => Orders.Where(o => o.State == OrderState.Ready && o.Delivery == d); + public IEnumerable Ready => Orders.Where(o => o.State == OrderState.Ready); } } diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 3439295..d9c985b 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -89,11 +89,13 @@ namespace Decentralized.Domain /// Ponero 兑换处:开放所需黑市声望、每次兑入 / 兑出的手续费、每天可兑换的额度(信用点)、对 PSC 的价格比。 public const int PoneroOpenRep = 30; public const double PoneroFee = 0.08, PoneroDailyCap = 1000, PoneroPscRatio = 0.5; - /// 黑市订单:订单价相对游戏基础价的折扣、托管费、提前放款再折扣、提前放款所需卖家成交数、上门 / 死投的天数、 - /// 上门 / 死投被截获的概率(镇压期间再加)、死投被别人先挖走的概率、骗子卖家占比、每晚平台跑路基础概率与每万信用点托管资金加的概率(封顶)、跑路后关闭的天数。 - public const double BmOrderDiscount = 0.15, BmEscrowFee = 0.05, BmAdvanceDiscount = 0.10, BmInterceptDoor = 0.08, BmInterceptDrop = 0.03, BmInterceptCrackdown = 0.15; - public const double BmDropTheft = 0.12, BmScamSellerShare = 0.15, BmRugBase = 0.002, BmRugPerTenK = 0.004, BmRugMax = 0.05; - public const int BmAdvanceMinTrades = 30, BmDoorDays = 3, BmDropDays = 2, BmRugClosedDays = 10, BmSellerCount = 4; + /// 黑市订单:订单价相对游戏基础价的折扣、托管费、提前放款再折扣、提前放款所需卖家成交数、上门的天数、 + /// 上门被截获的概率(镇压期间再加)、骗子卖家占比、每晚平台跑路基础概率与每万信用点托管资金加的概率(封顶)、跑路后关闭的天数。 + public const double BmOrderDiscount = 0.15, BmEscrowFee = 0.05, BmAdvanceDiscount = 0.10, BmInterceptDoor = 0.08, BmInterceptCrackdown = 0.15; + public const double BmScamSellerShare = 0.15, BmRugBase = 0.002, BmRugPerTenK = 0.004, BmRugMax = 0.05; + public const int BmAdvanceMinTrades = 30, BmDoorDays = 3, BmRugClosedDays = 10, BmSellerCount = 4; + /// 货架上除违禁品外的两类货:高科技物品(需中档声望)和显卡的出现概率、加价(在现货溢价 / 订单价之外再加),显卡里夹假卡的比例。 + public const double BmTechChance = 0.15, BmGpuChance = 0.20, BmTechMarkup = 0.5, BmGpuMarkup = 0.2, BmGpuFake = 0.20; /// 黑市人脉升级的价格。 public const int BmUnlockPrice = 1500; /// 单个金融违规事件记入的罪证上限。 diff --git a/src/Decentralized.Domain/UsedDealer.cs b/src/Decentralized.Domain/UsedDealer.cs index 07b1abd..0b05a41 100644 --- a/src/Decentralized.Domain/UsedDealer.cs +++ b/src/Decentralized.Domain/UsedDealer.cs @@ -38,6 +38,16 @@ namespace Decentralized.Domain return list.ToArray(); } + /// 从池里抽一张:随机型号与成色;按 fakeShare 的概率是假卡(背后是便宜得多的真实芯片,废片只给低档标称)。 + public static UsedCard Card(string[] pool, double fakeShare, int markupPct, Rng rng) + { + var label = pool[rng.Range(0, pool.Length - 1)]; + var card = new UsedCard { Model = label, Health = 25 + rng.NextDouble() * 60, MarkupPct = markupPct }; + var cheaper = Hardware.Specs.Values.Where(s => !s.Asic && s.Price * 2 <= Hardware.Specs[label].Price && (!s.Scrap || Hardware.Specs[label].Tier == 0)).Select(s => s.Model).ToArray(); + if (cheaper.Length > 0 && rng.Chance(fakeShare)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)]; + return card; + } + public static List Cards(DealerKind kind, int day, Rng rng) { var pool = Pool(day); @@ -46,12 +56,7 @@ namespace Decentralized.Domain 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 && (!s.Scrap || Hardware.Specs[label].Tier == 0)).Select(s => s.Model).ToArray(); - if (cheaper.Length > 0 && rng.Chance(share)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)]; - card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0; - cards.Add(card); + cards.Add(Card(pool, share, kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0, rng)); } return cards; } diff --git a/src/Decentralized/BlackDelivery.cs b/src/Decentralized/BlackDelivery.cs index f7ea4a4..34f8e7d 100644 --- a/src/Decentralized/BlackDelivery.cs +++ b/src/Decentralized/BlackDelivery.cs @@ -5,20 +5,18 @@ namespace Decentralized { /// /// Web3 黑市的货物进出:每周刷新现货与订单目录,夜间推进订单并写夜间日志, - /// 上门(快递员当顾客上门)和死投(去垃圾场取货)把到货交给玩家。 + /// 上门(快递员当顾客上门)把到货交给玩家。 /// public static class BlackDelivery { public static void Subscribe() { ModHook.OnGenerateCustomerNormal += OnGenerate; - ModHook.OnAfterhourArriveLate += OnArrive; } public static void Unsubscribe() { ModHook.OnGenerateCustomerNormal -= OnGenerate; - ModHook.OnAfterhourArriveLate -= OnArrive; } public static long Value(string id) @@ -30,9 +28,19 @@ namespace Decentralized return v; } + /// 货架上的货对应的游戏物品 id:显卡是 "gpu:型号",对应本 mod 的显卡物品。 + public static string GameId(string id) => id.StartsWith("gpu:") ? Items.GpuId(id.Substring(4)) : id; + + /// 造出这件货:显卡按成色与(可能的)假卡真身造,不带卖家的说法;其余直接造。 + public static GameItem Make(string id, string model, string fakeOf, double health) + { + if (id.StartsWith("gpu:")) return Items.NewCard(model, health, fakeOf, Session.Data.Rng); + return ModHelper.SpawnItem(id); + } + public static string ItemName(string id) { - var it = ModHelper.SpawnItem(id); + var it = ModHelper.SpawnItem(GameId(id)); if (it == null) return id; string n = it.name; it.Destroy(); @@ -46,7 +54,7 @@ namespace Decentralized if (d.Black.Closed(day)) return false; if (force) d.Black.RefreshedDay = -999; if (!d.Black.Refresh(day, rep, d.Rng, Value)) return false; - d.Black.RefreshOrders(rep, d.Rng, Value); + d.Black.RefreshOrders(day, rep, d.Rng, Value); return true; } @@ -56,14 +64,13 @@ namespace Decentralized foreach (var e in d.Black.Night(day, crackdown, d.Hot, d.Rng)) { string name = e.Order != null ? ItemName(e.Order.Item) : null; - Log.Info($"黑市订单事件:{e.Kind} {e.Order?.Item} {e.Order?.Seller} {e.Order?.Mode}/{e.Order?.Delivery} 金额 {e.Amount:0}"); + Log.Info($"黑市订单事件:{e.Kind} {e.Order?.Item} {e.Order?.Seller} {e.Order?.Mode} 金额 {e.Amount:0}"); switch (e.Kind) { - case "ready": ps.AddNightLog(L.T("nightlog.bm.ready." + e.Order.Delivery, name), "99bb66"); break; + 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 "intercept": ps.AddNightLog(L.T("nightlog.bm.intercept", name, Fmt.Num(e.Amount)), "cc7755"); break; - case "theft": ps.AddNightLog(L.T("nightlog.bm.theft", name, Fmt.Num(e.Amount)), "cc7755"); break; case "rug": ps.AddNightLog(L.T("nightlog.bm.rug", name, Fmt.Num(e.Amount)), "cc7755"); break; case "reopen": ps.AddNightLog(L.T("nightlog.bm.reopen"), "99bb66"); break; } @@ -83,7 +90,7 @@ namespace Decentralized Log.Guard("黑市快递员", () => { int day = StoreStation.GetDayCounter(); - if (day == courierDay || !Session.Data.Black.ReadyFor(Delivery.Door).Any()) return; + if (day == courierDay || !Session.Data.Black.Ready.Any()) return; courierDay = day; manager.AddClient(CreateCourier()); }); @@ -110,33 +117,13 @@ namespace Decentralized private static void DeliverDoor() { var d = Session.Data; - foreach (var o in d.Black.ReadyFor(Delivery.Door).ToList()) + foreach (var o in d.Black.Ready.ToList()) { - Items.Give(ModHelper.SpawnItem(o.Item)); + Items.Give(Make(o.Item, o.Model, o.FakeOf, o.Health)); d.Black.Deliver(o); - Log.Info($"黑市快递:{o.Item} 已放进储藏区"); + Log.Info($"黑市快递:{o.Item}{(o.FakeOf != null ? "(假卡,真身 " + o.FakeOf + ")" : "")} 已放进储藏区"); } Session.Save(); } - - // ---- 死投:晚上去垃圾场,货在那里 ---- - private static void OnArrive(string locationId) - { - if (locationId != "dumping_ground" || Session.Data == null || !Unlock.BlackUnlocked) return; - Log.Guard("黑市死投", () => - { - var d = Session.Data; - var list = d.Black.ReadyFor(Delivery.Drop).ToList(); - foreach (var o in list) - { - var item = ModHelper.SpawnItem(o.Item); - GeneralHelper.SetItemOwned(item, true); - GraphUtils.EmporiumTryAdd(EmporiumEntry.Instance.afterhourInventory, item); - d.Black.Deliver(o); - Log.Info($"黑市死投:{o.Item} 已放进外出背包"); - } - if (list.Count > 0) { Session.Save(); } - }); - } } } diff --git a/src/Decentralized/Items.cs b/src/Decentralized/Items.cs index 0177790..6624996 100644 --- a/src/Decentralized/Items.cs +++ b/src/Decentralized/Items.cs @@ -16,6 +16,7 @@ namespace Decentralized public const string Terminal = "pd_terminal"; public const string Rig = "pd_rig"; public const string Guide = "pd_gpu_guide"; + public const string Web3Guide = "pd_web3_guide"; public const string GpuPrefix = "pd_gpu_"; public const string HealthTag = "PD_HEALTH"; @@ -38,6 +39,7 @@ namespace Decentralized dir.Add(Terminal, CreateTerminal); dir.Add(Rig, CreateRig); dir.Add(Guide, CreateGuide); + dir.Add(Web3Guide, CreateWeb3Guide); foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap)) { var s = spec; @@ -77,6 +79,20 @@ namespace Decentralized return item; } + /// Web3 手册:银行、交易、混币器、黑市与 Ponero 的规则。界面上不放说明,规则都在这里。 + private static GameItem CreateWeb3Guide() + { + var item = ItemDirectory.CreateEmptyItem(Web3Guide); + item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_guide"); + item.name = L.T("item.web3guide.name"); + item.shortDescription = L.T("item.web3guide.desc"); + item.unitValue = 0; + item.EnableTag(GeneralHelper.IMPORTANT_TAG); + item.EnableTag(TypeHelper.NOT_FOR_RESALE); + ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name); + return item; + } + // ---------- 显卡 ---------- /// RTX70 → "RTX 70",RX800 → "RX 800"。 diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 10c4354..cbee266 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -164,8 +164,6 @@ bank.ops,"Deposit / withdraw" poin.account.h,"Poinbase account" trade.h,"Trade" web3.wallet.h,"On-chain wallet" -trade.hint,"Buying pays in the quote coin; |selling sells the traded coin." -bank.hint,"Deposit turns credits into CRC; |withdrawing turns CRC back into credits |and passes compliance review." col.peg,"Price" col.status,"Status" stable.ok,"Pegged" @@ -184,7 +182,6 @@ mixer.col.taint,Taint mixer.col.ready,Ready mixer.day,Day {0} mixer.deposit,Deposit -mixer.hint,"Pick asset, amount and rounds.|Each round: fee, less taint.|Rounds = days of waiting.|Coins return to this address.|Bigger deposits clean worse." mixer.balance,Balance: {1} {0} mixer.rounds,Rounds mixer.preview,"Expect {0} {1} back, taint down about {2}%, ready on day {3}." @@ -206,23 +203,20 @@ nightlog.crt_freeze,The black market handed over addresses during the crackdown: nightlog.thaw,Frozen {0} {1} thawed. news.crtfreeze.title,Black market freezes some CRT addresses for the crackdown news.crtfreeze.desc,"Word is the black market gave security a list of CRT addresses in exchange for being left alone. Holders with low standing are likelier to be frozen." -tab.black,Black market black.upgrade.title,Web3 Black Market -black.upgrade.desc,"Unlocks the terminal's black-market page: the black-market faction's on-chain arm. A spot counter restocks weekly with contraband, payable only in coin (PSC, CRT, CRA, Ponero), tainted coin accepted, at a premium; higher standing also opens a Ponero desk. Needs black-market standing; offline during crackdowns." +black.upgrade.desc,"Unlocks the black market in the Web3 page: the black-market faction's on-chain arm. Weekly shelves of contraband, occasionally high-tech goods and GPUs (at a markup); buy spot, or order and wait for a courier. Coin only (PSC, CRT, CRA, Ponero), tainted coin accepted. Needs black-market standing; offline during crackdowns." black.offline,The black market's on-chain entrance is offline during the crackdown. black.closed,"You need black-market standing of {0} before anyone lets you in (currently {1})." black.sub.shop,Spot counter black.sub.desk,Ponero desk black.shop.h,Spot counter -black.rep,"Black-market standing {0}, goods tier {1}/3 visible. Restocks weekly." +black.rep,"Black-market standing {0}" black.empty,Sold out for this week. black.offer,"{0} x{1} {2} credits" -black.buy,"Buy with coin ({0})" +black.buy,Buy black.bought,"Deal done: {0} is in storage." black.pay.h,Payment -black.hint,"Coin only, no CRC.|Prices are 30% above normal channels.|The coin you pay leaves its share of taint with the seller; the goods come without a stolen tag." black.desk.h,Ponero desk -black.desk.hint,"Ponero is a privacy coin; it cannot be traced on-chain.|Swapping any coin into Ponero leaves that value's taint behind.|{0}% fee each way, with a daily limit.|Coin swapped out of Ponero has no origin; cash-out screening gets stricter." black.desk.left,"Left today: {0} (limit {1})" black.desk.price,"Ponero price: {0}" black.desk.swap,Swap @@ -233,23 +227,15 @@ black.desk.fail,Swap failed: balance, limit or amount is wrong. black.sub.orders,Orders black.sub.mine,My orders & intel black.rugged,"The platform is down; nobody will reopen for {0} more days." -black.seller,"Seller {0} trades {1} rating {2}%" +black.seller,"{0}, {1} trades, {2}%" black.orders.h,Order catalog -black.listing,"{0} pay {1} credits" -black.order,Order -black.noadvance,This seller does not take advance release -black.orders.hint,"Cheaper than spot, but it takes days|and things can go wrong.|Escrow: {0}% fee, released on delivery;|refunded if the seller runs.|Advance: {1}% off, but a scammer keeps it;|only sellers with >= {2} trades accept.|The platform can rug too; escrow and|advance money both sit with it." +black.order,"Order ({0} days)" black.mode.Escrow,Escrow black.mode.Advance,Advance release -black.del.Door,Door delivery -black.del.Drop,Dead drop -black.del.hint.Door,"A courier comes by in {0} days (as a customer when you open); higher interception chance." -black.del.hint.Drop,"Left at the dumping ground in {0} days, pick it up at night; low interception chance, but someone may dig it up first." black.ordered,"Ordered: {0}, due day {1}." black.mine.h,My orders black.mine.none,No orders yet. -black.mine.line,"{0} ({1}, {2}): {3}, day {4}{5}" -black.mine.drop,", pick up at the dumping ground" +black.mine.line,"{0} ({1}, {2}): {3}, day {4}" black.state.Pending,in transit black.state.Ready,arrived black.state.Done,picked up @@ -257,17 +243,13 @@ black.state.Refunded.scam,seller ran, refunded black.state.Lost.scam,seller ran, money gone black.state.Lost.rug,platform rugged, money gone black.state.Lost.intercept,intercepted -black.state.Lost.theft,dug up by someone else black.forum.h,Forum intel black.forum.quiet,Quiet lately. black.forum.scam,"Someone says {0} takes the money and never ships." black.forum.rug,"Withdrawals have been slow lately, the site is running promos, and a lot of complaint threads got deleted." -nightlog.bm.ready.Door,"Black market: {0} arrived; a courier will come by once you open." -nightlog.bm.ready.Drop,"Black market: {0} is at the dumping ground, go get it at night." nightlog.bm.scam_refund,"Black market: seller {0} ran; the escrow for {1} was refunded." nightlog.bm.scam_loss,"Black market: seller {0} took the advance and ran; {1} is gone ({2} credits)." nightlog.bm.intercept,"Black market: the parcel with {0} was intercepted; {1} credits of payment lost." -nightlog.bm.theft,"Black market: {0} at the dumping ground was dug up by someone else; {1} credits lost." nightlog.bm.rug,"Black market: the platform rugged; the in-transit funds for {0} ({1}) are gone." nightlog.bm.reopen,Black market: someone reopened at the old spot. news.bmrug.title,Black-market platform rugged @@ -277,3 +259,15 @@ courier.line1.1,"Delivery's here." courier.line1.2,"Package for you." courier.line2.1,"Don't tell me what's inside. I don't want to know." courier.line2.2,"Dropped it here. I'm gone." +web3.sub.black,Black market +black.stock,"{0} left" +black.price,"{0} credits" +black.noadvance,No advance release +black.kind.gpu,"GPU condition {0}%" +black.kind.Contraband.1,Contraband (low) +black.kind.Contraband.2,Contraband (mid) +black.kind.Contraband.3,Contraband (high) +black.kind.Tech,High-tech (markup) +nightlog.bm.ready,"Black market: {0} arrived; a courier will come by once you open." +item.web3guide.name,Web3 Handbook +item.web3guide.desc,"Bank: depositing turns credits into CRC; withdrawing turns CRC back into credits and goes through compliance screening. Trading: buys are paid in the quote coin, sells in the traded coin. Mixer: pick a coin, amount and rounds; each round takes a fee and lowers taint, rounds equal days of waiting, and the coins come back to the same address; the more you deposit, the worse the cleaning. Black market: coin only, no CRC, 30% above normal channels, with extra markup on high-tech goods and GPUs; the coin you pay leaves its share of taint with the seller and the goods carry no stolen tag; GPUs may be fake, inspect them yourself. Orders are cheaper than spot but take days and may be intercepted; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug. Ponero desk: Ponero is a privacy coin and swapping in wipes the taint on that value; 8% fee each way with a daily limit; coin swapped out of Ponero has no origin and cash-out screening gets stricter." diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 873d242..1ffe0bc 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -163,8 +163,6 @@ bank.ops,"存取" poin.account.h,"Poinbase 账户" trade.h,"交易" web3.wallet.h,"链上钱包" -trade.hint,"买入按计价币的数量支付;|卖出按所交易币的数量卖出。" -bank.hint,"存入把信用点换成 CRC;|提取把 CRC 换回信用点,|提取要过合规审查。" col.peg,"价格" col.status,"状态" stable.ok,"锚定正常" @@ -183,7 +181,6 @@ mixer.col.taint,污点 mixer.col.ready,到期 mixer.day,第 {0} 天 mixer.deposit,存入 -mixer.hint,"选币种、填数量和轮数。|每轮扣手续费并降低污点。|轮数即等待天数。|到期提回同一个地址。|存入越多,清洗越差。" mixer.balance,余额:{1} {0} mixer.rounds,轮数 mixer.preview,"预计提回 {0} {1},污点约降低 {2}%,第 {3} 天到期。" @@ -205,23 +202,20 @@ nightlog.crt_freeze,镇压期间黑市交出了一批地址:你的 {0} CRT 被 nightlog.thaw,冻结的 {0} {1} 已解冻。 news.crtfreeze.title,黑市配合镇压冻结部分 CRT 地址 news.crtfreeze.desc,"传闻黑市为换取放行,向安保交出了一批 CRT 地址。声望低的持有者更容易被冻结。" -tab.black,黑市 black.upgrade.title,Web3 黑市 -black.upgrade.desc,"解锁终端的黑市页:黑市派系在链上的实体。现货柜台每周上一批违禁品,只收币(PSC、CRT、CRA、Ponero),脏币也收,价格比正常渠道高;声望够高还有 Ponero 兑换处。需要黑市声望,镇压期间下线。" +black.upgrade.desc,"解锁 Web3 页里的黑市:黑市派系在链上的实体。货架上每周上一批违禁品,偶尔有高科技物品和显卡(要加价);可以直接买现货,也可以下订单等快递员上门。只收币(PSC、CRT、CRA、Ponero),脏币也收。需要黑市声望,镇压期间下线。" black.offline,镇压期间,黑市的链上入口已经下线。 black.closed,"黑市声望达到 {0} 才会有人带你进去(现在 {1})。" black.sub.shop,现货柜台 black.sub.desk,Ponero 兑换处 black.shop.h,现货柜台 -black.rep,"黑市声望 {0},可见货物档次 {1}/3。每周刷新。" +black.rep,"黑市声望 {0}" black.empty,这周的货卖完了。 black.offer,"{0} ×{1} {2} 信用点等值" -black.buy,"用币购买({0})" +black.buy,购买 black.bought,"成交:{0} 已放进储藏区。" black.pay.h,付款 -black.hint,"只收币,不收 CRC。|价格比正常渠道高 30%。|付出去的币带着按比例的污点一起留在卖家那边,拿到手的货不带赃物标签。" black.desk.h,Ponero 兑换处 -black.desk.hint,"Ponero 是隐私币,链上无法追踪。|任何币兑入 Ponero 时,这部分价值的污点留在原地。|兑入、兑出各收 {0}% 手续费,每天有额度。|从 Ponero 兑出的币来路不明,出金审查会加重。" black.desk.left,"今天还能兑换 {0}(上限 {1})" black.desk.price,"Ponero 价格:{0}" black.desk.swap,兑换 @@ -232,23 +226,15 @@ black.desk.fail,兑换失败:余额、额度或数量不对。 black.sub.orders,订单 black.sub.mine,我的订单与情报 black.rugged,"平台出事了,入口关闭,还要 {0} 天才会有人重新开张。" -black.seller,"卖家 {0} 成交 {1} 好评 {2}%" +black.seller,"{0},成交 {1},好评 {2}%" black.orders.h,订单目录 -black.listing,"{0} 实付 {1} 信用点等值" -black.order,下单 -black.noadvance,这个卖家不接受提前放款 -black.orders.hint,"比现货便宜,但要等几天,|路上可能出事。|托管:多付 {0}% 托管费,|收货后放款,卖家跑路时退款。|提前放款:再打 {1}% 折,|卖家若是骗子钱就没了;|成交 ≥ {2} 的卖家才接受。|平台自己也可能跑路,|托管和提前放款的钱都在它手里。" +black.order,"下单({0} 天到)" black.mode.Escrow,托管 black.mode.Advance,提前放款 -black.del.Door,"送货上门" -black.del.Drop,死投点 -black.del.hint.Door,"{0} 天后快递员上门(开店时来个顾客),被截获的概率较高。" -black.del.hint.Drop,"{0} 天后货放在垃圾场,晚上去取;被截获的概率低,但可能被别人先挖走。" black.ordered,"已下单:{0},预计第 {1} 天到。" black.mine.h,我的订单 black.mine.none,还没有订单。 -black.mine.line,"{0}({1},{2}):{3},第 {4} 天{5}" -black.mine.drop,,去垃圾场取 +black.mine.line,"{0}({1},{2}):{3},第 {4} 天" black.state.Pending,在途 black.state.Ready,到货 black.state.Done,已取 @@ -256,17 +242,13 @@ black.state.Refunded.scam,卖家跑路,已退款 black.state.Lost.scam,卖家跑路,钱没了 black.state.Lost.rug,平台跑路,钱没了 black.state.Lost.intercept,被截获 -black.state.Lost.theft,被别人先挖走 black.forum.h,论坛情报 black.forum.quiet,最近很安静。 black.forum.scam,"有人说 {0} 收了钱不发货。" black.forum.rug,"最近提币变慢了,全场还在搞促销,投诉帖被删了不少。" -nightlog.bm.ready.Door,"黑市:{0} 到货,开店后会有快递员上门。" -nightlog.bm.ready.Drop,"黑市:{0} 已放在垃圾场,晚上去取。" nightlog.bm.scam_refund,"黑市:卖家 {0} 跑路了,{1} 的托管款已退回。" nightlog.bm.scam_loss,"黑市:卖家 {0} 拿了提前放款的钱跑路,{1} 没有了({2} 信用点等值)。" nightlog.bm.intercept,"黑市:{0} 的包裹被截获,{1} 信用点等值的货款没了。" -nightlog.bm.theft,"黑市:垃圾场的 {0} 被别人先挖走了,{1} 信用点等值的货款没了。" nightlog.bm.rug,"黑市:平台跑路,{0} 的在途款项({1})归零。" nightlog.bm.reopen,黑市:有人在老地方重新开张了。 news.bmrug.title,黑市平台跑路 @@ -276,3 +258,15 @@ courier.line1.1,"东西到了。" courier.line1.2,"有你的包裹。" courier.line2.1,"别问我里面是什么,我也不想知道。" courier.line2.2,"货放这儿了,我走了。" +web3.sub.black,黑市 +black.stock,"剩 {0} 件" +black.price,"{0} 信用点等值" +black.noadvance,不接受提前放款 +black.kind.gpu,"显卡 成色 {0}%" +black.kind.Contraband.1,违禁品(低) +black.kind.Contraband.2,违禁品(中) +black.kind.Contraband.3,违禁品(高) +black.kind.Tech,高科技物品(加价) +nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。" +item.web3guide.name,Web3 手册 +item.web3guide.desc,"银行:存入把信用点换成 CRC;提取把 CRC 换回信用点,提取要过合规审查。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路。Ponero 兑换处:Ponero 是隐私币,兑入时这部分价值的污点被清除;兑入、兑出各收 8% 手续费,每天有额度;从 Ponero 兑出的币来路不明,出金审查会加重。" diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 29f274e..caac440 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -13,7 +13,7 @@ namespace Decentralized public static class TerminalWindow { public const string Id = "pd_terminal"; - private enum Page { Home, Bank, Poinbase, Web3, Black, Report } + private enum Page { Home, Bank, Poinbase, Web3, Report } private static Page _page = Page.Home; private static string _amount = ""; @@ -24,25 +24,23 @@ namespace Decentralized if (Session.Data == null) return; var b = CustomUIManager.Window(Id, L.T("term.title")); if (b == null) return; - b.SetSize(780, 580).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle(); + b.SetSize(780, 660).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle(); b.BeginRow(); foreach (Page p in Enum.GetValues(typeof(Page))) { - if (p == Page.Black && !Unlock.BlackUnlocked) continue; var page = p; string name = L.T("tab." + p.ToString().ToLowerInvariant()); b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; Open(); }); } b.End(); b.AddSeparator(); - b.BeginScroll(450); + b.BeginScroll(540); switch (_page) { case Page.Home: HomePage(b); break; case Page.Bank: BankPage(b); break; case Page.Poinbase: ExchangePage(b, Venue.Exchange); break; case Page.Web3: Web3Page(b); break; - case Page.Black: BlackPage(b); break; default: ReportPage(b); break; } b.End(); @@ -139,7 +137,6 @@ namespace Decentralized }, r => { r.AddHeader(L.T("bank.ops")); - foreach (var line in L.T("bank.hint").Split('|')) r.AddLabel(line); AmountRow(r); AmlPreview(r); r.AddButton(L.T("trade.deposit"), Deposit); @@ -229,7 +226,6 @@ namespace Decentralized }, r => { r.AddHeader(L.T("trade.h")); - foreach (var line in L.T("trade.hint").Split('|')) r.AddLabel(line); AmountRow(r); TradeRows(r, venue); }); @@ -239,13 +235,21 @@ namespace Decentralized private static void Web3Page(CustomUIBuilder b) { var w = Session.Data.Hot; - if (Unlock.MixerUnlocked) + if (Unlock.MixerUnlocked || Unlock.BlackUnlocked) { + if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade; + if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade; b.BeginRow(); - b.AddButton(_web3Mixer ? L.T("web3.sub.trade") : $"[{L.T("web3.sub.trade")}]", () => { _web3Mixer = false; _message = null; Open(); }); - b.AddButton(_web3Mixer ? $"[{L.T("web3.sub.mixer")}]" : L.T("web3.sub.mixer"), () => { _web3Mixer = true; _message = null; Open(); }); + foreach (Web3Tab t in Enum.GetValues(typeof(Web3Tab))) + { + if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked)) continue; + var tab = t; + string name = L.T("web3.sub." + t.ToString().ToLowerInvariant()); + b.AddButton(tab == _web3Tab ? $"[{name}]" : name, () => { _web3Tab = tab; _message = null; Open(); }); + } b.End(); - if (_web3Mixer) { MixerPage(b); return; } + if (_web3Tab == Web3Tab.Mixer) { MixerPage(b); return; } + if (_web3Tab == Web3Tab.Black) { BlackPage(b); return; } } Columns(b, l => { @@ -258,7 +262,6 @@ namespace Decentralized }, r => { r.AddHeader(L.T("trade.h")); - foreach (var line in L.T("trade.hint").Split('|')) r.AddLabel(line); AmountRow(r); TradeRows(r, Venue.Dex); }); @@ -267,12 +270,28 @@ namespace Decentralized private enum BlackTab { Shop, Orders, Mine, Desk } private static BlackTab _blackTab = BlackTab.Shop; private static PayMode _mode = PayMode.Escrow; - private static Delivery _delivery = Delivery.Door; private static string _deskAsset = Assets.PSC; private static bool _deskIn = true; private static string _payAsset = Assets.CRT; + private static readonly System.Collections.Generic.Dictionary IconCache = new System.Collections.Generic.Dictionary(); - /// Web3 黑市:黑市派系的链上实体。现货柜台(只收币)与 Ponero 兑换处。 + /// 货架上的商品图:游戏物品自己的贴图(本 mod 的显卡用本 mod 的贴图)。 + private static Sprite Icon(string id) + { + if (IconCache.TryGetValue(id, out var cached)) return cached; + Sprite sprite = null; + var it = ModHelper.SpawnItem(BlackDelivery.GameId(id)); + if (it != null) + { + try { sprite = it.useSpriteFromMod ? Items.ModSprite(it.spritePath) : RenderHandler.LoadFromAtlas(it.spriteAtlasPath, it.spritePath); } + catch (Exception ex) { Log.Info($"商品图读取失败 {id}:{ex.Message}"); } + it.Destroy(); + } + IconCache[id] = sprite; + return sprite; + } + + /// Web3 页里的黑市:黑市派系的链上实体。货架式商城(现货、订单)、我的订单与情报、Ponero 兑换处。 private static void BlackPage(CustomUIBuilder b) { int rep = StoreReputation.GetBMReputation(); @@ -292,34 +311,80 @@ namespace Decentralized b.End(); if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1)); return; } if (_blackTab == BlackTab.Desk && desk) { DeskPage(b, rep, day); return; } - if (_blackTab == BlackTab.Orders) { OrdersPage(b, day); return; } if (_blackTab == BlackTab.Mine) { MinePage(b, day); return; } + bool orders = _blackTab == BlackTab.Orders; var w = d.Hot; - Columns(b, l => + b.AddLabel(L.T("black.rep", rep, BlackMarket.MaxLevel(rep)) + " " + L.T("mixer.balance", _payAsset, Fmt.Num(w.Avail(_payAsset))) + (w.Taint > 0.5 ? " " + L.T("wallet.taint", Fmt.Credits(w.Taint)) : "")); + b.BeginRow(); + foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero }) { - l.AddHeader(L.T("black.shop.h")); - l.AddLabel(L.T("black.rep", rep, BlackMarket.MaxLevel(rep))); - if (d.Black.Offers.Count == 0) l.AddLabel(L.T("black.empty")); - foreach (var o in d.Black.Offers.ToList()) + var asset = a; + b.AddButton(asset == _payAsset ? $"[{asset}]" : asset, () => { _payAsset = asset; Open(); }); + } + if (orders) + foreach (PayMode m in Enum.GetValues(typeof(PayMode))) { - var offer = o; - l.AddLabel(L.T("black.offer", ItemName(offer.Item), offer.Qty, offer.Price)); - l.AddButton(L.T("black.buy", offer.Price), () => BuyOffer(offer)); + var mode = m; + string n = L.T("black.mode." + m); + b.AddButton(mode == _mode ? $"[{n}]" : n, () => { _mode = mode; Open(); }); } - }, r => + b.End(); + b.AddSeparator(); + if (!orders) { - r.AddHeader(L.T("black.pay.h")); - foreach (var line in L.T("black.hint").Split('|')) r.AddLabel(line); - r.BeginRow(); - foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero }) + if (d.Black.Offers.Count == 0) b.AddLabel(L.T("black.empty")); + Shelf(b, d.Black.Offers.Cast().ToList(), (c, g) => { - var asset = a; - r.AddButton(asset == _payAsset ? $"[{asset}]" : asset, () => { _payAsset = asset; Open(); }); + var offer = (BmOffer)g; + GoodHead(c, g, L.T("black.stock", offer.Qty)); + c.AddLabel(L.T("black.price", offer.Price)); + c.AddFlexibleSpace(); + c.AddButton(L.T("black.buy"), () => BuyOffer(offer)); + }); + } + else + { + if (d.Black.Catalog.Count == 0) b.AddLabel(L.T("black.empty")); + Shelf(b, d.Black.Catalog.Cast().ToList(), (c, g) => + { + var listing = (BmListing)g; + var seller = d.Black.Sellers[listing.Seller]; + GoodHead(c, g, L.T("black.seller", seller.Name, seller.Trades, seller.Rating)); + c.AddLabel(L.T("black.price", BlackMarket.Pay(listing.Price, _mode))); + c.AddFlexibleSpace(); + bool ok = _mode == PayMode.Escrow || seller.Advance; + c.AddButton(ok ? L.T("black.order", Tuning.BmDoorDays) : L.T("black.noadvance"), () => { if (ok) PlaceOrder(listing); }); + }); + } + } + + private const int ShelfCols = 4; + private const float CardW = 172f, CardH = 268f; + + /// 货架:一行 4 张商品卡。 + private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List goods, Action card) + { + for (int i = 0; i < goods.Count; i += ShelfCols) + { + b.BeginRow(6f); + for (int k = i; k < Math.Min(goods.Count, i + ShelfCols); k++) + { + var g = goods[k]; + b.BeginPanel(CardW, CardH, TextAnchor.UpperCenter, 6f, 3f); + card(b, g); + b.End(); } - r.End(); - r.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(w.Avail(_payAsset)))); - if (w.Taint > 0.5) r.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint))); - }); + b.End(); + } + } + + private static void GoodHead(CustomUIBuilder c, BmGood g, string line2) + { + var sprite = Icon(g.Item); + if (sprite != null) c.AddImage(sprite, 64f, 64f); else c.AddSpace(64f); + c.AddLabel("" + ItemName(g.Item) + ""); + c.AddLabel(g.Kind == GoodKind.Gpu ? L.T("black.kind.gpu", (int)Math.Round(g.Health)) : L.T("black.kind." + g.Kind + (g.Kind == GoodKind.Contraband ? "." + g.Level : ""))); + c.AddLabel(line2); } private static string ItemName(string id) => BlackDelivery.ItemName(id); @@ -327,69 +392,19 @@ namespace Decentralized private static void BuyOffer(BmOffer o) { var d = Session.Data; + string item = o.Item, model = o.Model, fake = o.FakeOf; + double health = o.Health; if (!d.Black.Buy(o, d.Hot, _payAsset, Session.Price)) { _message = L.T("msg.balance"); Open(); return; } - Items.Give(ModHelper.SpawnItem(o.Item)); - _message = L.T("black.bought", ItemName(o.Item)); + Items.Give(BlackDelivery.Make(item, model, fake, health)); + _message = L.T("black.bought", ItemName(item)); Session.Save(); Open(); } - private static string SellerText(BmSeller s) => L.T("black.seller", s.Name, s.Trades, s.Rating); - - /// 订单目录:比现货便宜,要等几天,可能被截获、被骗。 - private static void OrdersPage(CustomUIBuilder b, int day) - { - var d = Session.Data; - var w = d.Hot; - Columns(b, l => - { - l.AddHeader(L.T("black.orders.h")); - if (d.Black.Catalog.Count == 0) l.AddLabel(L.T("black.empty")); - foreach (var x in d.Black.Catalog.ToList()) - { - var listing = x; - var seller = d.Black.Sellers[listing.Seller]; - l.AddLabel(L.T("black.listing", ItemName(listing.Item), BlackMarket.Pay(listing.Price, _mode))); - l.AddLabel(" " + SellerText(seller)); - bool ok = _mode == PayMode.Escrow || seller.Advance; - l.AddButton(ok ? L.T("black.order") : L.T("black.noadvance"), () => { if (ok) PlaceOrder(listing); }); - } - }, r => - { - r.AddHeader(L.T("black.pay.h")); - foreach (var line in L.T("black.orders.hint", (int)Math.Round(Tuning.BmEscrowFee * 100), (int)Math.Round(Tuning.BmAdvanceDiscount * 100), Tuning.BmAdvanceMinTrades).Split('|')) r.AddLabel(line); - r.BeginRow(); - foreach (PayMode m in Enum.GetValues(typeof(PayMode))) - { - var mode = m; - string n = L.T("black.mode." + m); - r.AddButton(mode == _mode ? $"[{n}]" : n, () => { _mode = mode; Open(); }); - } - r.End(); - r.BeginRow(); - foreach (Delivery dl in Enum.GetValues(typeof(Delivery))) - { - var del = dl; - string n = L.T("black.del." + dl, Tuning.BmDoorDays); - r.AddButton(del == _delivery ? $"[{n}]" : n, () => { _delivery = del; Open(); }); - } - r.End(); - r.AddLabel(L.T("black.del.hint." + _delivery, BlackMarket.Days(_delivery))); - r.BeginRow(); - foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero }) - { - var asset = a; - r.AddButton(asset == _payAsset ? $"[{asset}]" : asset, () => { _payAsset = asset; Open(); }); - } - r.End(); - r.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(w.Avail(_payAsset)))); - }); - } - private static void PlaceOrder(BmListing l) { var d = Session.Data; - var o = d.Black.Place(l, _mode, _delivery, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price); + var o = d.Black.Place(l, _mode, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price); if (o == null) { _message = L.T("msg.balance"); Open(); return; } _message = L.T("black.ordered", ItemName(o.Item), o.DueDay); Session.Save(); @@ -405,7 +420,7 @@ namespace Decentralized l.AddHeader(L.T("black.mine.h")); if (d.Black.Orders.Count == 0) l.AddLabel(L.T("black.mine.none")); foreach (var o in d.Black.Orders.OrderByDescending(x => x.PlacedDay)) - l.AddLabel(L.T("black.mine.line", ItemName(o.Item), 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, o.Delivery == Delivery.Drop && o.State == OrderState.Ready ? L.T("black.mine.drop") : "")); + l.AddLabel(L.T("black.mine.line", ItemName(o.Item), 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")); @@ -422,7 +437,6 @@ namespace Decentralized Columns(b, l => { l.AddHeader(L.T("black.desk.h")); - foreach (var line in L.T("black.desk.hint", (int)Math.Round(Tuning.PoneroFee * 100)).Split('|')) l.AddLabel(line); l.AddLabel(L.T("black.desk.left", Fmt.Credits(d.Black.DeskLeft(day)), Fmt.Credits(Tuning.PoneroDailyCap))); l.AddLabel(L.T("black.desk.price", Fmt.Num(Session.Price(Assets.Ponero)))); Holdings(l, w, a => a == Assets.Ponero || a == _deskAsset); @@ -460,7 +474,8 @@ namespace Decentralized Open(); } - private static bool _web3Mixer; + private enum Web3Tab { Trade, Mixer, Black } + private static Web3Tab _web3Tab = Web3Tab.Trade; private static string _mixAsset = Assets.PSC; private static int _mixRounds = 2; private static readonly float[] MixW = { 70f, 100f, 100f, 80f }; @@ -488,7 +503,6 @@ namespace Decentralized }, r => { r.AddHeader(L.T("mixer.deposit")); - foreach (var line in L.T("mixer.hint").Split('|')) r.AddLabel(line); r.BeginRow(); foreach (var a in new[] { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA }) { diff --git a/src/Decentralized/Unlock.cs b/src/Decentralized/Unlock.cs index 66a51a1..6ff8f93 100644 --- a/src/Decentralized/Unlock.cs +++ b/src/Decentralized/Unlock.cs @@ -44,6 +44,7 @@ namespace Decentralized Items.Give(ModHelper.SpawnItem(Items.Rig)); Items.Give(ModHelper.SpawnItem(Items.GpuId("RTX60"))); Items.Give(ModHelper.SpawnItem(Items.Guide)); + Items.Give(ModHelper.SpawnItem(Items.Web3Guide)); Log.Info("已加入 Web3 治理社区"); } } diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index c2a8cd9..66fb171 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -567,7 +567,7 @@ internal static class Program Func price = a => a == Assets.Ponero ? 50 : a == Assets.PSC ? 100 : 1; var bm = new BlackMarket(); Check(!bm.Refresh(10, 5, new Rng(1), id => 100) && bm.Offers.Count == 0, "声望不够不开放"); - Check(bm.Refresh(10, 45, new Rng(1), id => 100) && bm.Offers.Count > 0 && bm.Offers.All(o => o.Level <= 2 && o.Price == 130), "声望 45 开放中档,溢价 30%"); + Check(bm.Refresh(10, 45, new Rng(1), id => 100) && bm.Offers.Count > 0 && bm.Offers.Where(o => o.Kind == GoodKind.Contraband).All(o => o.Level <= 2 && o.Price == 130), "声望 45 开放中档,溢价 30%"); Check(!bm.Refresh(12, 45, new Rng(2), id => 100) && bm.Refresh(17, 45, new Rng(2), id => 100), "每周刷新"); var w = new Wallet { Address = "t" }; w.Add(Assets.PSC, 10); w.Add(Assets.CRT, 1000); w.Taint = 400; w.Basis = 800; // 总价值 2000 @@ -590,8 +590,8 @@ internal static class Program Func price = a => a == Assets.CRT ? 1 : 100; Func value = id => 200; var bm = new BlackMarket(); - bm.RefreshOrders(75, new Rng(5), value); - Check(bm.Sellers.Count == Tuning.BmSellerCount && bm.Catalog.Count > 0 && bm.Catalog.All(l => l.Price == 170), "目录:卖家与订单价(基础价 × 0.85)"); + bm.RefreshOrders(10, 75, new Rng(5), value); + Check(bm.Sellers.Count == Tuning.BmSellerCount && bm.Catalog.Count > 0 && bm.Catalog.Where(l => l.Kind == GoodKind.Contraband).All(l => l.Price == 170), "目录:卖家与订单价(基础价 × 0.85)"); Check(BlackMarket.Pay(170, PayMode.Escrow) == 170 + 9 && BlackMarket.Pay(170, PayMode.Advance) == 153, "托管多付 5%,提前放款打九折"); var good = new BmSeller { Name = "g", Trades = 100, Rating = 95 }; var bad = new BmSeller { Name = "b", Trades = 100, Rating = 95, Scam = true }; @@ -600,38 +600,38 @@ internal static class Program var lb = new BmListing { Seller = 1, Item = "x", Price = 1000 }; var w = new Wallet { Address = "t" }; w.Add(Assets.CRT, 10000); w.Taint = 4000; w.Basis = 5000; - var o = bm.Place(lb, PayMode.Escrow, Delivery.Door, w, Assets.CRT, 10, price); - Check(o != null && Near(w.Get(Assets.CRT), 10000 - 1050, 1e-9) && w.Taint < 4000 && bm.Place(lg, PayMode.Escrow, Delivery.Door, w, Assets.CRC, 10, price) == null, "托管下单:扣币、污点留在订单上;不收 CRC"); + var o = bm.Place(lb, PayMode.Escrow, w, Assets.CRT, 10, price); + Check(o != null && Near(w.Get(Assets.CRT), 10000 - 1050, 1e-9) && w.Taint < 4000 && bm.Place(lg, PayMode.Escrow, w, Assets.CRC, 10, price) == null, "托管下单:扣币、污点留在订单上;不收 CRC"); double tBefore = 4000; var ev = bm.Night(13, false, w, new Rng(1)); Check(ev.Any(e => e.Kind == "scam_refund") && Near(w.Get(Assets.CRT), 10000, 1e-6) && Near(w.Taint, tBefore, 1e-6) && Near(w.Basis, 5000, 1e-6), "骗子卖家 + 托管:退款,污点与成本一并还回(不能借退款洗钱)"); - var o2 = bm.Place(lb, PayMode.Advance, Delivery.Drop, w, Assets.CRT, 20, price); - var ev2 = bm.Night(22, false, w, new Rng(2)); + var o2 = bm.Place(lb, PayMode.Advance, w, Assets.CRT, 20, price); + var ev2 = bm.Night(23, false, w, new Rng(2)); Check(o2.State == OrderState.Lost && Near(w.Get(Assets.CRT), 10000 - 900, 1e-6), "骗子卖家 + 提前放款:钱没了"); - var o3 = bm.Place(lg, PayMode.Escrow, Delivery.Door, w, Assets.CRT, 30, price); + var o3 = bm.Place(lg, PayMode.Escrow, w, Assets.CRT, 30, price); Check(bm.Night(31, false, w, new Rng(3)).Count == 0 && o3.State == OrderState.Pending, "没到期不动"); int ready = 0, lost = 0; for (int i = 0; i < 400; i++) { var t = new BlackMarket(); t.Sellers.Add(good); var ww = new Wallet(); ww.Add(Assets.CRT, 5000); - var oo = t.Place(new BmListing { Seller = 0, Item = "x", Price = 100 }, PayMode.Escrow, Delivery.Door, ww, Assets.CRT, 1, price); + var oo = t.Place(new BmListing { Seller = 0, Item = "x", Price = 100 }, PayMode.Escrow, ww, Assets.CRT, 1, price); t.Night(oo.DueDay, false, ww, new Rng((ulong)i + 7)); if (oo.State == OrderState.Ready) ready++; else lost++; } Check(ready > 340 && lost > 10, $"上门:约 {Tuning.BmInterceptDoor:P0} 被截获(跑路另算):到货 {ready},丢失 {lost}"); var c = new BlackMarket(); c.Sellers.Add(good); var cw = new Wallet(); cw.Add(Assets.CRT, 5000); - var co = c.Place(new BmListing { Seller = 0, Item = "x", Price = 100 }, PayMode.Escrow, Delivery.Door, cw, Assets.CRT, 1, price); + var co = c.Place(new BmListing { Seller = 0, Item = "x", Price = 100 }, PayMode.Escrow, cw, Assets.CRT, 1, price); c.Night(co.DueDay, true, cw, new Rng(1)); Check(co.State == OrderState.Pending && co.DueDay == 5, "镇压期间到期顺延一天"); // 跑路:托管资金多则概率高;跑路后关闭 var r = new BlackMarket(); r.Sellers.Add(good); var rw = new Wallet(); rw.Add(Assets.CRT, 1000000); - for (int i = 0; i < 20; i++) r.Place(new BmListing { Seller = 0, Item = "x", Price = 10000 }, PayMode.Escrow, Delivery.Door, rw, Assets.CRT, 1, price); + for (int i = 0; i < 20; i++) r.Place(new BmListing { Seller = 0, Item = "x", Price = 10000 }, PayMode.Escrow, rw, Assets.CRT, 1, price); Check(r.RugChance > BlackMarketTestRug(), "托管资金越多越容易跑路"); r.ClosedUntil = 40; - Check(r.Closed(40) && !r.Closed(41) && r.Place(new BmListing { Seller = 0, Item = "x", Price = 1 }, PayMode.Escrow, Delivery.Door, rw, Assets.CRT, 30, price) == null, "关闭期间不能下单"); + Check(r.Closed(40) && !r.Closed(41) && r.Place(new BmListing { Seller = 0, Item = "x", Price = 1 }, PayMode.Escrow, rw, Assets.CRT, 30, price) == null, "关闭期间不能下单"); }); Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");