diff --git a/CHANGELOG.md b/CHANGELOG.md index 31705ce..4134aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ - 挖矿报告并进 Web3 页的"挖矿"子页,终端顶栏不再有"报告"。 - 游戏内验证:受控交付的对话与两个选项显示,拒签后快递员离开、下一位顾客进来。没在游戏里验证:签收的罪证、蜜罐与落网的夜间追查(只有单元测试)。 +- 货架重做:违禁品池只留能转手的货(枪械、钥匙卡、密码解序器芯片、爆炸物),弹药、毒品、药品、化学品、种子全部移除;高科技物品池换成矿机能用的东西(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);平台跑路时上架的货和未放款的货款都没了;蜜罐期间成交的货款和近期买家落网会被追查记罪证;镇压期间平台不成交。"我的订单与情报"页多出"我的上架",可以撤下。 +- 游戏内验证:货架显示显卡、模组、能量电池;买一张卡后出售页列出储藏区里的货,上架(标价 400,上架费 20),过夜成交(货款 368),3 天后到账(CRT 2804 → 3172,这次随机没带污点)。没有在游戏里验证:假卡退货、跑路时的出售损失(只有单元测试)。 + ### 第三阶段:数值调优(进行中) - 混币器每轮手续费 6% → 4%;用币付款的顾客概率提到上层 30%、黑市 50%、革命派 50%、下层 25%,并多付 12%(溢价只加在币数上,污点仍按货值算)。 diff --git a/src/Decentralized.Domain/BlackMarket.cs b/src/Decentralized.Domain/BlackMarket.cs index 2f106cf..d0b91d0 100644 --- a/src/Decentralized.Domain/BlackMarket.cs +++ b/src/Decentralized.Domain/BlackMarket.cs @@ -37,23 +37,22 @@ 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), - P("dream_cap_seed", 1), P("eng_keycard", 1), P("med_keycard", 1), P("ser_keycard", 1), P("sup_keycard", 1), - P("crypto_module_ser", 1), P("crypto_module_sup", 1), - P("revolver", 2), P("heavy_handmade_pistol", 2), P("small_pistol_ammo_p", 2), P("heavy_pistol_ammo_p", 2), - P("flashbang_grenade", 2), P("smoke_grenade", 2), P("dream_dust", 2), P("restricted_chemical", 2), - P("sci_keycard", 2), P("sec_keycard", 2), P("crypto_module_eng", 2), P("crypto_module_med", 2), P("unlicensed_phagimycin_pill", 2), - 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), + P("handmade_pistol", 1), P("printed_gun", 1), P("stun_baton", 1), + P("eng_keycard", 1), P("med_keycard", 1), P("ser_keycard", 1), P("sup_keycard", 1), P("crypto_module_ser", 1), P("crypto_module_sup", 1), + P("revolver", 2), P("heavy_handmade_pistol", 2), P("flashbang_grenade", 2), P("smoke_grenade", 2), + P("sci_keycard", 2), P("sec_keycard", 2), P("crypto_module_eng", 2), P("crypto_module_med", 2), + P("shotgun", 3), P("smg", 3), P("c4", 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", + "system_module_overclock", "system_module_performance", "system_module_quality", "system_module_eco", "system_module_efficiancy", + "system_module_fineness", "system_module_streamlining", "energy_credit_ext", "energy_credit_hyp", "energy_credit_breeder", + "powerblock", "module_extractor_advanced", "pd_rig", }; private static KeyValuePair P(string id, int level) => new KeyValuePair(id, level); @@ -84,7 +83,7 @@ namespace Decentralized.Domain 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) + else if (r < Tuning.BmGpuChance + Tuning.BmTechChance && tech.Count > 0) { int k = rng.Range(0, tech.Count - 1); string id = tech[k]; diff --git a/src/Decentralized.Domain/BmOrders.cs b/src/Decentralized.Domain/BmOrders.cs index 3b6340b..b2abc35 100644 --- a/src/Decentralized.Domain/BmOrders.cs +++ b/src/Decentralized.Domain/BmOrders.cs @@ -59,6 +59,7 @@ namespace Decentralized.Domain { public string Kind; // ready / scam_refund / scam_loss / rug / intercept / reopen public BmOrder Order; + public BmSale Sale; public double Amount; } @@ -84,7 +85,7 @@ namespace Decentralized.Domain public bool Closed(int day) => day <= ClosedUntil; /// 托管中和已提前放款但尚未交付的资金(信用点),平台跑路的概率随它增加。 - public double Held => Orders.Where(o => o.State == OrderState.Pending).Sum(o => (double)o.Price); + public double Held => Orders.Where(o => o.State == OrderState.Pending).Sum(o => (double)o.Price) + HeldSales; 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); @@ -151,18 +152,23 @@ namespace Decentralized.Domain } /// 每晚推进订单:平台跑路、到期的订单(卖家跑路、被截获、死投被挖走)、转入可取状态。镇压期间到期顺延一天。 - public List Night(int day, bool crackdown, Wallet w, Rng rng, int secRep = 0) + public List Night(int day, bool crackdown, Wallet w, Rng rng, int secRep = 0, Func price = null) { var ev = new List(); if (!Honeypot(day) && rng.Chance(HoneypotChance(secRep))) { HoneypotUntil = day + Tuning.BmHoneypotDays; if (!Forum.Contains("easy")) Forum.Add("easy"); } if (ClosedUntil == day) ev.Add(new BmEvent { Kind = "reopen" }); - if (!Closed(day) && Orders.Any(o => o.State == OrderState.Pending) && rng.Chance(RugChance)) + if (!Closed(day) && (Orders.Any(o => o.State == OrderState.Pending) || Sales.Any(x => x.State == SaleState.Listed || x.State == SaleState.Sold)) && rng.Chance(RugChance)) { foreach (var o in Orders.Where(o => o.State == OrderState.Pending)) { o.State = OrderState.Lost; o.Why = "rug"; ev.Add(new BmEvent { Kind = "rug", Order = o, Amount = o.Price }); } + foreach (var sl in Sales.Where(x => x.State == SaleState.Listed || x.State == SaleState.Sold)) + { + ev.Add(new BmEvent { Kind = "sale_rug", Sale = sl, Amount = sl.State == SaleState.Sold ? sl.Proceeds : sl.ListPrice }); + sl.State = SaleState.Lost; sl.Why = "rug"; + } ClosedUntil = day + Tuning.BmRugClosedDays; Offers.Clear(); Catalog.Clear(); Sellers.Clear(); return ev; @@ -186,6 +192,7 @@ namespace Decentralized.Domain o.State = OrderState.Ready; ev.Add(new BmEvent { Kind = "ready", Order = o }); } + if (price != null) SalesNight(day, crackdown, w, price, rng, ev); // 卖家落网:近期成交过的卖家各有小概率被抓,他们留存的订单记录之后会连累买家 foreach (var name in Orders.Where(x => x.State == OrderState.Done && day - x.DueDay <= 14).Select(x => x.Seller).Distinct().ToList()) { diff --git a/src/Decentralized.Domain/BmSales.cs b/src/Decentralized.Domain/BmSales.cs new file mode 100644 index 0000000..a47b245 --- /dev/null +++ b/src/Decentralized.Domain/BmSales.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + public enum SaleState { Listed, Sold, Paid, Lost, Delisted } + + /// 玩家在黑市上架的一件货。货交给平台(从储藏区拿走),成交后货款托管几天再到钱包,可能带污点。 + public sealed class BmSale + { + public string Item; + public string Model, FakeOf; + public double Health; + /// 标价倍率与标价(信用点)。 + public double Mult; + public int ListPrice; + public int ListedDay, PayDay; + public SaleState State = SaleState.Listed; + /// 成交后扣掉抽成的货款(信用点)与其中带的污点。 + public int Proceeds; + public double Taint; + /// 终态原因:fake(买家看穿假卡退货)/ rug / delist。 + public string Why; + } + + public sealed partial class BlackMarket + { + public List Sales = new List(); + /// 玩家作为卖家的好评(%):影响成交速度;卖假卡被看穿会下降,每笔成交回升一点。 + public int Rating = Tuning.BmRatingStart; + + /// 期望成交天数:倍率 ≤0.9 一天,1.0 两天,1.8 十四天,之间插值(0.9–1.0 线性,1.0 以上指数)。 + public static double ExpectedDays(double m) + { + if (m <= 0.9) return Tuning.BmSellDaysFast; + if (m < 1.0) return Tuning.BmSellDaysFast + (m - 0.9) / 0.1 * (Tuning.BmSellDays1 - Tuning.BmSellDaysFast); + return Tuning.BmSellDays1 * Math.Pow(Tuning.BmSellDaysMax / Tuning.BmSellDays1, (m - 1.0) / (Tuning.BmSellMax - 1.0)); + } + + private static double T(double m) => Math.Max(0, Math.Min(1, (m - Tuning.BmSellMin) / (Tuning.BmSellMax - Tuning.BmSellMin))); + /// 货款带污点的概率与(带的时候)污点占货款的比例:都随标价倍率上升。 + public static double TaintChance(double m) => Tuning.BmSaleTaintChanceMin + T(m) * (Tuning.BmSaleTaintChanceMax - Tuning.BmSaleTaintChanceMin); + public static double TaintRatio(double m) => Tuning.BmSaleTaintRatioMin + T(m) * (Tuning.BmSaleTaintRatioMax - Tuning.BmSaleTaintRatioMin); + + /// 每晚成交的概率:1 / 期望天数,乘好评系数(好评 90 为 1,封顶 1.1),最多 1。 + public double SaleChance(double m) => Math.Min(1, 1 / ExpectedDays(m) * Math.Min(1.1, Rating / (double)Tuning.BmRatingStart)); + + public static int ListPriceOf(long refValue, double m) => (int)Math.Max(1, Math.Round(refValue * m)); + public static int ListFee(int listPrice) => (int)Math.Ceiling(listPrice * Tuning.BmListFee); + + /// 上架:交付一件货,收上架费(付给 feeAsset 币,污点与成本按比例带走)。倍率超出范围、平台关闭或余额不足返回 null。 + public BmSale List(string item, string model, string fakeOf, double health, long refValue, double m, Wallet w, string feeAsset, int day, Func price) + { + if (m < Tuning.BmSellMin - 1e-9 || m > Tuning.BmSellMax + 1e-9 || refValue <= 0 || Closed(day) || feeAsset == Assets.CRC) return null; + int listPrice = ListPriceOf(refValue, m); + double p = price(feeAsset); + if (p <= 0) return null; + double qty = ListFee(listPrice) / p; + if (w.Avail(feeAsset) + 1e-9 < qty) return null; + double total = w.Value(price); + double share = total > 0 ? Math.Min(1, ListFee(listPrice) / total) : 0; + w.Taint -= w.Taint * share; w.Basis -= w.Basis * share; + w.Add(feeAsset, -qty); + var s = new BmSale { Item = item, Model = model, FakeOf = fakeOf, Health = health, Mult = m, ListPrice = listPrice, ListedDay = day }; + Sales.Add(s); + return s; + } + + public void Delist(BmSale s) { s.State = SaleState.Delisted; s.Why = "delist"; } + + /// 未到账的货款(信用点):跑路时一并归零,也推高跑路概率。 + public double HeldSales => Sales.Where(x => x.State == SaleState.Sold).Sum(x => (double)x.Proceeds); + + /// 每晚推进出售:上架的货按概率成交(假卡可能被看穿退货),成交的货款托管到期后进钱包。镇压期间平台下线,不推进。 + private void SalesNight(int day, bool crackdown, Wallet w, Func price, Rng rng, List ev) + { + if (crackdown) return; + foreach (var s in Sales.Where(x => x.State == SaleState.Listed && x.ListedDay < day).ToList()) + { + if (!rng.Chance(SaleChance(s.Mult))) continue; + if (s.FakeOf != null && rng.Chance(Tuning.BmFakeDetect)) + { + s.State = SaleState.Delisted; s.Why = "fake"; + Rating = Math.Max(0, Rating - Tuning.BmRatingFakeLoss); + ev.Add(new BmEvent { Kind = "sale_fake", Sale = s }); + continue; + } + s.State = SaleState.Sold; + s.Proceeds = (int)Math.Round(s.ListPrice * (1 - Tuning.BmCommission)); + s.Taint = rng.Chance(TaintChance(s.Mult)) ? s.Proceeds * TaintRatio(s.Mult) : 0; + s.PayDay = day + Tuning.BmSaleHold; + Rating = Math.Min(100, Rating + 1); + if (Honeypot(day)) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = s.Proceeds, Why = "honeypot", Item = s.Item }); + ev.Add(new BmEvent { Kind = "sale_sold", Sale = s, Amount = s.Proceeds }); + } + foreach (var s in Sales.Where(x => x.State == SaleState.Sold && day >= x.PayDay).ToList()) + { + double p = price(Assets.CRT); + if (p <= 0) continue; + w.Add(Assets.CRT, s.Proceeds / p); + w.Taint += s.Taint; + w.Basis += s.Proceeds; + s.State = SaleState.Paid; + ev.Add(new BmEvent { Kind = "sale_paid", Sale = s, Amount = s.Proceeds }); + } + // 买家落网:近期到账的出售记录会连累卖家(玩家) + var recent = Sales.Where(x => x.State == SaleState.Paid && day - x.PayDay <= 14).ToList(); + if (recent.Count > 0 && rng.Chance(Tuning.BmSellerBust)) + { + foreach (var s in recent) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = s.Proceeds, Why = "seller", Item = s.Item }); + Forum.Add("busted_buyer"); + } + Sales.RemoveAll(x => (x.State == SaleState.Paid || x.State == SaleState.Delisted || x.State == SaleState.Lost) && day - Math.Max(x.ListedDay, x.PayDay) > 14); + } + + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 144a6ef..4e79866 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -99,7 +99,13 @@ namespace Decentralized.Domain public const double BmHoneypotBase = 0.01, BmHoneypotPerSecRep = 0.0004, BmHoneypotMax = 0.06, BmControlled = 0.35, BmSellerBust = 0.008; public const int BmHoneypotDays = 6, BmTrapDelay = 4; /// 货架上除违禁品外的两类货:高科技物品(需中档声望)和显卡的出现概率、加价(在现货溢价 / 订单价之外再加),显卡里夹假卡的比例。 - public const double BmTechChance = 0.15, BmGpuChance = 0.20, BmTechMarkup = 0.5, BmGpuMarkup = 0.2, BmGpuFake = 0.20; + public const double BmTechChance = 0.30, BmGpuChance = 0.30, BmTechMarkup = 0.5, BmGpuMarkup = 0.2, BmGpuFake = 0.20; + /// 玩家上架出售:标价倍率范围(相对参考价)、上架费、成交抽成、成交后货款托管天数、假卡被买家看穿的概率; + /// 倍率 ≤0.9 / 1.0 / 1.8 时的期望成交天数(之间插值);货款带污点的概率与比例随倍率从下限涨到上限;卖家好评的初始值、看穿一次扣的点数。 + public const double BmListFee = 0.05, BmCommission = 0.08, BmSellMin = 0.6, BmSellMax = 1.8, BmFakeDetect = 0.5; + public const double BmSellDaysFast = 1, BmSellDays1 = 2, BmSellDaysMax = 14; + public const double BmSaleTaintChanceMin = 0.2, BmSaleTaintChanceMax = 0.9, BmSaleTaintRatioMin = 0.2, BmSaleTaintRatioMax = 0.8; + public const int BmSaleHold = 3, BmRatingStart = 90, BmRatingFakeLoss = 10; /// 黑市人脉升级的价格。 public const int BmUnlockPrice = 1500; /// 单个金融违规事件记入的罪证上限。 diff --git a/src/Decentralized/BlackDelivery.cs b/src/Decentralized/BlackDelivery.cs index 9914295..e7d057a 100644 --- a/src/Decentralized/BlackDelivery.cs +++ b/src/Decentralized/BlackDelivery.cs @@ -48,6 +48,31 @@ namespace Decentralized return n; } + // ---- 玩家出售:从储藏区挑能上架的货 ---- + + /// 这件物品在黑市上的货 id(显卡是 "gpu:型号",其余是物品 id);不能上架返回 null。 + public static string SellId(GameItem it) + { + if (it == null || it.identifier == null) return null; + if (Items.IsGpu(it)) return "gpu:" + Items.ToUnit(it).Model; + string id = it.identifier; + return BlackMarket.Pool.Any(x => x.Key == id) || BlackMarket.TechPool.Contains(id) ? id : null; + } + + public static System.Collections.Generic.List Sellable() => + GraphUtils.FindAllChildrenType(EmporiumEntry.Instance.invElement, i => GeneralHelper.IsItemOwned(i) && SellId(i) != null, i => i.identifier != Items.Rig); + + /// 参考价:显卡按标称型号与成色,其余按游戏基础价。 + public static long RefValue(GameItem it) + { + string id = SellId(it); + if (id == null) return 0; + if (Items.IsGpu(it)) { var u = Items.ToUnit(it); return CardValue.Of(u.Model, u.Health); } + return Value(id); + } + + public static GoodKind KindOf(string id) => id.StartsWith("gpu:") ? GoodKind.Gpu : BlackMarket.TechPool.Contains(id) ? GoodKind.Tech : GoodKind.Contraband; + /// 每周刷新:现货与订单目录一起。返回是否刷新了。 public static bool Restock(SaveData d, int day, bool force = false) { @@ -62,8 +87,25 @@ namespace Decentralized public static void Night(SaveData d, int day, bool crackdown) { var ps = PlayerStore.Instance; - foreach (var e in d.Black.Night(day, crackdown, d.Hot, d.Rng, StoreReputation.GetSecReputation())) + foreach (var e in d.Black.Night(day, crackdown, d.Hot, d.Rng, StoreReputation.GetSecReputation(), Session.Price)) { + if (e.Sale != null) + { + string sname = ItemName(e.Sale.Item); + Log.Info($"黑市出售事件:{e.Kind} {e.Sale.Item} ×{e.Sale.Mult} 货款 {e.Amount:0} 污点 {e.Sale.Taint:0}"); + switch (e.Kind) + { + case "sale_sold": ps.AddNightLog(L.T("nightlog.bm.sale_sold", sname, Fmt.Num(e.Amount), e.Sale.PayDay), "99bb66"); break; + case "sale_paid": ps.AddNightLog(L.T("nightlog.bm.sale_paid", sname, Fmt.Num(e.Amount), Fmt.Num(e.Sale.Taint)), "99bb66"); break; + case "sale_fake": + Items.Give(Make(e.Sale.Item, e.Sale.Model, e.Sale.FakeOf, e.Sale.Health)); + ps.AddNightLog(L.T("nightlog.bm.sale_fake", sname), "cc7755"); + break; + case "sale_rug": ps.AddNightLog(L.T("nightlog.bm.sale_rug", sname, Fmt.Num(e.Amount)), "cc7755"); break; + } + if (e.Kind == "sale_rug") News.Show("pd_news_bmrug_" + day, L.T("news.bmrug.title"), L.T("news.bmrug.desc"), 4); + continue; + } string name = e.Order != null && e.Kind != "trap" ? ItemName(e.Order.Item) : null; Log.Info($"黑市订单事件:{e.Kind} {e.Order?.Item} {e.Order?.Seller} {e.Order?.Mode} 受控 {e.Order?.Controlled} 金额 {e.Amount:0}"); switch (e.Kind) diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 1d999a1..63ca15b 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -270,7 +270,7 @@ 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, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometimes turns into enforcement at your door: signing logs the crime on the spot and seizes the goods, refusing only costs the payment. 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." +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, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometimes turns into enforcement at your door: signing logs the crime on the spot and seizes the goods, refusing only costs the payment. Selling: on the black market's Sell page, list contraband, high-tech goods and GPUs from storage at 0.6x to 1.8x the reference price. The higher the multiplier, the slower the sale (below 0.9x about a day, 1.0x about two, 1.8x about fourteen), and the likelier and larger the taint on the payout; 5% listing fee, 8% commission, payout held 3 days before reaching your wallet; a fake card may be spotted and returned, hurting your rating; if the platform rugs, listed goods and unreleased payouts are gone. 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." courier.odd1,...Package is here. courier.odd2,"(His hand stays in his pocket.) Sign here." courier.sign,Sign @@ -286,3 +286,26 @@ nightlog.bm.trap.seller,"Black market: a seller was arrested and the order recor web3.sub.mining,Mining courier.sign.say,I'll sign. courier.refuse.say,Not taking it. Send it back. +black.forum.busted_buyer,"Word is a batch of buyers got arrested, and the sale records are still on the platform." +nightlog.bm.sale_sold,"Black market: your {0} sold for {1}; payout on day {2}." +nightlog.bm.sale_paid,"Black market: payout of {1} for {0} arrived (CRT), taint {2}." +nightlog.bm.sale_fake,"Black market: the buyer saw that {0} is a fake and returned it; your rating dropped." +nightlog.bm.sale_rug,"Black market: the platform rugged; your {0} and the payout ({1}) are gone." +black.sub.sell,Sell +black.sell.h,Sell +black.sell.mult,"Price x{0}" +black.sell.info,"Price {0} ~{1} days to sell payout tainted {2}% of the time ({3}% of it)" +black.sell.list,List +black.sell.none,Nothing sellable in storage. +black.sell.ref,"Ref {0}" +black.sell.listed,"Listed: {0} at {1}." +black.sell.delist,Take down +black.sell.rating,"Rating {0}%" +black.mine.sales,My listings +black.mine.sale,"{0} x{1}: {2}" +black.sale.Listed,listed +black.sale.Sold,"sold, payout day {0}" +black.sale.Paid,paid +black.sale.Lost,platform rugged, gone +black.sale.Delisted,taken down +black.sale.Delisted.fake,buyer spotted the fake and returned it diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index b4a08ad..8cf1083 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -269,7 +269,7 @@ 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 兑出的币来路不明,出金审查会加重。" +item.web3guide.desc,"银行:存入把信用点换成 CRC;提取把 CRC 换回信用点,提取要过合规审查。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的出售页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高;上架收 5% 上架费,成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了。Ponero 兑换处:Ponero 是隐私币,兑入时这部分价值的污点被清除;兑入、兑出各收 8% 手续费,每天有额度;从 Ponero 兑出的币来路不明,出金审查会加重。" courier.odd1,……东西到了。 courier.odd2,"(他的手一直放在口袋里)在这儿签个字。" courier.sign,签收 @@ -285,3 +285,26 @@ nightlog.bm.trap.seller,"黑市:卖家落网,他留存的订单记录连累 web3.sub.mining,挖矿 courier.sign.say,签收。 courier.refuse.say,不收,退回去。 +black.forum.busted_buyer,"有人说最近一批买家被抓了,成交记录还在平台上。" +nightlog.bm.sale_sold,"黑市:你上架的 {0} 卖出了,货款 {1},第 {2} 天放款。" +nightlog.bm.sale_paid,"黑市:{0} 的货款 {1} 到账(CRT),其中污点 {2}。" +nightlog.bm.sale_fake,"黑市:买家看穿了 {0} 是假卡,退了货,你的好评下降。" +nightlog.bm.sale_rug,"黑市:平台跑路,你的 {0} 和货款({1})没了。" +black.sub.sell,出售 +black.sell.h,出售 +black.sell.mult,"标价 ×{0}" +black.sell.info,"标价 {0} 约 {1} 天成交 货款带污点 {2}%(比例 {3}%)" +black.sell.list,上架 +black.sell.none,储藏区里没有能卖的货。 +black.sell.ref,"参考价 {0}" +black.sell.listed,"已上架:{0},标价 {1}。" +black.sell.delist,撤下 +black.sell.rating,"好评 {0}%" +black.mine.sales,我的上架 +black.mine.sale,"{0} ×{1}:{2}" +black.sale.Listed,上架中 +black.sale.Sold,"已成交,第 {0} 天放款" +black.sale.Paid,已到账 +black.sale.Lost,平台跑路,没了 +black.sale.Delisted,已撤下 +black.sale.Delisted.fake,买家看穿假卡退货 diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 78ec3f4..f292753 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -267,7 +267,7 @@ namespace Decentralized }); } - private enum BlackTab { Shop, Orders, Mine, Desk } + private enum BlackTab { Shop, Orders, Sell, Mine, Desk } private static BlackTab _blackTab = BlackTab.Shop; private static PayMode _mode = PayMode.Escrow; private static string _deskAsset = Assets.PSC; @@ -312,6 +312,7 @@ namespace Decentralized 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.Mine) { MinePage(b, day); return; } + if (_blackTab == BlackTab.Sell) { SellPage(b, day); return; } bool orders = _blackTab == BlackTab.Orders; var w = d.Hot; 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)) : "")); @@ -358,6 +359,60 @@ namespace Decentralized } } + private static double _sellMult = 1.0; + private static readonly double[] SellMults = { 0.6, 0.8, 0.9, 1.0, 1.2, 1.5, 1.8 }; + + /// 出售:储藏区里能上架的货做成货架,选标价倍率后上架。 + private static void SellPage(CustomUIBuilder b, int day) + { + var d = Session.Data; + b.BeginRow(); + foreach (var m in SellMults) + { + double mult = m; + string n = m.ToString("0.0", CultureInfo.InvariantCulture); + b.AddButton(Math.Abs(mult - _sellMult) < 1e-9 ? $"[×{n}]" : "×" + n, () => { _sellMult = mult; Open(); }); + } + b.End(); + b.AddLabel(L.T("black.sell.info", "×" + _sellMult.ToString("0.0", CultureInfo.InvariantCulture), (int)Math.Round(BlackMarket.ExpectedDays(_sellMult)), (int)Math.Round(BlackMarket.TaintChance(_sellMult) * 100), (int)Math.Round(BlackMarket.TaintRatio(_sellMult) * 100)) + " " + L.T("black.sell.rating", d.Black.Rating)); + b.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(d.Hot.Avail(_payAsset)))); + b.AddSeparator(); + var items = BlackDelivery.Sellable(); + if (items.Count == 0) { b.AddLabel(L.T("black.sell.none")); return; } + Shelf(b, items.Select(it => + { + string id = BlackDelivery.SellId(it); + var kind = BlackDelivery.KindOf(id); + var g = new BmGood { Item = id, Kind = kind, Level = kind == GoodKind.Contraband ? BlackMarket.Pool.First(x => x.Key == id).Value : 3 }; + if (kind == GoodKind.Gpu) g.Health = Items.ToUnit(it).Health; + g.Price = BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), _sellMult); + return new { g, it }; + }).Select(x => { SellCards[x.g] = x.it; return x.g; }).ToList(), (c, g) => + { + var it = SellCards[g]; + GoodHead(c, g, L.T("black.sell.ref", BlackDelivery.RefValue(it))); + c.AddLabel(L.T("black.price", g.Price)); + c.AddFlexibleSpace(); + c.AddButton(L.T("black.sell.list"), () => ListItem(it)); + }); + } + + private static readonly System.Collections.Generic.Dictionary SellCards = new System.Collections.Generic.Dictionary(); + + private static void ListItem(GameItem it) + { + var d = Session.Data; + string id = BlackDelivery.SellId(it); + if (id == null) { Open(); return; } + var u = Items.IsGpu(it) ? Items.ToUnit(it) : null; + var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), _sellMult, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price); + if (s == null) { _message = L.T("msg.balance"); Open(); return; } + it.Destroy(); + _message = L.T("black.sell.listed", ItemName(id), s.ListPrice); + Session.Save(); + Open(); + } + private const int ShelfCols = 4; private const float CardW = 172f, CardH = 268f; @@ -411,6 +466,15 @@ namespace Decentralized Open(); } + private static void Delist(BmSale s) + { + var d = Session.Data; + d.Black.Delist(s); + Items.Give(BlackDelivery.Make(s.Item, s.Model, s.FakeOf, s.Health)); + Session.Save(); + Open(); + } + /// 我的订单与论坛情报。 private static void MinePage(CustomUIBuilder b, int day) { @@ -418,7 +482,14 @@ namespace Decentralized Columns(b, l => { l.AddHeader(L.T("black.mine.h")); - if (d.Black.Orders.Count == 0) l.AddLabel(L.T("black.mine.none")); + if (d.Black.Orders.Count == 0 && d.Black.Sales.Count == 0) l.AddLabel(L.T("black.mine.none")); + foreach (var sl in d.Black.Sales.Where(x => x.State != SaleState.Delisted || x.Why == "fake").OrderByDescending(x => x.ListedDay)) + { + var sale = sl; + string st = sale.State == SaleState.Sold ? L.T("black.sale.Sold", sale.PayDay) : L.T("black.sale." + sale.State + (sale.State == SaleState.Delisted && sale.Why != null ? "." + sale.Why : "")); + l.AddLabel(L.T("black.mine.sale", ItemName(sale.Item), sale.Mult.ToString("0.0", CultureInfo.InvariantCulture), st)); + 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.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 => diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 1110205..1080d22 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -654,6 +654,54 @@ internal static class Program 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, "关闭期间不能下单"); }); + Test("黑市出售", () => + { + Func price = a => a == Assets.CRT ? 1 : 100; + Check(Near(BlackMarket.ExpectedDays(0.6), 1, 1e-9) && Near(BlackMarket.ExpectedDays(0.9), 1, 1e-9) && Near(BlackMarket.ExpectedDays(1.0), 2, 1e-9) && Near(BlackMarket.ExpectedDays(1.8), 14, 1e-6), "期望成交天数:0.9 以下 1 天,1.0 两天,1.8 十四天"); + Check(BlackMarket.ExpectedDays(1.2) > BlackMarket.ExpectedDays(1.0) && BlackMarket.ExpectedDays(1.5) > BlackMarket.ExpectedDays(1.2), "倍率越高越慢"); + Check(BlackMarket.TaintChance(1.8) > BlackMarket.TaintChance(0.6) && BlackMarket.TaintRatio(1.8) > BlackMarket.TaintRatio(0.6) && Near(BlackMarket.TaintChance(0.6), Tuning.BmSaleTaintChanceMin, 1e-9), "倍率越高,带污点的概率和比例越高"); + var bm = new BlackMarket(); + var w = new Wallet(); w.Add(Assets.CRT, 1000); + Check(bm.List("c4", null, null, 0, 500, 2.0, w, Assets.CRT, 1, price) == null && bm.List("c4", null, null, 0, 500, 0.5, w, Assets.CRT, 1, price) == null, "倍率超出 0.6–1.8 不能上架"); + var s = bm.List("c4", null, null, 0, 500, 1.0, w, Assets.CRT, 1, price); + Check(s != null && s.ListPrice == 500 && Near(w.Get(Assets.CRT), 1000 - 25, 1e-9), "上架:标价 500,上架费 5%"); + int sold1 = 0, sold18 = 0; + for (int i = 0; i < 2000; i++) { if (new Rng((ulong)i + 1).Chance(bm.SaleChance(0.8))) sold1++; if (new Rng((ulong)i + 1).Chance(bm.SaleChance(1.8))) sold18++; } + Check(sold1 == 2000 && sold18 > 80 && sold18 < 220, $"每晚成交概率:0.8 倍必成交,1.8 倍约 1/14:{sold18}/2000"); + // 成交与到账:夜晚 1 成交(0.8 倍必成交),托管 3 天后到钱包 + var b2 = new BlackMarket(); + var w2 = new Wallet(); w2.Add(Assets.CRT, 1000); + var s2 = b2.List("c4", null, null, 0, 500, 0.8, w2, Assets.CRT, 1, price); + double crt0 = w2.Get(Assets.CRT); + var e1 = b2.Night(2, false, w2, new Rng(7), 0, price); + Check(s2.State == SaleState.Sold && s2.Proceeds == (int)Math.Round(400 * (1 - Tuning.BmCommission)) && s2.PayDay == 2 + Tuning.BmSaleHold && Near(w2.Get(Assets.CRT), crt0, 1e-9), "成交:扣抽成,货款先托管"); + b2.Night(4, false, w2, new Rng(8), 0, price); + Check(s2.State == SaleState.Sold, "没到放款日不动"); + b2.Night(5, false, w2, new Rng(9), 0, price); + Check(s2.State == SaleState.Paid && Near(w2.Get(Assets.CRT), crt0 + s2.Proceeds, 1e-9) && Near(w2.Taint, s2.Taint, 1e-9) && (s2.Taint == 0 || Near(s2.Taint, s2.Proceeds * BlackMarket.TaintRatio(0.8), 1e-6)), "放款:货款进钱包,污点按比例"); + // 假卡被看穿 + int fake = 0; + for (int i = 0; i < 400; i++) + { + var b3 = new BlackMarket(); var w3 = new Wallet(); w3.Add(Assets.CRT, 1000); + var s3 = b3.List("gpu:RTX70", "RTX70", "RTX60", 60, 500, 0.8, w3, Assets.CRT, 1, price); + b3.Night(2, false, w3, new Rng((ulong)i + 3), 0, price); + if (s3.State == SaleState.Delisted && s3.Why == "fake") { fake++; if (b3.Rating >= Tuning.BmRatingStart) throw new Exception("好评没降"); } + } + Check(fake > 150 && fake < 250, $"假卡约一半被看穿退货:{fake}/400"); + // 平台跑路:上架的货与未放款货款都没了 + var b4 = new BlackMarket(); var w4 = new Wallet(); w4.Add(Assets.CRT, 100000); + for (int i = 0; i < 30; i++) b4.List("c4", null, null, 0, 5000, 1.8, w4, Assets.CRT, 1, price); + bool rugged = false; + for (int d = 2; d < 400 && !rugged; d++) { var ev = b4.Night(d, false, w4, new Rng((ulong)d * 31), 0, price); rugged = ev.Any(e => e.Kind == "sale_rug"); } + Check(rugged && b4.Sales.All(x => x.State != SaleState.Listed && x.State != SaleState.Sold), "跑路:上架与未放款全部归零"); + // 镇压期间不推进 + var b5 = new BlackMarket(); var w5 = new Wallet(); w5.Add(Assets.CRT, 1000); + var s5 = b5.List("c4", null, null, 0, 500, 0.8, w5, Assets.CRT, 1, price); + b5.Night(2, true, w5, new Rng(1), 0, price); + Check(s5.State == SaleState.Listed, "镇压期间平台下线,不成交"); + }); + Console.WriteLine($"\n{_passed} 通过,{_failed} 失败"); return _failed == 0 ? 0 : 1; }