Stage 9.2: mixer operator Tumbler storyline with key drive item

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-04 09:12:47 +08:00
1 parent 62bd641283
commit a5f9639970
15 files changed
+386 -6

No files matched your search

+9
View File
@@ -10,6 +10,15 @@
- 伦警官被捕:14 天内出金审查、操纵检出、内幕查处 ×1.3(和治安部整顿叠加)。
- 无尽模式关闭角色剧情线,这些选择不会出现。放贷人 Lou 被捕没有接(借贷暂缓)。
- 游戏内验证:调试入口设置"证据给革命军、泄露数据盘、伦警官被捕"后过一夜("Run night (test)"),日志三条"剧情联动生效",第二天报纸头版登出三条新闻。没有在游戏里验证:真实剧情对话里做出选择后的联动、各倍率在游戏里的实际效果(单元测试覆盖:出金命中率上升、CRT 脱锚减半、SPARK 推涨合计 ×1.2、叠加)。
- 9.2 本 mod 自己的剧情线:混币器运营者 Tumbler。解锁混币器、第 30 天起,四次上门(游戏的对话顾客,开始日、+3、+5、+9 天,错过的往后补):
1. 自我介绍:混币器是他写的。
2. 治安部在查链上混币服务。当晚混币器节点被查封 4 天,报纸登"治安部查封混币节点"。
3. 求你藏密钥盘,三选一(游戏自己的选项界面):
- 藏好:给你一件"密钥盘"(违禁品,高级,价值 600,游戏自己的搜查会翻出来记罪证)。第 4 次他来取:密钥盘还在就收走,留 1,000 CRT 谢礼进当前钱包,混币器手续费永久减半、匿名集 ×1.5;弄丢了(被搜走、卖掉)他只说"算了"。
- 举报:马上拿 1,200 信用点赏金、治安部声望 +8,混币器永久关停(在途的照常提回,不能再存),报纸"混币器运营者落网",第 4 次不再上门。
- 卖给黑市:3,000 CRT 进当前钱包(一半带污点)、黑市声望 +5,之后混币器手续费 ×1.5、跑路概率 ×4,报纸"混币服务易主";第 4 次他上门质问。
- 新画了密钥盘的像素图(深紫数据棒、金属接口、红色指示灯)。
- 游戏内验证:调试入口"Tumbler: next visit now"依次叫进四次,第 1、2、3 次对话正常,第 3 次出现三个选项;选"藏好"后日志"选择 1";第 4 次取回密钥盘,晨间日志"Tumbler 取回了密钥盘,留下 1,000 CRT 谢礼(已转入 主钱包),混币器手续费减半"。没有在游戏里验证:举报和卖给黑市两条结局(单元测试只覆盖倍率)、第 2 次之后的查封和新闻、搜查翻出密钥盘、自然触发(第 30 天、已解锁混币器)。
- 报纸修复:报纸按重要度排序、最多登 4 条;mod 新闻原来的重要度是 2,游戏事件一多就被挤掉(此前各阶段"报纸新闻没有验证"的原因)。改为默认 5(游戏事件多在 4–7),剧情联动 9。
### 行情图
+16
View File
@@ -649,6 +649,21 @@ def hw_wallet():
return c
def key_drive():
"""Tumbler 的密钥盘(16×16,一格):斜放的旧式数据棒,深紫机身、金属接口、一枚红色指示灯和手写标签。"""
c = Canvas(16, 16)
c.rect(3, 4, 12, 11, 'V') # 机身
c.rect(3, 4, 12, 4, 'v'); c.rect(3, 4, 3, 11, 'v') # 受光边
c.rect(4, 11, 12, 11, 'n'); c.rect(12, 5, 12, 11, 'n') # 背光边
c.rect(13, 6, 15, 9, 'l'); c.rect(13, 9, 15, 9, 'm') # 金属接口
c.put(14, 7, 'k'); c.put(14, 8, 'k')
c.rect(5, 6, 9, 9, 'w') # 手写标签
c.rect(6, 7, 8, 7, 'k'); c.rect(6, 8, 7, 8, 'k')
c.put(11, 6, 'R') # 指示灯
outline(c)
return c
SPRITES = {
'gpu_guide': guide,
'gpu_burnt_f': lambda: gpu_front('e', 'c', burnt=True),
@@ -658,6 +673,7 @@ SPRITES = {
'rig': rig,
'terminal': terminal,
'hw_wallet': hw_wallet,
'key_drive': key_drive,
'asic_m1': lambda: asic('M1'),
'asic_m2': lambda: asic('M2'),
'asic_m3': lambda: asic('M3'),
+6 -3
View File
@@ -44,7 +44,10 @@ namespace Decentralized.Domain
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>每轮手续费(剧情可能改动)。</summary>
public static double Fee => Math.Min(0.5, Tuning.MixerFee * StoryMods.Current.MixerFee);
public static double RoundRate(double depositValue) => Tuning.MixerCleanRate * Tuning.MixerNpcVolume * StoryMods.Current.MixerVolume / (Tuning.MixerNpcVolume * StoryMods.Current.MixerVolume + Math.Max(0, depositValue));
/// <summary>存入:从钱包按价值比例带走污点与成本。轮数超出范围或余额不足返回 null。</summary>
public MixDeposit Deposit(Wallet w, string asset, double amount, int rounds, int day, Func<string, double> price)
@@ -78,7 +81,7 @@ namespace Decentralized.Domain
var results = new List<MixResult>();
foreach (var d in Pending.ToList())
{
if (rng.Chance(Tuning.MixerRugChance))
if (rng.Chance(Math.Min(1, Tuning.MixerRugChance * StoryMods.Current.MixerRug)))
{
Pending.Remove(d);
results.Add(new MixResult { Rugged = true, Asset = d.Asset, Amount = d.Amount });
@@ -88,7 +91,7 @@ namespace Decentralized.Domain
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);
double amount = d.Amount * Math.Pow(1 - Fee, d.Rounds);
bool flagged = Sanctioned(day);
var w = wallet(d.Wallet);
w.Add(d.Asset, amount);
+55
View File
@@ -24,6 +24,11 @@ namespace Decentralized.Domain
/// <summary>SPARK 每晚额外乘的倍数,还剩几晚。</summary>
public double SparkPush = 1;
public int SparkPushNights;
/// <summary>混币器:手续费、匿名集(NPC 混币量)、跑路概率的倍率;被举报后永久关闭(在途的照常提回,不能再存)。</summary>
public double MixerFee = 1, MixerVolume = 1, MixerRug = 1;
public bool MixerClosed;
/// <summary>本 mod 自己的剧情线:混币器运营者 Tumbler。</summary>
public Tumbler Tumbler = new Tumbler();
public bool Has(string key) => Applied.Contains(key);
@@ -71,6 +76,15 @@ namespace Decentralized.Domain
case "lun_arrested":
LunUntil = day + Tuning.StoryLunDays;
break;
case "tumbler_kept":
MixerFee *= Tuning.TumblerKeptFee; MixerVolume *= Tuning.TumblerKeptVolume;
break;
case "tumbler_reported":
MixerClosed = true;
break;
case "tumbler_sold":
MixerFee *= Tuning.TumblerSoldFee; MixerRug *= Tuning.TumblerSoldRug;
break;
}
return true;
}
@@ -85,4 +99,45 @@ namespace Decentralized.Domain
SparkPushNights = n;
}
}
/// <summary>
/// 混币器运营者 Tumbler 的剧情线:解锁混币器、第 <see cref="Tuning.TumblerMinDay"/> 天起开始,四次上门。
/// 1 自我介绍 → 2 说治安部在追查混币器(当晚混币器被制裁)→ 3 求你藏密钥盘(三选一:藏 / 举报 / 卖给黑市)→ 4 结局。
/// 只记进度,对话和物品在接入层。
/// </summary>
public sealed class Tumbler
{
public const int Hide = 1, Report = 2, Sell = 3;
public int StartDay = -1, Stage, Choice, VisitedDay = -1;
public bool Done;
/// <summary>第几次上门在哪一天(相对开始日)。</summary>
public static readonly int[] Offsets = { 0, 3, 5, 9 };
public int NextVisitDay => StartDay < 0 || Stage >= Offsets.Length ? -1 : StartDay + Offsets[Stage];
/// <summary>每天开门时问:是否开始(明天第一次上门)。</summary>
public bool TryStart(int day, bool mixerUnlocked)
{
if (StartDay >= 0 || Done || !mixerUnlocked || day < Tuning.TumblerMinDay) return false;
StartDay = day + 1;
return true;
}
/// <summary>今天该上门的是第几次(1–4),没有就返回 0(第 4 次举报过的话由接入层改成新闻:他被抓了)。</summary>
public int Due(int day)
{
if (Done || StartDay < 0 || Stage >= Offsets.Length || VisitedDay == day) return 0;
if (day < NextVisitDay) return 0;
return Stage + 1;
}
/// <summary>这一次上门已经安排了。</summary>
public void Visited(int day)
{
VisitedDay = day;
Stage++;
Done = Stage >= Offsets.Length;
}
}
}
+7
View File
@@ -244,6 +244,13 @@ namespace Decentralized.Domain
public const double StorySecAmlMult = 1.5, StorySecSanctionMult = 1.5, StorySecEnforceMult = 1.3;
public const double StoryLeakHaircutMult = 2, StoryLeakSpark = 1.15, StoryReturnCrcMult = 0.5;
public const int StoryLunDays = 14, StorySparkNights = 5;
/// <summary>混币器运营者 Tumbler:第几天起开始;帮他藏好密钥盘后的混币器手续费、匿名集倍率和谢礼(CRT);举报的赏金(信用点)和治安部声望;
/// 卖给黑市得的 CRT(一半带污点)、黑市声望、之后混币器的手续费和跑路倍率;密钥盘的价值(违禁品,搜到按它记罪证)。</summary>
public const int TumblerMinDay = 30;
public const double TumblerKeptFee = 0.5, TumblerKeptVolume = 1.5, TumblerGift = 1000;
public const int TumblerBounty = 1200, TumblerSecRep = 8, TumblerBmRep = 5;
public const double TumblerSellCrt = 3000, TumblerSoldFee = 1.5, TumblerSoldRug = 4;
public const int TumblerDriveValue = 600;
public const double StoryLunMult = 1.3;
/// <summary>行情图的 NPC 成交量:每天的基础量占主池深度的比例(按币种),价格波动越大成交越多(|涨跌幅| × 倍数),再乘 0.6–1.4 的随机。</summary>
+11
View File
@@ -138,6 +138,17 @@ namespace Decentralized
cd.serpicoDocumentChoice = SerpicoStoryline.CHOICE_REVOLUTION; cd.eleanorDriveChoice = 2; cd.officerLunArrested = true;
Log.Info("测试:剧情选择已设置(革命军 / 泄露 / 伦警官被捕),今晚生效");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: next visit now (test)", () => Log.Guard("Tumbler 测试", () =>
{
// 把 Tumbler 的下一次上门挪到今天,并立刻叫进来
var t = Session.Data.Story.Tumbler; int day = StoreStation.GetDayCounter();
if (t.StartDay < 0) t.StartDay = day;
t.StartDay = day - Tumbler.Offsets[System.Math.Min(t.Stage, Tumbler.Offsets.Length - 1)]; t.VisitedDay = -1;
PlayerStore.Instance.currentClientInstance = null;
var m = PlayerStore.Instance.storeClientManager;
TumblerStory.ForceVisit(m);
PlayerStore.Instance.TryCallNextClient();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Predict: window = tomorrow (test)", () => { foreach (var m in Session.Data.Predict.Markets) { m.FromDay = m.ToDay = StoreStation.GetDayCounter() + 1; } Log.Info("测试:所有盘口窗口改成明天"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Perp fund 0 (test)", () => { Session.Data.Perps.InsuranceFund = 0; Log.Info("测试:保险基金清零"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "SPARK x0.3 (test)", () => { var st = Session.Data.Stable; st.Spark *= 0.3; Log.Info($"测试:SPARK 价格 {st.Spark:0.00}"); });
+15
View File
@@ -19,6 +19,7 @@ namespace Decentralized
public const string Web3Guide = "pd_web3_guide";
public const string GpuPrefix = "pd_gpu_";
public const string HwWallet = "pd_hw_wallet";
public const string KeyDrive = "pd_key_drive";
/// <summary>硬件钱包被动过手脚(供应链投毒)。外观、说明都不显示。</summary>
public const string PoisonTag = "PD_POISONED";
@@ -47,6 +48,7 @@ namespace Decentralized
dir.Add(Guide, CreateGuide);
dir.Add(Web3Guide, CreateWeb3Guide);
dir.Add(HwWallet, CreateHwWallet);
dir.Add(KeyDrive, CreateKeyDrive);
AsicItems.Register(dir);
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap))
{
@@ -128,6 +130,19 @@ namespace Decentralized
return item;
}
/// <summary>Tumbler 的密钥盘:违禁品(高),搜查时被翻出来按价值记罪证。</summary>
private static GameItem CreateKeyDrive()
{
var item = ItemDirectory.CreateEmptyItem(KeyDrive);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "key_drive");
item.name = L.T("item.keydrive.name");
item.shortDescription = L.T("item.keydrive.desc");
item.unitValue = Tuning.TumblerDriveValue;
ContrabandHelper.InitContrabandItem(item, 3);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
private static bool CanTarget(GameItem t) => t != null && t.identifier == Terminal && (CanAddWallet?.Invoke() ?? false);
public static GameItem NewHwWallet(bool poisoned)
+2
View File
@@ -20,6 +20,7 @@ namespace Decentralized
UsedDealerClient.Subscribe();
CoinCustomers.Subscribe();
BlackDelivery.Subscribe();
TumblerStory.Subscribe();
GpuInspectView.Ensure();
ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame;
@@ -34,6 +35,7 @@ namespace Decentralized
UsedDealerClient.Unsubscribe();
CoinCustomers.Unsubscribe();
BlackDelivery.Unsubscribe();
TumblerStory.Unsubscribe();
GpuInspectView.Remove();
WalletDriver.Remove();
ModHook.OnGameLoadedLate -= Session.Load;
+39 -1
View File
@@ -268,7 +268,7 @@ black.kind.Contraband.3,Contraband (high)
black.kind.Tech,High-tech (markup)
nightlog.bm.ready,"Black market: {0} arrived; a courier will come by once you open."
item.web3guide.name,Web3 Handbook
item.web3guide.desc,"Bank: depositing turns credits into CRC in your Poinbase account; withdrawing turns CRC back into credits and goes through compliance screening, which looks at the Poinbase account's taint. 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. Hardware wallets: on-chain coins sit in PLEDGER hardware wallets, each with its own balances and taint; open and connect them on the terminal's Wallets page. The Web3 page's trading, black market and mixer use the connected wallet, and coin-paying customers and mining rewards go into it too. Payments (black market, mixer, Ponero desk, listing fees) start as an invoice on the terminal; type the payee address on the wallet and hold ✓ to sign. The last two characters of an address are a checksum, so a one-character typo is usually caught; once in a while it happens to be valid and the coins go to an address nobody uses. DEX swaps also need a signature on the wallet. RatDigg miners (M1 / M2 / M3) are standalone machines with far more hash than a card and a lower power draw per hash; they need a battery, take modules and use no rig slots. Only the black market's miner dealers sell them, gated by black-market standing (M1 40, M2 60, M3 80), sometimes second-hand. When one burns out, repair it by putting electronic components in its repair slot in order; it comes back 20 health lower. Somewhere between day 50 and day 90 a "miner era" arrives, announced in the paper a few days earlier: the big farms halve their power costs, network hash climbs, GPU miners are diluted and low-end cards barely earn, while miners still do fine. Perps: the Perps tab on Poinbase uses your account CRC as margin, up to 5× on PSC and 3× on SPARK, one position per product, never long and short at once. Fills are worse than the quote (more so for bigger orders, like the spot pool) and opening and closing both cost a fee. Liquidation is checked only overnight against the day's average price: equity under 10% of notional wipes the margin. There is also a nightly funding payment; when the price is above its 7-day average, longs pay shorts. If a liquidation loses more than the margin, the insurance fund covers the gap; when it runs dry (a SPARK collapse can do that) winning positions are auto-deleveraged and give up half their profit; anything beyond that becomes your debt. Debt grows 2% a day, is repaid first from CRC in the account every night, blocks new positions and cash-outs, and after seven days items in your shop are seized to cover it; whatever is still short is written off and the perp account is banned for 30 days. Poinbase is a custodial account and can withdraw straight to the connected wallet; depositing from a wallet means sending to Poinbase's deposit address, which takes only CRC and PSC. More hardware wallets are sold by Dr. Jackson; drag one onto the trading terminal to add it, up to 5. The black market has cheaper ones. A wallet you no longer want can be destroyed on the Wallets page once its coins are moved out. Transfers pay a fee: slow, normal or fast, and gas prices rise and fall over roughly a week; when the network is congested fees rise and slow transactions may be dropped from the mempool. Check the full address before sending. Staking: CRA staked in the Revolution pool earns a daily rate that compounds into the stake; withdrawing means requesting first and waiting four days, with no interest while waiting and no way to take it early. The pool pays from a subsidy fund; when it runs low the rate is cut and word of big holders pulling out may spread. Treat that as a warning: once it collapses CRA is worthless within three days and four days of unlocking is too slow. The balance token SPARK can be minted and burned against CRA at the going price (with a fee); its price swings hard and crashes with the collapse. Staking, minting and burning are contract calls and need a signature on the wallet. 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; merchants sell by type at their own price factor, 5+ items get 5% off and 10+ get 10% off, and a bulk merchant appears at random with the lowest prices but six days of waiting; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometLine truncated
item.web3guide.desc,"Bank: depositing turns credits into CRC in your Poinbase account; withdrawing turns CRC back into credits and goes through compliance screening, which looks at the Poinbase account's taint. 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. Hardware wallets: on-chain coins sit in PLEDGER hardware wallets, each with its own balances and taint; open and connect them on the terminal's Wallets page. The Web3 page's trading, black market and mixer use the connected wallet, and coin-paying customers and mining rewards go into it too. Payments (black market, mixer, Ponero desk, listing fees) start as an invoice on the terminal; type the payee address on the wallet and hold ✓ to sign. The last two characters of an address are a checksum, so a one-character typo is usually caught; once in a while it happens to be valid and the coins go to an address nobody uses. DEX swaps also need a signature on the wallet. RatDigg miners (M1 / M2 / M3) are standalone machines with far more hash than a card and a lower power draw per hash; they need a battery, take modules and use no rig slots. Only the black market's miner dealers sell them, gated by black-market standing (M1 40, M2 60, M3 80), sometimes second-hand. When one burns out, repair it by putting electronic components in its repair slot in order; it comes back 20 health lower. Somewhere between day 50 and day 90 a "miner era" arrives, announced in the paper a few days earlier: the big farms halve their power costs, network hash climbs, GPU miners are diluted and low-end cards barely earn, while miners still do fine. Perps: the Perps tab on Poinbase uses your account CRC as margin, up to 5× on PSC and 3× on SPARK, one position per product, never long and short at once. Fills are worse than the quote (more so for bigger orders, like the spot pool) and opening and closing both cost a fee. Liquidation is checked only overnight against the day's average price: equity under 10% of notional wipes the margin. There is also a nightly funding payment; when the price is above its 7-day average, longs pay shorts. If a liquidation loses more than the margin, the insurance fund covers the gap; when it runs dry (a SPARK collapse can do that) winning positions are auto-deleveraged and give up half their profit; anything beyond that becomes your debt. Debt grows 2% a day, is repaid first from CRC in the account every night, blocks new positions and cash-outs, and after seven days items in your shop are seized to cover it; whatever is still short is written off and the perp account is banned for 30 days. Poinbase is a custodial account and can withdraw straight to the connected wallet; depositing from a wallet means sending to Poinbase's deposit address, which takes only CRC and PSC. More hardware wallets are sold by Dr. Jackson; drag one onto the trading terminal to add it, up to 5. The black market has cheaper ones. A wallet you no longer want can be destroyed on the Wallets page once its coins are moved out. Transfers pay a fee: slow, normal or fast, and gas prices rise and fall over roughly a week; when the network is congested fees rise and slow transactions may be dropped from the mempool. Check the full address before sending. Staking: CRA staked in the Revolution pool earns a daily rate that compounds into the stake; withdrawing means requesting first and waiting four days, with no interest while waiting and no way to take it early. The pool pays from a subsidy fund; when it runs low the rate is cut and word of big holders pulling out may spread. Treat that as a warning: once it collapses CRA is worthless within three days and four days of unlocking is too slow. The balance token SPARK can be minted and burned against CRA at the going price (with a fee); its price swings hard and crashes with the collapse. Staking, minting and burning are contract calls and need a signature on the wallet. 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; merchants sell by type at their own price factor, 5+ items get 5% off and 10+ get 10% off, and a bulk merchant appears at random with the lowest prices but six days of waiting; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometLine truncated
courier.odd1,...Package is here.
courier.odd2,"(His hand stays in his pocket.) Sign here."
courier.sign,Sign
@@ -743,3 +743,41 @@ news.story.eleanor_return.title,Pastelle recovers stolen data
news.story.eleanor_return.desc,"Pastelle confirmed it recovered stolen research data. Upper-level capital breathed a sigh of relief, and the CRC custodian bank was upgraded."
news.story.lun_arrested.title,Officer Lun arrested for abuse of power
news.story.lun_arrested.desc,Security arrested Officer Lun on the spot for abuse of power and promised a strict crackdown on illegal trading for the next two weeks.
tumbler.name,Tumbler
tumbler.v1.1,Hi. I hear the terminal here is hooked up to my mixer.
tumbler.v1.2,I wrote that thing. People call me Tumbler.
tumbler.v1.3,It doesn't touch your goods or keep your books. It just stirs the coins.
tumbler.v1.4,Was passing by. Wanted to see what my users look like.
tumbler.v2.1,Things are getting rough. Security is going after on-chain mixing services.
tumbler.v2.2,They flagged a few big addresses and traced them back to my node.
tumbler.v2.3,"If the mixer goes down for a few days, don't panic. Coins inside won't be lost."
tumbler.v3.1,They shut my node down. Sooner or later they'll find where I live.
tumbler.v3.2,I've got a key drive with the mixer's signing keys on it.
tumbler.v3.3,"If Security gets it, they can rebuild every user's history. Yours too."
tumbler.v3.ask,Can you hide it for a few days?
tumbler.v3.choice.hide,[Take the drive and hide it]
tumbler.v3.choice.report,"[Agree, then tip off Security]"
tumbler.v3.choice.sell,[Take the drive and sell it to the black market]
tumbler.v3.thanks1,Thanks. I'll come back for it in a few days.
tumbler.v3.thanks2,"Don't leave it lying around. If a search turns it up, we're both done."
tumbler.v3.report1,...Okay. I'll come by later.
tumbler.v4.back.1,I'm here for the drive.
tumbler.v4.back.2,The node's moved somewhere new. Security won't find it anytime soon.
tumbler.v4.back.3,Here's something for your trouble. From now on your mixer fees are half price.
tumbler.v4.lost.1,Where's the drive?
tumbler.v4.lost.2,...You lost it?
tumbler.v4.lost.3,Forget it. I'll figure something out.
tumbler.v4.sold.1,"You sold the drive to the black market, didn't you."
tumbler.v4.sold.2,"They run the mixer now. Fees doubled, and users' money could vanish any day."
tumbler.v4.sold.3,I won't forget this.
tumbler.v4.none.1,Never mind. I was never here.
item.keydrive.name,Key drive
item.keydrive.desc,"A data stick Tumbler left with you, holding the mixer's signing keys. Contraband: if a search turns it up, it goes on your record."
mixer.closed,The mixer has been shut down and takes no new deposits. Coins already inside come back as usual.
news.tumbler.sanction.title,Security seizes mixing node
news.tumbler.sanction.desc,"Security announced the seizure of a node of an on-chain mixing service, saying it facilitated money laundering. The service is temporarily down."
news.tumbler.caught.title,Mixer operator arrested
news.tumbler.caught.desc,The mixer operator known as Tumbler was arrested in the lower levels. Security says the mixing service has been shut down for good.
news.tumbler.sold.title,Mixing service changes hands
news.tumbler.sold.desc,"Word is the signing keys of a lower-level mixing service have fallen into black-market hands, and fees went up at once. Users fear for their funds."
nightlog.tumbler.gift,Tumbler took back the key drive and left {0} CRT as thanks (sent to {1}); mixer fees halved.
+39 -1
View File
@@ -267,7 +267,7 @@ black.kind.Contraband.3,违禁品(高)
black.kind.Tech,高科技物品(加价)
nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。"
item.web3guide.name,Web3 手册
item.web3guide.desc,"银行:存入把信用点换成 Poinbase 账户里的 CRC;提取把 CRC 换回信用点,要过合规审查,审查看 Poinbase 账户的污点。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。硬件钱包:链上的币放在 PLEDGER 硬件钱包里,每个钱包的余额和污点各算各的;在终端的钱包页打开、连接。Web3 页的交易、黑市、混币器用已连接的钱包,用币付款的顾客和挖矿收益也进它。付款(黑市、混币器、Ponero 兑换处、上架费)先在终端开账单,再在钱包里手输收款地址、按住 ✓ 签名;地址末尾两位是校验位,输错一位多半会被拦下,偶尔碰巧合法,币就转进了没人用的地址。DEX 兑换也要在钱包上签名。Poinbase 是托管账户,可以直接提到已连接的钱包;从钱包充值要转到 Poinbase 的充值地址,只收 CRC 和 PSC。更多硬件钱包向杰克逊博士购买,拖到交易终端上添加,最多 5 个;黑市上也有更便宜的。不要的钱包可以在钱包页销毁,要先把币转走。转账要付手续费,分慢、普通、快三档,gas 价格大约一周一个起落;网络拥堵时手续费变贵,慢档可能被挤出内存池。转账前核对完整地址。质押:把 CRA 质押进革命军质押池每天有日息,利息并入本金;取出要先申请,等四天才到账,等待期间没有利息,也不能提前拿。质押池靠补贴池付息,补贴池见底会降息,有时还会传出大户撤出的消息,那是预兆:一旦崩盘,三天内 CRA 就一文不值,四天的解锁期来不及。平衡代币 SPARK 可以和 CRA 按价格互相铸销(收手续费),价格波动很大,崩盘时会跟着暴跌。质押、铸销都是合约调用,要在钱包上签名。RatDigg 矿机(M1 / M2 / M3)是独立的机器,算力比显卡大得多、耗电更省,要放电池才挖,可以放模块,不占矿架槽位;只在黑市的矿机商那里有,要黑市声望(M1 40、M2 60、M3 80),偶尔有二手的。暴毙后要往修理槽里放电子元件按顺序修好,修好健康少 20。第 50 到 90 天之间会有一次"矿机时代",提前几天上报纸预告,之后大矿场的耗电成本砍半,全网算力涨上去,显卡矿工的收益被摊薄,低档显卡几乎不赚钱,矿机还行。永续合约:Poinbase 的"永续"页,用账户里的 CRC 当保证金,PSC 最高 5 倍、SPARK 最高 3 倍,一个品种只有一个仓位,不能同时多空。成交价比现价差一截(单越大差得越多,和现货池一样),开平仓还收手续费。强平只在夜里按当天均价检查,权益不到名义价值的 10% 就被强平,保证金没了;每晚还有资金费,价格比近 7 天均价高时多头付给空头。强平后亏光保证金还不够的缺口,先由保险基金垫,基金见底(SPARK 崩盘这类大行情会抽干它)后盈利的仓位会被自动减仓、少拿一半盈利,再不够就记成你的欠款:欠款每天涨 2%,账户里的 CRC 每晚先拿去还,没还清不能开仓、不能出金,七天不还就没收店里的物品抵债,抵不够的核销并拉黑永续账户 30 天。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;商家按类型卖货,价格系数各不相同,一次订 5 件以上 95 折、10 件以上 9 折,大宗商家随机出现,价格最低但要等六天;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的卖货页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高,近 7 天出售的量越大,污点越重;同时最多上架 10 件,上架收 5% 上架费,上架后可以改价(收 2% 改价费,标价按参考价重算,成交的快慢跟着新倍率走),成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了Line truncated
item.web3guide.desc,"银行:存入把信用点换成 Poinbase 账户里的 CRC;提取把 CRC 换回信用点,要过合规审查,审查看 Poinbase 账户的污点。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。硬件钱包:链上的币放在 PLEDGER 硬件钱包里,每个钱包的余额和污点各算各的;在终端的钱包页打开、连接。Web3 页的交易、黑市、混币器用已连接的钱包,用币付款的顾客和挖矿收益也进它。付款(黑市、混币器、Ponero 兑换处、上架费)先在终端开账单,再在钱包里手输收款地址、按住 ✓ 签名;地址末尾两位是校验位,输错一位多半会被拦下,偶尔碰巧合法,币就转进了没人用的地址。DEX 兑换也要在钱包上签名。Poinbase 是托管账户,可以直接提到已连接的钱包;从钱包充值要转到 Poinbase 的充值地址,只收 CRC 和 PSC。更多硬件钱包向杰克逊博士购买,拖到交易终端上添加,最多 5 个;黑市上也有更便宜的。不要的钱包可以在钱包页销毁,要先把币转走。转账要付手续费,分慢、普通、快三档,gas 价格大约一周一个起落;网络拥堵时手续费变贵,慢档可能被挤出内存池。转账前核对完整地址。质押:把 CRA 质押进革命军质押池每天有日息,利息并入本金;取出要先申请,等四天才到账,等待期间没有利息,也不能提前拿。质押池靠补贴池付息,补贴池见底会降息,有时还会传出大户撤出的消息,那是预兆:一旦崩盘,三天内 CRA 就一文不值,四天的解锁期来不及。平衡代币 SPARK 可以和 CRA 按价格互相铸销(收手续费),价格波动很大,崩盘时会跟着暴跌。质押、铸销都是合约调用,要在钱包上签名。RatDigg 矿机(M1 / M2 / M3)是独立的机器,算力比显卡大得多、耗电更省,要放电池才挖,可以放模块,不占矿架槽位;只在黑市的矿机商那里有,要黑市声望(M1 40、M2 60、M3 80),偶尔有二手的。暴毙后要往修理槽里放电子元件按顺序修好,修好健康少 20。第 50 到 90 天之间会有一次"矿机时代",提前几天上报纸预告,之后大矿场的耗电成本砍半,全网算力涨上去,显卡矿工的收益被摊薄,低档显卡几乎不赚钱,矿机还行。永续合约:Poinbase 的"永续"页,用账户里的 CRC 当保证金,PSC 最高 5 倍、SPARK 最高 3 倍,一个品种只有一个仓位,不能同时多空。成交价比现价差一截(单越大差得越多,和现货池一样),开平仓还收手续费。强平只在夜里按当天均价检查,权益不到名义价值的 10% 就被强平,保证金没了;每晚还有资金费,价格比近 7 天均价高时多头付给空头。强平后亏光保证金还不够的缺口,先由保险基金垫,基金见底(SPARK 崩盘这类大行情会抽干它)后盈利的仓位会被自动减仓、少拿一半盈利,再不够就记成你的欠款:欠款每天涨 2%,账户里的 CRC 每晚先拿去还,没还清不能开仓、不能出金,七天不还就没收店里的物品抵债,抵不够的核销并拉黑永续账户 30 天。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;商家按类型卖货,价格系数各不相同,一次订 5 件以上 95 折、10 件以上 9 折,大宗商家随机出现,价格最低但要等六天;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的卖货页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高,近 7 天出售的量越大,污点越重;同时最多上架 10 件,上架收 5% 上架费,上架后可以改价(收 2% 改价费,标价按参考价重算,成交的快慢跟着新倍率走),成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了Line truncated
courier.odd1,……东西到了。
courier.odd2,"(他的手一直放在口袋里)在这儿签个字。"
courier.sign,签收
@@ -742,3 +742,41 @@ news.story.eleanor_return.title,帕斯泰尔追回失窃数据
news.story.eleanor_return.desc,帕斯泰尔公司证实追回一份失窃的研究数据。上层资本松了口气,CRC 的托管银行评级上调。
news.story.lun_arrested.title,伦警官因滥用职权被捕
news.story.lun_arrested.desc,治安部当场逮捕了滥用职权的伦警官,并表示接下来两周将从严查处各类违法交易。
tumbler.name,Tumbler
tumbler.v1.1,你好。听说你这儿的终端连着我的混币器。
tumbler.v1.2,那东西是我写的。圈子里都叫我 Tumbler。
tumbler.v1.3,它不碰你的货,也不记你的账,只是把币搅匀。
tumbler.v1.4,顺路过来看看,用户长什么样。
tumbler.v2.1,最近不太平。治安部在查链上的混币服务。
tumbler.v2.2,他们盯上了几个大额地址,顺着摸到了我的节点。
tumbler.v2.3,这几天混币器要是停了,别慌,在里面的币不会丢。
tumbler.v3.1,节点被封了。他们迟早会查到我住的地方。
tumbler.v3.2,我手上有个密钥盘,里面是混币器的签名密钥。
tumbler.v3.3,要是落到治安部手里,所有用户的记录都能还原出来。包括你的。
tumbler.v3.ask,能帮我藏几天吗?
tumbler.v3.choice.hide,[收下密钥盘,帮他藏好]
tumbler.v3.choice.report,[假装答应,转头通知治安部]
tumbler.v3.choice.sell,[收下密钥盘,转手卖给黑市]
tumbler.v3.thanks1,谢了。过几天我来取。
tumbler.v3.thanks2,别放在显眼的地方。搜查的时候被翻出来,咱俩都完蛋。
tumbler.v3.report1,……好。我晚点再过来。
tumbler.v4.back.1,我来取密钥盘了。
tumbler.v4.back.2,节点挪到了新地方,治安部一时半会儿找不着。
tumbler.v4.back.3,这是谢礼。以后你用混币器,手续费给你打对折。
tumbler.v4.lost.1,密钥盘呢?
tumbler.v4.lost.2,……你把它弄丢了?
tumbler.v4.lost.3,算了。我自己想办法。
tumbler.v4.sold.1,密钥盘是你卖给黑市的吧。
tumbler.v4.sold.2,混币器现在归他们管了。费用翻了一倍,用户的钱随时可能被卷走。
tumbler.v4.sold.3,我会记住这件事的。
tumbler.v4.none.1,算了,当我没来过。
item.keydrive.name,密钥盘
item.keydrive.desc,Tumbler 托你保管的数据棒,里面是混币器的签名密钥。违禁品,搜查时被翻出来会记罪证。
mixer.closed,混币器已经关停,不再接受新的存入。在途的币照常提回。
news.tumbler.sanction.title,治安部查封混币节点
news.tumbler.sanction.desc,治安部宣布查封一处链上混币服务的节点,称其为洗钱提供便利。服务暂时中断。
news.tumbler.caught.title,混币器运营者落网
news.tumbler.caught.desc,化名 Tumbler 的混币器运营者在下层区被捕。治安部表示,相关混币服务已永久关停。
news.tumbler.sold.title,混币服务易主
news.tumbler.sold.desc,有消息称,下层区一家混币服务的签名密钥已落入黑市之手,费用随即上调。用户担心资金安全。
nightlog.tumbler.gift,Tumbler 取回了密钥盘,留下 {0} CRT 谢礼(已转入 {1}),混币器手续费减半。
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

