Phase 2: mixer (Web3 tab in the network, terminal page, sanctions, night return), unlock registration per upgrade

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 16:39:40 +08:00
1 parent 133367b58d
commit 09a5cae69c
11 files changed
+290 -16

No files matched your search

+6
View File
@@ -15,6 +15,12 @@ namespace Decentralized
public static void Subscribe()
{
ModHook.OnGenerateCustomerLate += OnGenerate;
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tainted wallet (test)", () => Log.Guard("带污点的钱包(测试)", () =>
{
var w = Session.Data.Hot;
w.Add(Assets.PSC, 100); w.Taint += 3000; w.Basis += 2000;
Log.Info($"钱包:PSC {w.Get(Assets.PSC):0.##},污点 {w.Taint:0}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer (test)", () => Log.Guard("用币付款顾客(测试)", () =>
{
var c = new StoreClient
@@ -1,5 +1,6 @@
# Probably Decentralized localization table: English (en)
# Source language and fallback. Format: key,text (text may be quoted; \n is a line break).
upgrade.tab,Web3
upgrade.title,Web3 Governance Community
upgrade.desc,"Members receive a trading terminal, a mining rig, an RZX 60 graphics card and a graphics card appraisal guide. The terminal holds the bank, the Poinbase exchange and the Web3 wallet. More cards and rigs are sold by Dr. Jackson."
item.terminal.name,Trading Terminal
@@ -174,3 +175,27 @@ stable.spiral,"Collapsing"
aml.window,"Withdrawn in the last {0} days: {1} credits (cumulative line {2})"
aml.taint,"Wallet taint share: {0}%"
aml.chance,"At this amount, estimated review chance: {0}%"
mixer.h,Mixer
mixer.ok,Status: operating normally
mixer.sanctioned,Status: sanctioned ({0} more day(s)). Coins withdrawn meanwhile are flagged.
mixer.pending,Pending deposits
mixer.none,No pending deposits.
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}."
mixer.go,Deposit to mixer
mixer.min,Deposits must be worth at least {0} credits.
mixer.done,Deposited {0} {1}; back on day {2}.
mixer.upgrade.title,Mixer
mixer.upgrade.desc,"Unlocks the Mixer page in the terminal: deposit tainted coins, get them back days later with less taint. Cash-outs of freshly mixed coins are screened harder; during crackdowns the mixer may be sanctioned and coins withdrawn meanwhile are flagged."
news.sanction.title,Mixer put on sanctions list
news.sanction.desc,"Security has sanctioned a batch of mixing services. Funds withdrawn from mixers in the next few days will be treated as suspicious."
nightlog.mixer,Mixer returned {0} {1}; taint {2} -> {3}.
nightlog.mixer_flag, (withdrawn during sanctions; address flagged)
nightlog.mixer_rug,The mixer's operator ran off with it: {0} {1} never came back.
tab.mixer,Mixer
1 # Probably Decentralized localization table: English (en)
2 # Source language and fallback. Format: key
3 upgrade.tab
4 upgrade.title
5 upgrade.desc
6 item.terminal.name
175 aml.window
176 aml.taint
177 aml.chance
178 mixer.h
179 mixer.ok
180 mixer.sanctioned
181 mixer.pending
182 mixer.none
183 mixer.col.taint
184 mixer.col.ready
185 mixer.day
186 mixer.deposit
187 mixer.hint
188 mixer.balance
189 mixer.rounds
190 mixer.preview
191 mixer.go
192 mixer.min
193 mixer.done
194 mixer.upgrade.title
195 mixer.upgrade.desc
196 news.sanction.title
197 news.sanction.desc
198 nightlog.mixer
199 nightlog.mixer_flag
200 nightlog.mixer_rug
201 tab.mixer
@@ -1,4 +1,5 @@
# Probably Decentralized 本地化表:简体中文(zh)
upgrade.tab,Web3
upgrade.title,Web3 治理社区
upgrade.desc,"成员可获得一台交易终端、一个矿架、一张 RZX 60 显卡和一本显卡鉴定手册。终端里有银行、Poinbase 交易所和 Web3 钱包。更多显卡和矿架由杰克逊博士出售。"
item.terminal.name,交易终端
@@ -173,3 +174,27 @@ stable.spiral,"崩盘中"
aml.window,"近 {0} 天已提取 {1} 信用点(累计线 {2})"
aml.taint,"钱包污点比例:{0}%"
aml.chance,"按这个数量,预计审查概率:{0}%"
mixer.h,混币器
mixer.ok,状态:运行正常
mixer.sanctioned,状态:已被制裁(还有 {0} 天),此期间提出的币视为被标记。
mixer.pending,在途存入
mixer.none,没有在途的存入。
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} 天到期。"
mixer.go,存入混币器
mixer.min,存入价值不得低于 {0} 信用点。
mixer.done,已存入 {0} {1},第 {2} 天提回。
mixer.upgrade.title,混币器
mixer.upgrade.desc,"解锁终端里的混币器页:把带污点的币存进去,几天后提回,污点降低。出金审查对刚从混币器出来的币更严格;镇压期间混币器可能被制裁,期间提出的币会被标记。"
news.sanction.title,混币器被列入制裁名单
news.sanction.desc,"安保部门宣布制裁一批混币服务。此后几天从混币器提出的资金将被视为可疑。"
nightlog.mixer,混币器提回 {0} {1},污点 {2} → {3}。
nightlog.mixer_flag,(制裁期间提出,地址被标记)
nightlog.mixer_rug,混币器的运营者卷款跑路了:{0} {1} 没有提回。
tab.mixer,混币器
1 # Probably Decentralized 本地化表:简体中文(zh)
2 upgrade.tab
3 upgrade.title
4 upgrade.desc
5 item.terminal.name
174 aml.window
175 aml.taint
176 aml.chance
177 mixer.h
178 mixer.ok
179 mixer.sanctioned
180 mixer.pending
181 mixer.none
182 mixer.col.taint
183 mixer.col.ready
184 mixer.day
185 mixer.deposit
186 mixer.hint
187 mixer.balance
188 mixer.rounds
189 mixer.preview
190 mixer.go
191 mixer.min
192 mixer.done
193 mixer.upgrade.title
194 mixer.upgrade.desc
195 news.sanction.title
196 news.sanction.desc
197 nightlog.mixer
198 nightlog.mixer_flag
199 nightlog.mixer_rug
200 tab.mixer
+12 -1
View File
@@ -82,7 +82,9 @@ namespace Decentralized
var d = Session.Data;
int day = StoreStation.GetDayCounter();
var events = ActiveEvents();
foreach (var ev in events.Where(e => !d.ActiveEvents.Contains(e))) d.Stable.OnEventStart(ev, day, d.Rng);
bool wasSanctioned = d.Mixer.Sanctioned(day);
foreach (var ev in events.Where(e => !d.ActiveEvents.Contains(e))) { d.Stable.OnEventStart(ev, day, d.Rng); d.Mixer.OnEvent(ev, day, d.Rng); }
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.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1);
@@ -117,6 +119,15 @@ namespace Decentralized
var ps = PlayerStore.Instance;
if (mined > 0) ps.AddNightLog(L.T("nightlog.mined", Fmt.Num(mined)));
foreach (var m in d.Mixer.Night(d.Hot, day, Session.Price, d.Rng))
{
if (m.Rugged) ps.AddNightLog(L.T("nightlog.mixer_rug", Fmt.Num(m.Amount), m.Asset), "cc7755");
else
{
string line = L.T("nightlog.mixer", Fmt.Num(m.Amount), m.Asset, Fmt.Credits(m.TaintBefore), Fmt.Credits(m.TaintAfter)) + (m.Flagged ? L.T("nightlog.mixer_flag") : "");
if (m.Flagged) ps.AddNightLog(line, "cc7755"); else ps.AddNightLog(line);
}
}
foreach (var name in report.Burnt) ps.AddNightLog(L.T("nightlog.burnt", name), "cc7755");
int unpowered = report.Rigs - report.PoweredRigs;
if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755");
+1
View File
@@ -25,6 +25,7 @@ namespace Decentralized
public Network Network = new Network();
public Wallet Hot = new Wallet { Address = "hot" };
public Exchange Exchange = new Exchange();
public Mixer Mixer = new Mixer();
public List<MarketEvent> ActiveEvents = new List<MarketEvent>();
public NightReport LastNight;
public bool OpeningNewsShown;
+70 -2
View File
@@ -13,7 +13,7 @@ namespace Decentralized
public static class TerminalWindow
{
public const string Id = "pd_terminal";
private enum Page { Home, Bank, Poinbase, Web3, Report }
private enum Page { Home, Bank, Poinbase, Web3, Mixer, Report }
private static Page _page = Page.Home;
private static string _amount = "";
@@ -28,6 +28,7 @@ namespace Decentralized
b.BeginRow();
foreach (Page p in Enum.GetValues(typeof(Page)))
{
if (p == Page.Mixer && !Unlock.MixerUnlocked) continue;
var page = p;
string name = L.T("tab." + p.ToString().ToLowerInvariant());
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.Poinbase: ExchangePage(b, Venue.Exchange); break;
case Page.Web3: Web3Page(b); break;
case Page.Mixer: MixerPage(b); break;
default: ReportPage(b); break;
}
b.End();
@@ -252,6 +254,72 @@ namespace Decentralized
});
}
private static string _mixAsset = Assets.PSC;
private static int _mixRounds = 2;
private static readonly float[] MixW = { 70f, 100f, 100f, 80f };
/// <summary>混币器:把币存进去,几轮之后提回同一个地址,污点降低,每轮扣手续费。</summary>
private static void MixerPage(CustomUIBuilder b)
{
var d = Session.Data;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
Columns(b, l =>
{
l.AddHeader(L.T("mixer.h"));
l.AddLabel(d.Mixer.Sanctioned(day) ? L.T("mixer.sanctioned", d.Mixer.SanctionedUntil - day + 1) : L.T("mixer.ok"));
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
l.AddSeparator();
l.AddHeader(L.T("mixer.pending"));
if (d.Mixer.Pending.Count == 0) l.AddLabel(L.T("mixer.none"));
else
{
TableHeader(l, MixW, L.T("col.asset"), L.T("col.amount"), L.T("mixer.col.taint"), L.T("mixer.col.ready"));
foreach (var p in d.Mixer.Pending)
TableRow(l, MixW, p.Asset, Fmt.Num(p.Amount), Fmt.Credits(p.Taint), L.T("mixer.day", p.ReadyDay));
}
}, 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 })
{
var asset = a;
r.AddButton(asset == _mixAsset ? $"[{asset}]" : asset, () => { _mixAsset = asset; Open(); });
}
r.End();
r.AddLabel(L.T("mixer.balance", _mixAsset, Fmt.Num(w.Get(_mixAsset))));
AmountRow(r);
r.AddLabel(L.T("mixer.rounds"));
r.BeginRow();
for (int n = 1; n <= Tuning.MixerMaxRounds; n++)
{
int rounds = n;
r.AddButton(rounds == _mixRounds ? $"[{rounds}]" : rounds.ToString(), () => { _mixRounds = rounds; Open(); });
}
r.End();
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0)
{
double rate = Mixer.RoundRate(v * Session.Price(_mixAsset));
r.AddLabel(L.T("mixer.preview", Fmt.Num(v * Math.Pow(1 - Tuning.MixerFee, _mixRounds)), _mixAsset, (int)Math.Round((1 - Math.Pow(1 - rate, _mixRounds)) * 100), day + _mixRounds));
}
r.AddButton(L.T("mixer.go"), MixDeposit);
});
}
private static void MixDeposit()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
int day = StoreStation.GetDayCounter();
if (d.Hot.Get(_mixAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var dep = d.Mixer.Deposit(d.Hot, _mixAsset, v, _mixRounds, day, Session.Price);
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), _mixAsset, dep.ReadyDay);
Session.Save();
Open();
}
private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none");
private static void ReportPage(CustomUIBuilder b)
@@ -299,7 +367,7 @@ namespace Decentralized
if (!ReadAmount(out double v)) return;
var d = Session.Data;
if (d.Hot.Get(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var r = d.Exchange.Cashout(d.Hot, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), false, d.Rng);
var r = d.Exchange.Cashout(d.Hot, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), d.Mixer.RecentlyMixed(StoreStation.GetDayCounter()), d.Rng);
int got = (int)Math.Floor(r.Received - r.Tax);
ModHelper.AddCredit(got, true);
_message = L.T("msg.cashout", got, Fmt.Credits(r.Tax));
+19 -13
View File
@@ -8,24 +8,30 @@ namespace Decentralized
public const string Id = "PD_WEB3_COMMUNITY";
public const int Price = 400;
private const string Card = "Service";
/// <summary>人脉网络里本 mod 自己的一页:入口、混币器等 Web3 相关升级都放在这里。</summary>
public const string Tab = "PD_WEB3";
public const string MixerId = "PD_MIXER";
public const int MixerPrice = 700;
public static bool MixerUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(MixerId);
public static bool IsUnlocked => PlayerStore.Instance != null && ModHelper.IsNetworkUpgradeUnlocked(Id);
public static void Register()
{
try
{
// 分类用游戏自己的 "Service" 键,和 Probably Automated 一样归进现有的"服务"栏;
// 标题和说明用回调:OnEnable 时 mod 本地化表还没加载
ModHelper.AddNetworkUpgrade(ModInfo.Id, Id, Price, Card,
() => L.T("upgrade.title"), () => L.T("upgrade.desc"), OnPurchased,
iconKey: ModInfo.Id + ":terminal");
}
catch (NullReferenceException)
{
// 登记定义后游戏会访问 PlayerStore.Instance,主菜单阶段为空会抛异常;定义已登记,读档时游戏会把升级补进这一局。
Log.Info("人脉升级已登记(主菜单阶段访问 PlayerStore 失败,已忽略)");
}
// 分类用游戏自己的 "Service" 键,和 Probably Automated 一样归进现有的"服务"栏;
// 标题和说明用回调:OnEnable 时 mod 本地化表还没加载
ModHelper.SetNetworkTabLabel(Tab, () => L.T("upgrade.tab"));
// 登记定义后游戏会访问 PlayerStore.Instance,主菜单阶段为空会抛异常;定义已登记,读档时游戏会把升级补进这一局。
// 每个升级单独 try,免得前一个抛异常时后面的没登记上。
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 });
}
private static void Add(string id, int price, Func<string> title, Func<string> desc, Action onUnlock, string icon, string[] prerequisites)
{
try { ModHelper.AddNetworkUpgrade(ModInfo.Id, id, price, Card, title, desc, onUnlock, tab: Tab, iconKey: ModInfo.Id + ":" + icon, prerequisites: prerequisites); }
catch (NullReferenceException) { Log.Info($"人脉升级 {id} 已登记(主菜单阶段访问 PlayerStore 失败,已忽略)"); }
}
private static void OnPurchased()