Stage 4: Web3 black market spot counter and Ponero desk

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 19:35:48 +08:00
1 parent 74f0dac57e
commit f43cd487e5
11 files changed
+366 -1

No files matched your search

+8
View File
@@ -2,6 +2,14 @@
## 未发布 ## 未发布
### 第四阶段:Web3 黑市与 Ponero(进行中)
- 人脉网络新增"Web3 黑市"(前置:Web3 治理社区,1500):黑市派系在链上的实体;终端多一页"黑市",需要黑市声望 ≥ 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。
- 还没做:订单(托管、送货、死投点)、平台跑路、包裹截获、蜜罐、受控交付。
### 第三阶段:数值调优(进行中) ### 第三阶段:数值调优(进行中)
- 混币器每轮手续费 6% → 4%;用币付款的顾客概率提到上层 30%、黑市 50%、革命派 50%、下层 25%,并多付 12%(溢价只加在币数上,污点仍按货值算)。 - 混币器每轮手续费 6% → 4%;用币付款的顾客概率提到上层 30%、黑市 50%、革命派 50%、下层 25%,并多付 12%(溢价只加在币数上,污点仍按货值算)。
+129
View File
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>黑市现货柜台上的一件货:游戏物品 id、剩余件数、含溢价的单价(信用点)。</summary>
public sealed class BmOffer
{
public string Item;
public int Qty;
public int Price;
public int Level;
}
/// <summary>
/// Web3 黑市:黑市派系在链上的实体。现货柜台每周刷新一批违禁品(档次随黑市声望放开),只收币,脏币也收,
/// 付出去的币连同按比例的污点一起留在卖家那边;Ponero 兑换处把任意币兑成无法追踪的 Ponero(污点留在原地),有每日额度和手续费。
/// 镇压期间整个平台下线。
/// </summary>
public sealed class BlackMarket
{
public List<BmOffer> Offers = new List<BmOffer>();
public int RefreshedDay = -999;
/// <summary>Ponero 兑换处当天已兑换的价值(信用点)及其日期。</summary>
public double DeskToday;
public int DeskDay = -1;
/// <summary>现货池:游戏物品 id 与违禁等级(1 低、2 中、3 高)。全部来自游戏已有的违禁品。</summary>
public static readonly KeyValuePair<string, int>[] 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),
};
private static KeyValuePair<string, int> P(string id, int level) => new KeyValuePair<string, int>(id, level);
public static bool Open(int rep) => rep >= Tuning.BmOpenRep;
public static int MaxLevel(int rep) => rep >= Tuning.BmHighRep ? 3 : rep >= Tuning.BmMidRep ? 2 : rep >= Tuning.BmOpenRep ? 1 : 0;
public static bool DeskOpen(int rep) => rep >= Tuning.PoneroOpenRep;
public static int StockCount(int rep) => Tuning.BmStockBase + (int)Math.Max(0, rep / Tuning.BmStockPerRep);
/// <summary>溢价后的单价(信用点)。</summary>
public static int PriceOf(long baseValue) => (int)Math.Max(1, Math.Round(baseValue * (1 + Tuning.BmPremium)));
/// <summary>每周刷新一次(或首次开放时):按声望开放的档次抽 StockCount 种货,每种 1–3 件。value 给出物品的游戏基础价。</summary>
public bool Refresh(int day, int rep, Rng rng, Func<string, long> 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) });
}
return true;
}
/// <summary>买一件:付 asset 币。钱包里按价值比例带走的污点和成本留在卖家那边。成功返回 true,库存减一。</summary>
public bool Buy(BmOffer o, Wallet w, string asset, Func<string, double> price)
{
if (o == null || o.Qty <= 0 || asset == Assets.CRC) return false;
double p = price(asset);
if (p <= 0) return false;
double qty = o.Price / p;
if (w.Avail(asset) + 1e-9 < qty) return false;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, o.Price / total) : 0;
w.Taint -= w.Taint * share;
w.Basis -= w.Basis * share;
w.Add(asset, -qty);
o.Qty--;
if (o.Qty <= 0) Offers.Remove(o);
return true;
}
/// <summary>Ponero 对信用点的价格:跟随 PSC。</summary>
public static double PoneroPrice(double pscPrice) => pscPrice * Tuning.PoneroPscRatio;
public double DeskLeft(int day) => Math.Max(0, Tuning.PoneroDailyCap - (DeskDay == day ? DeskToday : 0));
/// <summary>兑入 Ponero:from 币 amount 个,扣手续费后按 Ponero 价到账;这部分价值带的污点留在原地(不跟着 Ponero)。返回到账数量,额度或余额不足返回 -1。</summary>
public double ToPonero(Wallet w, string from, double amount, int day, Func<string, double> price)
{
double value = amount * price(from);
if (from == Assets.Ponero || amount <= 0 || value <= 0 || w.Avail(from) + 1e-9 < amount || value > DeskLeft(day) + 1e-9) return -1;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, value / total) : 0;
w.Taint -= w.Taint * share;
w.Add(from, -amount);
double got = value * (1 - Tuning.PoneroFee) / price(Assets.Ponero);
w.Add(Assets.Ponero, got);
w.RecordBuy(Assets.Ponero, got, value);
Spend(day, value);
return got;
}
/// <summary>Ponero 兑出成 to 币:手续费同上,计入当天额度。返回到账数量,不足返回 -1。</summary>
public double FromPonero(Wallet w, string to, double amount, int day, Func<string, double> price)
{
double value = amount * price(Assets.Ponero);
if (to == Assets.Ponero || amount <= 0 || value <= 0 || w.Avail(Assets.Ponero) + 1e-9 < amount || value > DeskLeft(day) + 1e-9 || price(to) <= 0) return -1;
w.Add(Assets.Ponero, -amount);
double got = value * (1 - Tuning.PoneroFee) / price(to);
w.Add(to, got);
w.RecordBuy(to, got, value);
Spend(day, value);
return got;
}
private void Spend(int day, double value)
{
if (DeskDay != day) { DeskDay = day; DeskToday = 0; }
DeskToday += value;
}
}
}
+8
View File
@@ -83,6 +83,14 @@ namespace Decentralized.Domain
public const double CardValueFloor = 0.25; public const double CardValueFloor = 0.25;
/// <summary>非主交易对每晚被拉回参考价的比例,及其深度占主交易对的比例。</summary> /// <summary>非主交易对每晚被拉回参考价的比例,及其深度占主交易对的比例。</summary>
public const double SideArbRate = 0.5, SideDepthShare = 0.3; public const double SideArbRate = 0.5, SideDepthShare = 0.3;
/// <summary>Web3 黑市:开放所需黑市声望,中 / 高档货所需声望,现货每周刷新的天数与基础件数(每高 BmStockPerRep 声望多一件),现货溢价。</summary>
public const int BmOpenRep = 10, BmMidRep = 40, BmHighRep = 80, BmRefreshDays = 7, BmStockBase = 4;
public const double BmStockPerRep = 40, BmPremium = 0.30;
/// <summary>Ponero 兑换处:开放所需黑市声望、每次兑入 / 兑出的手续费、每天可兑换的额度(信用点)、对 PSC 的价格比。</summary>
public const int PoneroOpenRep = 30;
public const double PoneroFee = 0.08, PoneroDailyCap = 1000, PoneroPscRatio = 0.5;
/// <summary>黑市人脉升级的价格。</summary>
public const int BmUnlockPrice = 1500;
/// <summary>单个金融违规事件记入的罪证上限。</summary> /// <summary>单个金融违规事件记入的罪证上限。</summary>
public const double CrimeEventCap = 400; public const double CrimeEventCap = 400;
+18
View File
@@ -47,6 +47,24 @@ namespace Decentralized
w.Add(Assets.PSC, 100); w.Add(Assets.CRT, 1000); w.Taint += 3000; w.Basis += 2000; w.Add(Assets.PSC, 100); w.Add(Assets.CRT, 1000); w.Taint += 3000; w.Basis += 2000;
Log.Info($"钱包:PSC {w.Get(Assets.PSC):0.##},污点 {w.Taint:0}"); Log.Info($"钱包:PSC {w.Get(Assets.PSC):0.##},污点 {w.Taint:0}");
})); }));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Terminal (test)", () => Log.Guard("终端(测试)", TerminalWindow.Open));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market +50 rep, restock (test)", () => Log.Guard("黑市(测试)", () =>
{
StoreReputation.ModReputation("FACTION_BLACK_MARKET", 50);
if (!Unlock.BlackUnlocked) ModHelper.GetNetworkUpgrade(Unlock.BlackId)?.Unlock();
var d = Session.Data;
d.Black.RefreshedDay = -999;
d.Hot.Add(Assets.PSC, 20); d.Hot.Add(Assets.CRT, 3000); d.Hot.Taint += 2000; d.Hot.Basis += 1000;
d.Black.Refresh(StoreStation.GetDayCounter(), StoreReputation.GetBMReputation(), d.Rng, id =>
{
var it = ModHelper.SpawnItem(id);
if (it == null) return 0;
long v = it.GetBaseValue();
it.Destroy();
return v;
});
Log.Info($"黑市:声望 {StoreReputation.GetBMReputation()},上架 {d.Black.Offers.Count} 种:{string.Join(", ", d.Black.Offers.Select(o => o.Item + "×" + o.Qty + "@" + o.Price))},解锁 {Unlock.BlackUnlocked}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer (test)", () => Log.Guard("用币付款顾客(测试)", () => ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer (test)", () => Log.Guard("用币付款顾客(测试)", () =>
{ {
var c = new StoreClient var c = new StoreClient
@@ -206,3 +206,27 @@ nightlog.crt_freeze,The black market handed over addresses during the crackdown:
nightlog.thaw,Frozen {0} {1} thawed. nightlog.thaw,Frozen {0} {1} thawed.
news.crtfreeze.title,Black market freezes some CRT addresses for the crackdown 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." 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.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.empty,Sold out for this week.
black.offer,"{0} x{1} {2} credits"
black.buy,"Buy with coin ({0})"
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
black.desk.in,Into Ponero
black.desk.out,Out of Ponero
black.desk.go,Swap
black.desk.fail,Swap failed: balance, limit or amount is wrong.
1 # Probably Decentralized localization table: English (en)
206 nightlog.thaw
207 news.crtfreeze.title
208 news.crtfreeze.desc
209 tab.black
210 black.upgrade.title
211 black.upgrade.desc
212 black.offline
213 black.closed
214 black.sub.shop
215 black.sub.desk
216 black.shop.h
217 black.rep
218 black.empty
219 black.offer
220 black.buy
221 black.bought
222 black.pay.h
223 black.hint
224 black.desk.h
225 black.desk.hint
226 black.desk.left
227 black.desk.price
228 black.desk.swap
229 black.desk.in
230 black.desk.out
231 black.desk.go
232 black.desk.fail
@@ -205,3 +205,27 @@ nightlog.crt_freeze,镇压期间黑市交出了一批地址:你的 {0} CRT 被
nightlog.thaw,冻结的 {0} {1} 已解冻。 nightlog.thaw,冻结的 {0} {1} 已解冻。
news.crtfreeze.title,黑市配合镇压冻结部分 CRT 地址 news.crtfreeze.title,黑市配合镇压冻结部分 CRT 地址
news.crtfreeze.desc,"传闻黑市为换取放行,向安保交出了一批 CRT 地址。声望低的持有者更容易被冻结。" news.crtfreeze.desc,"传闻黑市为换取放行,向安保交出了一批 CRT 地址。声望低的持有者更容易被冻结。"
tab.black,黑市
black.upgrade.title,Web3 黑市
black.upgrade.desc,"解锁终端的黑市页:黑市派系在链上的实体。现货柜台每周上一批违禁品,只收币(PSC、CRT、CRA、Ponero),脏币也收,价格比正常渠道高;声望够高还有 Ponero 兑换处。需要黑市声望,镇压期间下线。"
black.offline,镇压期间,黑市的链上入口已经下线。
black.closed,"黑市声望达到 {0} 才会有人带你进去(现在 {1})。"
black.sub.shop,现货柜台
black.sub.desk,Ponero 兑换处
black.shop.h,现货柜台
black.rep,"黑市声望 {0},可见货物档次 {1}/3。每周刷新。"
black.empty,这周的货卖完了。
black.offer,"{0} ×{1} {2} 信用点等值"
black.buy,"用币购买({0})"
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,兑换
black.desk.in,兑入 Ponero
black.desk.out,兑出 Ponero
black.desk.go,兑换
black.desk.fail,兑换失败:余额、额度或数量不对。
1 # Probably Decentralized 本地化表:简体中文(zh)
205 nightlog.thaw
206 news.crtfreeze.title
207 news.crtfreeze.desc
208 tab.black
209 black.upgrade.title
210 black.upgrade.desc
211 black.offline
212 black.closed
213 black.sub.shop
214 black.sub.desk
215 black.shop.h
216 black.rep
217 black.empty
218 black.offer
219 black.buy
220 black.bought
221 black.pay.h
222 black.hint
223 black.desk.h
224 black.desk.hint
225 black.desk.left
226 black.desk.price
227 black.desk.swap
228 black.desk.in
229 black.desk.out
230 black.desk.go
231 black.desk.fail
+9
View File
@@ -101,6 +101,15 @@ namespace Decentralized
foreach (var t in d.Hot.Thaw(day)) PlayerStore.Instance.AddNightLog(L.T("nightlog.thaw", Fmt.Num(t.Value), t.Key)); foreach (var t in d.Hot.Thaw(day)) PlayerStore.Instance.AddNightLog(L.T("nightlog.thaw", Fmt.Num(t.Value), t.Key));
if (!wasSanctioned && d.Mixer.Sanctioned(day) && Unlock.MixerUnlocked) News.Show("pd_news_sanction_" + day, L.T("news.sanction.title"), L.T("news.sanction.desc"), 3); if (!wasSanctioned && d.Mixer.Sanctioned(day) && Unlock.MixerUnlocked) News.Show("pd_news_sanction_" + day, L.T("news.sanction.title"), L.T("news.sanction.desc"), 3);
d.ActiveEvents = events; d.ActiveEvents = events;
if (Unlock.BlackUnlocked && !events.Contains(MarketEvent.Crackdown))
d.Black.Refresh(day, StoreReputation.GetBMReputation(), d.Rng, id =>
{
var it = ModHelper.SpawnItem(id);
if (it == null) return 0;
long v = it.GetBaseValue();
it.Destroy();
return v;
});
d.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1); d.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1);
var units = Session.TonightCards.Select(c => (item: c.Key, unit: Items.ToUnit(c.Key), bonus: c.Value)).ToList(); var units = Session.TonightCards.Select(c => (item: c.Key, unit: Items.ToUnit(c.Key), bonus: c.Value)).ToList();
+2
View File
@@ -26,6 +26,7 @@ namespace Decentralized
public Wallet Hot = new Wallet { Address = "hot" }; public Wallet Hot = new Wallet { Address = "hot" };
public Exchange Exchange = new Exchange(); public Exchange Exchange = new Exchange();
public Mixer Mixer = new Mixer(); public Mixer Mixer = new Mixer();
public BlackMarket Black = new BlackMarket();
public List<MarketEvent> ActiveEvents = new List<MarketEvent>(); public List<MarketEvent> ActiveEvents = new List<MarketEvent>();
public NightReport LastNight; public NightReport LastNight;
public bool OpeningNewsShown; public bool OpeningNewsShown;
@@ -70,6 +71,7 @@ namespace Decentralized
{ {
if (asset == Assets.PSC) return Data.Market.Price; if (asset == Assets.PSC) return Data.Market.Price;
var spec = Assets.Get(asset); var spec = Assets.Get(asset);
if (asset == Assets.Ponero) return BlackMarket.PoneroPrice(Data.Market.Price);
if (spec != null && spec.Stable) return Data.Stable.Price(asset); if (spec != null && spec.Stable) return Data.Stable.Price(asset);
return 0; return 0;
} }
+117 -1
View File
@@ -13,7 +13,7 @@ namespace Decentralized
public static class TerminalWindow public static class TerminalWindow
{ {
public const string Id = "pd_terminal"; public const string Id = "pd_terminal";
private enum Page { Home, Bank, Poinbase, Web3, Report } private enum Page { Home, Bank, Poinbase, Web3, Black, Report }
private static Page _page = Page.Home; private static Page _page = Page.Home;
private static string _amount = ""; private static string _amount = "";
@@ -28,6 +28,7 @@ namespace Decentralized
b.BeginRow(); b.BeginRow();
foreach (Page p in Enum.GetValues(typeof(Page))) foreach (Page p in Enum.GetValues(typeof(Page)))
{ {
if (p == Page.Black && !Unlock.BlackUnlocked) continue;
var page = p; var page = p;
string name = L.T("tab." + p.ToString().ToLowerInvariant()); string name = L.T("tab." + p.ToString().ToLowerInvariant());
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; Open(); }); b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; Open(); });
@@ -41,6 +42,7 @@ namespace Decentralized
case Page.Bank: BankPage(b); break; case Page.Bank: BankPage(b); break;
case Page.Poinbase: ExchangePage(b, Venue.Exchange); break; case Page.Poinbase: ExchangePage(b, Venue.Exchange); break;
case Page.Web3: Web3Page(b); break; case Page.Web3: Web3Page(b); break;
case Page.Black: BlackPage(b); break;
default: ReportPage(b); break; default: ReportPage(b); break;
} }
b.End(); b.End();
@@ -262,6 +264,120 @@ namespace Decentralized
}); });
} }
private static bool _blackDesk;
private static string _deskAsset = Assets.PSC;
private static bool _deskIn = true;
private static string _payAsset = Assets.CRT;
/// <summary>Web3 黑市:黑市派系的链上实体。现货柜台(只收币)与 Ponero 兑换处。</summary>
private static void BlackPage(CustomUIBuilder b)
{
int rep = StoreReputation.GetBMReputation();
int day = StoreStation.GetDayCounter();
var d = Session.Data;
if (d.ActiveEvents.Contains(MarketEvent.Crackdown)) { b.AddLabel(L.T("black.offline")); return; }
if (!BlackMarket.Open(rep)) { b.AddLabel(L.T("black.closed", Tuning.BmOpenRep, rep)); return; }
bool desk = BlackMarket.DeskOpen(rep);
if (desk)
{
b.BeginRow();
b.AddButton(_blackDesk ? L.T("black.sub.shop") : $"[{L.T("black.sub.shop")}]", () => { _blackDesk = false; _message = null; Open(); });
b.AddButton(_blackDesk ? $"[{L.T("black.sub.desk")}]" : L.T("black.sub.desk"), () => { _blackDesk = true; _message = null; Open(); });
b.End();
}
if (desk && _blackDesk) { DeskPage(b, rep, day); return; }
var w = d.Hot;
Columns(b, l =>
{
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 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));
}
}, r =>
{
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 })
{
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))));
if (w.Taint > 0.5) r.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
});
}
private static string ItemName(string id)
{
var it = ModHelper.SpawnItem(id);
if (it == null) return id;
string n = it.name;
it.Destroy();
return n;
}
private static void BuyOffer(BmOffer o)
{
var d = Session.Data;
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));
Session.Save();
Open();
}
private static void DeskPage(CustomUIBuilder b, int rep, int day)
{
var d = Session.Data;
var w = d.Hot;
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);
}, r =>
{
r.AddHeader(L.T("black.desk.swap"));
r.BeginRow();
r.AddButton(_deskIn ? $"[{L.T("black.desk.in")}]" : L.T("black.desk.in"), () => { _deskIn = true; Open(); });
r.AddButton(_deskIn ? L.T("black.desk.out") : $"[{L.T("black.desk.out")}]", () => { _deskIn = false; Open(); });
r.End();
r.BeginRow();
foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA })
{
var asset = a;
r.AddButton(asset == _deskAsset ? $"[{asset}]" : asset, () => { _deskAsset = asset; Open(); });
}
r.End();
r.AddLabel(_deskIn ? L.T("mixer.balance", _deskAsset, Fmt.Num(w.Avail(_deskAsset))) : L.T("mixer.balance", Assets.Ponero, Fmt.Num(w.Avail(Assets.Ponero))));
AmountRow(r);
r.AddButton(L.T("black.desk.go"), DeskSwap);
});
}
private static void DeskSwap()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
int day = StoreStation.GetDayCounter();
double got = _deskIn ? d.Black.ToPonero(d.Hot, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(d.Hot, _deskAsset, v, day, Session.Price);
if (got < 0) { _message = L.T("black.desk.fail"); Open(); return; }
// 从 Ponero 出来的币来源不明:出金审查按"来自混币器"加重
if (!_deskIn) d.Mixer.LastReturnDay = day;
_message = _deskIn ? L.T("msg.trade", Fmt.Num(v), _deskAsset, Fmt.Num(got), Assets.Ponero) : L.T("msg.trade", Fmt.Num(v), Assets.Ponero, Fmt.Num(got), _deskAsset);
Session.Save();
Open();
}
private static bool _web3Mixer; private static bool _web3Mixer;
private static string _mixAsset = Assets.PSC; private static string _mixAsset = Assets.PSC;
private static int _mixRounds = 2; private static int _mixRounds = 2;
+4
View File
@@ -15,6 +15,9 @@ namespace Decentralized
public const int MixerPrice = Domain.Tuning.MixerUnlockPrice; public const int MixerPrice = Domain.Tuning.MixerUnlockPrice;
public static bool MixerUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(MixerId); public static bool MixerUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(MixerId);
public const string BlackId = "PD_BLACKMARKET";
public static bool BlackUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(BlackId);
public static bool IsUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(Id); public static bool IsUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(Id);
public static void Register() public static void Register()
@@ -26,6 +29,7 @@ namespace Decentralized
// 每个升级单独 try,免得前一个抛异常时后面的没登记上。 // 每个升级单独 try,免得前一个抛异常时后面的没登记上。
Add(Id, Price, () => L.T("upgrade.title"), () => L.T("upgrade.desc"), OnPurchased, "terminal", null); Add(Id, Price, () => L.T("upgrade.title"), () => L.T("upgrade.desc"), OnPurchased, "terminal", null);
Add(MixerId, MixerPrice, () => L.T("mixer.upgrade.title"), () => L.T("mixer.upgrade.desc"), () => Log.Info("混币器已解锁"), "ui_web3", new[] { Id }); Add(MixerId, MixerPrice, () => L.T("mixer.upgrade.title"), () => L.T("mixer.upgrade.desc"), () => Log.Info("混币器已解锁"), "ui_web3", new[] { Id });
Add(BlackId, Domain.Tuning.BmUnlockPrice, () => L.T("black.upgrade.title"), () => L.T("black.upgrade.desc"), () => Log.Info("Web3 黑市已解锁"), "ui_web3", new[] { Id });
} }
private static void Add(string id, int price, Func<string> title, Func<string> desc, Action onUnlock, string icon, string[] prerequisites) private static void Add(string id, int price, Func<string> title, Func<string> desc, Action onUnlock, string icon, string[] prerequisites)
@@ -560,6 +560,29 @@ internal static class Program
Check(low > 700 && low < 900 && high == 0, $"声望 0 约 40% 被冻:{low}/2000;声望 50 不会被冻:{high}"); Check(low > 700 && low < 900 && high == 0, $"声望 0 约 40% 被冻:{low}/2000;声望 50 不会被冻:{high}");
}); });
Test("Web3 黑市", () =>
{
Func<string, double> 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(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
var offer = new BmOffer { Item = "x", Qty = 1, Price = 500, Level = 1 };
bm.Offers.Add(offer);
Check(!bm.Buy(offer, w, Assets.CRC, price), "不收 CRC");
Check(bm.Buy(offer, w, Assets.CRT, price) && Near(w.Get(Assets.CRT), 500, 1e-9) && Near(w.Taint, 300, 1e-6) && Near(w.Basis, 600, 1e-6) && !bm.Offers.Contains(offer), "付币:污点与成本按比例留给卖家,卖完下架");
w = new Wallet { Address = "t" };
w.Add(Assets.PSC, 10); w.Taint = 500; w.Basis = 1000; // 总价值 1000,PSC 价 100
double got = bm.ToPonero(w, Assets.PSC, 5, 20, price); // 价值 500
Check(Near(got, 500 * (1 - Tuning.PoneroFee) / 50, 1e-9) && Near(w.Taint, 250, 1e-6) && Near(w.Get(Assets.PSC), 5, 1e-9), "兑入 Ponero:扣手续费,这部分污点留在原地");
Check(bm.ToPonero(w, Assets.PSC, 5.1, 20, price) < 0, "超过每日额度不能兑");
Check(bm.ToPonero(w, Assets.PSC, 5, 21, price) >= 0, "第二天额度恢复");
double back = bm.FromPonero(w, Assets.CRT, 10, 22, price);
Check(Near(back, 10 * 50 * (1 - Tuning.PoneroFee), 1e-9) && Near(w.Get(Assets.CRT), back, 1e-9), "兑出 Ponero");
});
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败"); Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
return _failed == 0 ? 0 : 1; return _failed == 0 ? 0 : 1;
} }