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:
1 parent
133367b58d
commit
09a5cae69c
11 files changed
+290
-16
No files matched your search
@@ -4,6 +4,9 @@
|
||||
|
||||
### 第二阶段(进行中)
|
||||
|
||||
- 人脉网络里本 mod 单独一页"Web3":入口(400)和混币器(700,需先加入)都放在这页。
|
||||
- 混币器:终端多一页"混币器"(买了升级才出现)。选币种、数量、1–5 轮,存入后每轮扣 2% 手续费并降低污点(基准每轮 35%,存入越大清洗越差,匿名集 = 2 万信用点背景量 + 存入量),轮数即等待天数,到期提回同一个地址;提回后一周内出金审查按"来自混币器"加重。镇压事件开始时 50% 被制裁 5 天,期间提出的币地址被标记,并上报纸;每晚 0.4% 跑路。夜间日志写提回与污点变化。开发者模式 F9 有 `Tainted wallet (test)`。游戏内验证了解锁、页面、存入与污点带走;夜间提回只有单元测试(测试存档里无法正常过夜)。
|
||||
|
||||
- 用币付款的顾客:排队的买家里,上层付 CRC(12%)、黑市付 CRT(25%)、革命派付 CRA(25%)、下层付 PSC(10%)。成交时扣回信用点(现金不变),按当日币价换成币进热钱包,顾客当场说一句"成交,N CRT 已转入热钱包";赃物款项不记销赃,改成币带污点(货值 × 赃物热度%);夜间日志汇总当天用币结算的额度。开发者模式 F9 有测试入口(Coin buyer)。已在游戏里实测:成交后现金不变,四笔种子按币价记入 CRT,顾客对话显示。
|
||||
|
||||
### 第一阶段完成(入门、挖矿、显卡鉴定、终端)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Decentralized.Domain
|
||||
{
|
||||
/// <summary>混币器里一笔在途的存入。</summary>
|
||||
public sealed class MixDeposit
|
||||
{
|
||||
public string Asset;
|
||||
public double Amount;
|
||||
/// <summary>存入时一并带走的污点与成本(信用点)。</summary>
|
||||
public double Taint, Basis;
|
||||
public int Rounds;
|
||||
public int ReadyDay;
|
||||
}
|
||||
|
||||
/// <summary>一笔混币结束时的结果,供夜间日志使用。</summary>
|
||||
public sealed class MixResult
|
||||
{
|
||||
public bool Rugged;
|
||||
public string Asset;
|
||||
public double Amount, TaintBefore, TaintAfter;
|
||||
public bool Flagged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 混币器:把币存进去,每轮扣手续费、降低污点,轮数 = 等待天数,到期提回同一个地址。
|
||||
/// 匿名集 = NPC 背景量 + 玩家存入量,玩家占比越大清洗效果越差。镇压事件开始时可能被制裁:
|
||||
/// 制裁期间提出的币视为被标记(出金审查按"来自混币器或被标记地址"加重,且地址被标记)。每晚有极小概率跑路。
|
||||
/// </summary>
|
||||
public sealed class Mixer
|
||||
{
|
||||
public List<MixDeposit> Pending = new List<MixDeposit>();
|
||||
public int SanctionedUntil = -1;
|
||||
/// <summary>提回的币最近一次从混币器出来的天数,用来给出金审查加重。</summary>
|
||||
public int LastReturnDay = -999;
|
||||
|
||||
public bool Sanctioned(int day) => day <= SanctionedUntil;
|
||||
|
||||
/// <summary>资金来自混币器:最近 AML 窗口内提回过币,或者被标记。</summary>
|
||||
public bool RecentlyMixed(int day) => day - LastReturnDay <= Tuning.AmlWindowDays;
|
||||
|
||||
/// <summary>一轮的清洗率:基准 × 匿名集系数。</summary>
|
||||
public static double RoundRate(double depositValue) => Tuning.MixerCleanRate * Tuning.MixerNpcVolume / (Tuning.MixerNpcVolume + Math.Max(0, depositValue));
|
||||
|
||||
/// <summary>存入:从钱包按价值比例带走污点与成本。轮数超出范围或余额不足返回 null。</summary>
|
||||
public MixDeposit Deposit(Wallet w, string asset, double amount, int rounds, int day, Func<string, double> price)
|
||||
{
|
||||
if (rounds < 1 || rounds > Tuning.MixerMaxRounds || amount <= 0 || w.Get(asset) + 1e-9 < amount) return null;
|
||||
double value = amount * price(asset);
|
||||
if (value < Tuning.MixerMin) return null;
|
||||
double total = w.Value(price);
|
||||
double share = total > 0 ? Math.Min(1, value / total) : 0;
|
||||
double taint = w.Taint * share, basis = w.Basis * share;
|
||||
w.Add(asset, -amount);
|
||||
w.Taint -= taint;
|
||||
w.Basis -= basis;
|
||||
var d = new MixDeposit { Asset = asset, Amount = amount, Taint = taint, Basis = basis, Rounds = rounds, ReadyDay = day + rounds };
|
||||
Pending.Add(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <summary>镇压事件开始:有一定概率被制裁,持续若干天。</summary>
|
||||
public void OnEvent(MarketEvent ev, int day, Rng rng)
|
||||
{
|
||||
if (ev == MarketEvent.Crackdown && rng.Chance(Tuning.MixerSanctionChance)) SanctionedUntil = day + Tuning.MixerSanctionDays;
|
||||
}
|
||||
|
||||
/// <summary>每晚:到期的存入提回钱包(污点按轮数降低),在途的各有小概率跑路。</summary>
|
||||
public List<MixResult> Night(Wallet w, int day, Func<string, double> price, Rng rng)
|
||||
{
|
||||
var results = new List<MixResult>();
|
||||
foreach (var d in Pending.ToList())
|
||||
{
|
||||
if (rng.Chance(Tuning.MixerRugChance))
|
||||
{
|
||||
Pending.Remove(d);
|
||||
results.Add(new MixResult { Rugged = true, Asset = d.Asset, Amount = d.Amount });
|
||||
continue;
|
||||
}
|
||||
if (day < d.ReadyDay) continue;
|
||||
Pending.Remove(d);
|
||||
double rate = RoundRate(d.Amount * price(d.Asset));
|
||||
double taint = d.Taint * Math.Pow(1 - rate, d.Rounds);
|
||||
double amount = d.Amount * Math.Pow(1 - Tuning.MixerFee, d.Rounds);
|
||||
bool flagged = Sanctioned(day);
|
||||
w.Add(d.Asset, amount);
|
||||
w.Taint += taint;
|
||||
w.Basis += d.Basis;
|
||||
if (flagged) w.Flagged = true;
|
||||
LastReturnDay = day;
|
||||
results.Add(new MixResult { Asset = d.Asset, Amount = amount, TaintBefore = d.Taint, TaintAfter = taint, Flagged = flagged });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ namespace Decentralized.Domain
|
||||
public const int AmlWindowDays = 7;
|
||||
public const double AmlTaintRatio = 0.10, AmlHeavyRatio = 0.30, AmlMixerBonus = 0.30, AmlMaxChance = 0.9;
|
||||
public const double TaxRate = 0.08;
|
||||
/// <summary>混币器:每轮手续费、基准清洗率、NPC 背景量(信用点)、最大轮数、最小存入、制裁概率与天数、每晚跑路概率。</summary>
|
||||
public const double MixerFee = 0.02, MixerCleanRate = 0.35, MixerNpcVolume = 20000, MixerMin = 100, MixerSanctionChance = 0.5, MixerRugChance = 0.004;
|
||||
public const int MixerMaxRounds = 5, MixerSanctionDays = 5;
|
||||
/// <summary>各派系顾客用币付款的概率。</summary>
|
||||
public const double CoinPayCrc = 0.12, CoinPayCrt = 0.25, CoinPayCra = 0.25, CoinPayPsc = 0.10;
|
||||
/// <summary>单个金融违规事件记入的罪证上限。</summary>
|
||||
|
||||
@@ -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,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,混币器
|
||||
|
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -498,6 +498,34 @@ internal static class Program
|
||||
Check(CoinPayment.Receive(w, Assets.CRC, 0, 1, 10) == 0, "零金额不收");
|
||||
});
|
||||
|
||||
Test("混币器:按价值比例带走污点、轮数即天数、手续费与清洗、匿名集、制裁标记", () =>
|
||||
{
|
||||
Func<string, double> px = a => a == Assets.PSC ? 10 : 1;
|
||||
var w = new Wallet { Address = "t" };
|
||||
w.Add(Assets.CRC, 1000); w.Add(Assets.PSC, 100); // 总价值 2000
|
||||
w.Taint = 400; w.Basis = 1000;
|
||||
var m = new Mixer();
|
||||
Check(m.Deposit(w, Assets.CRC, 1000, 9, 1, px) == null && m.Deposit(w, Assets.CRC, 50, 1, 1, px) == null, "轮数超限、低于最小额不收");
|
||||
var d = m.Deposit(w, Assets.CRC, 1000, 2, 1, px);
|
||||
Check(d != null && Near(d.Taint, 200, 1e-9) && Near(w.Taint, 200, 1e-9) && Near(w.Get(Assets.CRC), 0, 1e-9), "存入 1000 / 2000:带走一半污点");
|
||||
var rng = new Rng(3);
|
||||
Check(m.Night(w, 2, px, rng).Count == 0, "未到期不提回");
|
||||
var res = m.Night(w, 3, px, rng);
|
||||
Check(res.Count == 1 && !res[0].Rugged, "到期提回");
|
||||
double rate = Mixer.RoundRate(1000);
|
||||
Check(Near(w.Get(Assets.CRC), 1000 * 0.98 * 0.98, 1e-6) && Near(res[0].TaintAfter, 200 * (1 - rate) * (1 - rate), 1e-6), "两轮:手续费与污点按轮数递减");
|
||||
Check(Mixer.RoundRate(100000) < Mixer.RoundRate(1000) / 3, "存入越大匿名集占比越大,清洗越差");
|
||||
Check(m.RecentlyMixed(3) && !m.RecentlyMixed(3 + Tuning.AmlWindowDays + 1), "提回后一段时间内出金按混币来源加重");
|
||||
m.OnEvent(MarketEvent.Crackdown, 5, new Rng(1));
|
||||
int hit = 0; for (int i = 0; i < 400; i++) { var t = new Mixer(); t.OnEvent(MarketEvent.Crackdown, 1, new Rng((ulong)i + 1)); if (t.Sanctioned(2)) hit++; }
|
||||
Check(hit > 150 && hit < 250, $"镇压时约一半概率被制裁:{hit}/400");
|
||||
var w2 = new Wallet { Address = "u" }; w2.Add(Assets.CRC, 500);
|
||||
var m2 = new Mixer { SanctionedUntil = 10 };
|
||||
m2.Deposit(w2, Assets.CRC, 500, 1, 1, px);
|
||||
m2.Night(w2, 2, px, new Rng(5));
|
||||
Check(w2.Flagged, "制裁期间提出的币被标记");
|
||||
});
|
||||
|
||||
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
||||
return _failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user