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} 失败");