+1
View File
@@ -143,6 +143,7 @@ namespace Decentralized
var d = Session.Data;
int day = StoreStation.GetDayCounter();
StoryLinks.Night(d, day);
TumblerStory.Night(d, day);
var events = ActiveEvents();
Log.Info($"夜间事件:{string.Join(",", events)}(新增 {string.Join(",", events.Where(e => !d.ActiveEvents.Contains(e)))}),黑市声望 {StoreReputation.GetBMReputation()}");
bool wasSanctioned = d.Mixer.Sanctioned(day);
+2 -1
View File
@@ -1494,6 +1494,7 @@ namespace Decentralized
}, r =>
{
r.AddHeader(L.T("mixer.deposit"));
if (d.Story.MixerClosed) { r.AddLabel(L.T("mixer.closed")); return; }
r.BeginRow();
foreach (var a in new[] { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA })
{
@@ -1514,7 +1515,7 @@ namespace Decentralized
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.AddLabel(L.T("mixer.preview", Fmt.Num(v * Math.Pow(1 - Mixer.Fee, _mixRounds)), _mixAsset, (int)Math.Round((1 - Math.Pow(1 - rate, _mixRounds)) * 100), day + _mixRounds));
}
r.AddButton(L.T("mixer.go"), () =>
{
+155
View File
@@ -0,0 +1,155 @@
using System.Linq;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 本 mod 自己的剧情线:混币器运营者 Tumbler。解锁混币器、第 30 天起,四次上门(游戏的对话顾客):
/// 1 自我介绍;2 说治安部在查混币服务(当晚混币器被制裁几天);3 求你藏密钥盘,三选一(藏 / 举报 / 卖给黑市);4 结局。
/// 进度存在 <see cref="StoryMods.Tumbler"/>,结果改动混币器(见 <see cref="StoryMods.Apply"/>)。
/// </summary>
public static class TumblerStory
{
public static void Subscribe() => ModHook.OnGenerateCustomerLate += OnGenerate;
public static void Unsubscribe() => ModHook.OnGenerateCustomerLate -= OnGenerate;
public static void ForceVisit(StoreClientManager manager) => OnGenerate(manager);
private static void OnGenerate(StoreClientManager manager)
{
var d = Session.Data;
if (d == null || !Unlock.IsUnlocked) return;
Log.Guard("Tumbler", () =>
{
int day = StoreStation.GetDayCounter();
var t = d.Story.Tumbler;
if (t.TryStart(day, Unlock.MixerUnlocked)) Log.Info($"Tumbler 剧情开始:第 {t.StartDay} 天第一次上门");
int due = t.Due(day);
if (due == 0) return;
t.Visited(day);
if (due == 4 && t.Choice == Tumbler.Report) { Log.Info("Tumbler 已被举报,不再上门"); Session.Save(); return; }
var c = Visit(due, t.Choice);
manager.AddNextClient(c);
Log.Info($"Tumbler 第 {due} 次上门(选择 {t.Choice})");
Session.Save();
});
}
/// <summary>夜间:第 2 次上门那天,治安部封了混币器节点。</summary>
public static void Night(SaveData d, int day)
{
var t = d.Story.Tumbler;
if (t.Stage == 2 && t.VisitedDay == day && !d.Story.Has("tumbler_sanction"))
{
d.Story.Applied.Add("tumbler_sanction");
d.Mixer.SanctionedUntil = System.Math.Max(d.Mixer.SanctionedUntil, day + 4);
News.Show("pd_news_tumbler_sanction", L.T("news.tumbler.sanction.title"), L.T("news.tumbler.sanction.desc"), 3, 8);
}
}
private static StoreClient New()
{
var c = new StoreClient
{
identifier = "pd_tumbler", displayName = L.T("tumbler.name"), spriteName = SpriteDict.GetRandomShadySpriteName(),
clientIntent = StoreClient.ClientIntent.DIALOGUE, nonShootable = true, dismissable = false,
};
ModHelper.SetClientSource(c, ModInfo.Id);
return c;
}
private static Dialogue Lines(StoreClient c, Dialogue first, string prefix, int n)
{
var dl = first.SetText(c.displayName, L.T(prefix + "1"));
for (int i = 2; i <= n; i++) dl = dl.NextDialogue().SetText(c.displayName, L.T(prefix + i));
return dl;
}
private static StoreClient Visit(int n, int choice)
{
var c = New();
var d = Session.Data;
switch (n)
{
case 1: Lines(c, c.mainDialogue, "tumbler.v1.", 4).SetEndAction(Leave); break;
case 2: Lines(c, c.mainDialogue, "tumbler.v2.", 3).SetEndAction(Leave); break;
case 3:
{
var ask = new Dialogue();
var hide = new Dialogue(); var report = new Dialogue(); var sell = new Dialogue();
hide.SetText("PLAYER", L.T("tumbler.v3.choice.hide")).NextDialogue().SetText(c.displayName, L.T("tumbler.v3.thanks1")).NextDialogue().SetText(c.displayName, L.T("tumbler.v3.thanks2")).SetEndAction(Leave);
sell.SetText("PLAYER", L.T("tumbler.v3.choice.sell")).NextDialogue().SetText(c.displayName, L.T("tumbler.v3.thanks1")).NextDialogue().SetText(c.displayName, L.T("tumbler.v3.thanks2")).SetEndAction(Leave);
report.SetText("PLAYER", L.T("tumbler.v3.choice.report")).NextDialogue().SetText(c.displayName, L.T("tumbler.v3.report1")).SetEndAction(Leave);
Lines(c, c.mainDialogue, "tumbler.v3.", 3).SetNextDialogue(ask);
ask.SetText(c.displayName, L.T("tumbler.v3.ask"))
.AddChoice(DirectoryMaster.Item("$"), "-", "", hide, () => Choose(Tumbler.Hide))
.AddChoice(DirectoryMaster.Item("$"), "-", "", report, () => Choose(Tumbler.Report))
.AddChoice(DirectoryMaster.Item("$"), "-", "", sell, () => Choose(Tumbler.Sell));
break;
}
case 4:
if (choice == Tumbler.Hide)
{
bool has = EmporiumEntry.Instance != null && EmporiumEntry.Instance.GetAllOwnedItems().Any(i => i != null && i.identifier == Items.KeyDrive);
if (has) Lines(c, c.mainDialogue, "tumbler.v4.back.", 3).SetEndAction(() => { Returned(); Leave(); });
else Lines(c, c.mainDialogue, "tumbler.v4.lost.", 3).SetEndAction(Leave);
}
else if (choice == Tumbler.Sell) Lines(c, c.mainDialogue, "tumbler.v4.sold.", 3).SetEndAction(Leave);
else Lines(c, c.mainDialogue, "tumbler.v4.none.", 1).SetEndAction(Leave); // 第 3 次没做选择(被遣散)
break;
}
c.AddBasicDialog();
c.CompleteClientCreation();
return c;
}
private static void Leave() => DialogUIManager.Instance.CurrentClientLeave();
/// <summary>第 3 次上门做出的选择。</summary>
private static void Choose(int choice)
{
Log.Guard("Tumbler 选择", () =>
{
var d = Session.Data;
d.Story.Tumbler.Choice = choice;
int day = StoreStation.GetDayCounter();
switch (choice)
{
case Tumbler.Hide:
Items.Give(ModHelper.SpawnItem(Items.KeyDrive));
break;
case Tumbler.Report:
d.Story.Apply("tumbler_reported", day);
ModHelper.AddCredit(Tuning.TumblerBounty, true);
StoreReputation.ModSecReputation(Tuning.TumblerSecRep);
News.Show("pd_news_tumbler_caught", L.T("news.tumbler.caught.title"), L.T("news.tumbler.caught.desc"), 4, 8);
break;
case Tumbler.Sell:
d.Story.Apply("tumbler_sold", day);
d.Hot.Add(Assets.CRT, Tuning.TumblerSellCrt); d.Hot.Taint += Tuning.TumblerSellCrt * 0.5; d.Hot.Basis += Tuning.TumblerSellCrt;
StoreReputation.ModReputation(StoreReputation.FACTION_BLACK_MARKET, Tuning.TumblerBmRep);
News.Show("pd_news_tumbler_sold", L.T("news.tumbler.sold.title"), L.T("news.tumbler.sold.desc"), 4, 8);
break;
}
Log.Info($"Tumbler:选择 {choice}");
Session.Save();
});
}
/// <summary>第 4 次上门取回密钥盘:收走物品,谢礼进当前钱包,混币器降费、匿名集变大。</summary>
private static void Returned()
{
Log.Guard("Tumbler 取回密钥盘", () =>
{
var d = Session.Data;
var drive = EmporiumEntry.Instance?.GetAllOwnedItems().FirstOrDefault(i => i != null && i.identifier == Items.KeyDrive);
if (drive == null) return;
drive.Destroy();
d.Story.Apply("tumbler_kept", StoreStation.GetDayCounter());
d.Hot.Add(Assets.CRT, Tuning.TumblerGift); d.Hot.Basis += Tuning.TumblerGift;
PlayerStore.Instance.AddNightLog(L.T("nightlog.tumbler.gift", Fmt.Num(Tuning.TumblerGift), WalletWindow.NameOf(d.Hot)));
Session.Save();
});
}
}
}
@@ -1475,6 +1475,35 @@ internal static class Program
finally { StoryMods.Current = saved; }
});
Test("Tumbler 剧情线:开始条件、上门日程、混币器倍率", () =>
{
var saved = StoryMods.Current;
try
{
var t = new Tumbler();
Check(!t.TryStart(Tuning.TumblerMinDay - 1, true) && !t.TryStart(40, false), "没到日子或没解锁混币器不开始");
Check(t.TryStart(40, true) && t.StartDay == 41 && !t.TryStart(42, true), "开始一次,明天第一次上门");
Check(t.Due(40) == 0 && t.Due(41) == 1, "第 1 次在开始日");
t.Visited(41);
Check(t.Due(41) == 0 && t.Due(43) == 0 && t.Due(44) == 2, "同一天不重复,第 2 次在 +3");
t.Visited(44); Check(t.Due(46) == 3, "第 3 次在 +5"); t.Choice = Tumbler.Hide; t.Visited(46);
Check(t.Due(49) == 0 && t.Due(50) == 4, "第 4 次在 +9"); t.Visited(50);
Check(t.Done && t.Due(60) == 0, "结束");
var t2 = new Tumbler(); t2.TryStart(40, true); t2.Visited(45);
Check(t2.Due(46) == 2, "错过的上门往后补");
// 混币器倍率
var sm = new StoryMods(); StoryMods.Current = sm;
double fee0 = Mixer.Fee, rate0 = Mixer.RoundRate(1000);
sm.Apply("tumbler_kept", 50);
Check(Math.Abs(Mixer.Fee - fee0 * Tuning.TumblerKeptFee) < 1e-12 && Mixer.RoundRate(1000) > rate0, "藏好密钥盘:手续费降、匿名集变大");
var sm2 = new StoryMods(); StoryMods.Current = sm2; sm2.Apply("tumbler_sold", 50);
Check(Mixer.Fee > fee0 && sm2.MixerRug == Tuning.TumblerSoldRug, "卖给黑市:手续费涨、跑路概率上升");
var sm3 = new StoryMods(); sm3.Apply("tumbler_reported", 50);
Check(sm3.MixerClosed, "举报:混币器关闭");
}
finally { StoryMods.Current = saved; }
});
Test("预测市场:先验概率", () =>
{
Check(Math.Abs(PredictMath.PurityAtLeast(8500, 8000, 8999) - 0.5) < 1e-9, "纯度 8500 以上一半");