diff --git a/CHANGELOG.md b/CHANGELOG.md index 782987a..3b7861a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### 第九阶段:剧情与收尾 +- 交易终端窗口加大:默认 1200×900,mod 设置里可选 828×680(旧大小)、1200×900、1400×1000。窗口大时左右栏、表格列、货架列数、K 线图(3 倍像素)、首页卡片、订单两栏、钱包表按多出的宽度放宽,文字放大到约 1.24 倍(上限 1.3,行高和商品卡同步)。 +- mod 设置(分类:界面、剧情、难度):终端窗口大小;Tumbler 剧情开关;风险倍率(骗局、跑路、钱包投毒、地址劫持、钓鱼概率)和挖矿产出倍率,档位 ×0.5 / ×1 / ×1.5 / ×2,领域层 `Scales` 读取。分类名要等本地化表可用后再登记。 - 多语言:新增 de fr es pl ru ja 六种语言表(连同 zh en 共 8 种,对应游戏主菜单的语言)。英文表是源文,其他语言由 Gemini 分块译出(术语表 `docs/i18n-glossary.md` 在运营仓库,游戏已有的词用官方译名),未经人工校对,文件头有说明。键集合、占位符、`§ ¶ \\n` 标记由单元测试逐语言核对,非中日语言里不得出现汉字或假名。 - 多语言布局:表格单元、按钮文字放不下时自动缩小字号(最小 68%)再省略,商品名限定两行内缩放;de fr es pl ru 的钱包表、手册章节列表、硬件钱包粘贴键按语言放宽;ru de 等少数过长标签改短(永续“加保证金”键、收益池列头)。 - 中文“匿名币”改称“隐私币”(英文 privacy coin),手册、黑市标签同步。 diff --git a/README.md b/README.md index c97041e..733fafb 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,21 @@ All coins, exchanges and events are fictional and have no real-world value. | `tests/Decentralized.Domain.Tests` | Console test runner: `dotnet run --project tests/Decentralized.Domain.Tests` | | `art/sprites.py`, `art/big.py`, `art/big_cases.py` | Generate the item sprites, the large inspection art and its overlays, and the terminal icons into `src/Decentralized/ModFiles/Textures/Items`. | +## Settings + +Mod settings are in the game's mod list (select the mod, gear button); they apply to all saves. + +| Section | Setting | Values | +|---|---|---| +| Interface | Trading terminal size | 828×680, 1200×900 (default), 1400×1000. Layout and text scale with the window; applies when the terminal is next opened. | +| Story | Tumbler storyline | on / off. Off stops new visits; progress already made is kept. | +| Difficulty | Risk multiplier | ×0.5 / ×1 / ×1.5 / ×2 on the chance of scams, rug pulls, wallet poisoning, address hijacking and phishing. | +| Difficulty | Mining reward multiplier | ×0.5 / ×1 / ×1.5 / ×2 on the block reward paid to the player's rigs. | + +## Languages + +The interface follows the game language: en, zh, de, fr, es, pl, ru, ja. English is the source text; zh is hand-written; the other six are AI translations and have not been proofread. Tables are in `src/Decentralized/ModFiles/Localization/.csv`. + ## License MIT, see `LICENSE`. diff --git a/src/Decentralized.Domain/BmOrders.cs b/src/Decentralized.Domain/BmOrders.cs index 8f7b525..35f160e 100644 --- a/src/Decentralized.Domain/BmOrders.cs +++ b/src/Decentralized.Domain/BmOrders.cs @@ -114,7 +114,7 @@ namespace Decentralized.Domain /// 下单实付(信用点):托管 = 订单价 + 托管费;提前放款 = 订单价打折。 public static int Pay(int price, PayMode m) => m == PayMode.Escrow ? price + EscrowFee(price) : (int)Math.Round(price * (1 - Tuning.BmAdvanceDiscount)); - public double RugChance => Math.Min(Tuning.BmRugMax, Tuning.BmRugBase + Held / 10000.0 * Tuning.BmRugPerTenK); + public double RugChance => Scales.R(Math.Min(Tuning.BmRugMax, Tuning.BmRugBase + Held / 10000.0 * Tuning.BmRugPerTenK)); /// 每周必有的"矿工路线"商家类型(从中抽 2 家),其余按全部类型抽。 private static readonly MerchantType[] Core = { MerchantType.GpuNew, MerchantType.GpuUsed, MerchantType.Rigs, MerchantType.Modules }; @@ -232,7 +232,7 @@ namespace Decentralized.Domain if (scam && rng.Chance(Tuning.BmForumScamHint)) Forum.Add("scam:" + s.Name); } for (int i = 0; i < Sellers.Count; i++) Catalog.AddRange(MerchantGoods(i, Sellers[i], day, rep, rng, value)); - if (RugChance > Tuning.BmRugBase * Tuning.BmForumRugFactor) Forum.Add("rug"); + if (RugChance > Scales.R(Tuning.BmRugBase * Tuning.BmForumRugFactor)) Forum.Add("rug"); } /// 目录里全部货按订单价算的总值(信用点),用来核对每周周转量。 diff --git a/src/Decentralized.Domain/Mining.cs b/src/Decentralized.Domain/Mining.cs index 4295ac4..9aa0e50 100644 --- a/src/Decentralized.Domain/Mining.cs +++ b/src/Decentralized.Domain/Mining.cs @@ -165,7 +165,7 @@ namespace Decentralized.Domain { if (playerHash <= 0) return 0; double total = NpcHash * Availability + playerHash; - return Reward * playerHash / total * (1 - Tuning.PoolFee); + return Reward * playerHash / total * (1 - Tuning.PoolFee) * Scales.Mining; } public enum EraStep { None, Warn, Start } diff --git a/src/Decentralized.Domain/Mixer.cs b/src/Decentralized.Domain/Mixer.cs index 46ca436..d9c7376 100644 --- a/src/Decentralized.Domain/Mixer.cs +++ b/src/Decentralized.Domain/Mixer.cs @@ -81,7 +81,7 @@ namespace Decentralized.Domain var results = new List(); foreach (var d in Pending.ToList()) { - if (rng.Chance(Math.Min(1, Tuning.MixerRugChance * StoryMods.Current.MixerRug))) + if (rng.Chance(Scales.R(Tuning.MixerRugChance * StoryMods.Current.MixerRug))) { Pending.Remove(d); results.Add(new MixResult { Rugged = true, Asset = d.Asset, Amount = d.Amount }); diff --git a/src/Decentralized.Domain/Scales.cs b/src/Decentralized.Domain/Scales.cs new file mode 100644 index 0000000..18dba63 --- /dev/null +++ b/src/Decentralized.Domain/Scales.cs @@ -0,0 +1,26 @@ +using System; + +namespace Decentralized.Domain +{ + /// mod 设置里的倍率(全局,不随存档)。接入层读设置后写进来;领域层在对应概率和产出处乘上。 + public static class Scales + { + /// 风险倍率:骗局、跑路、钱包投毒、地址劫持、钓鱼的发生概率。 + public static double Risk = 1; + /// 挖矿产出倍率:玩家矿机拿到的区块奖励。 + public static double Mining = 1; + + /// 概率乘风险倍率,封顶 1。 + public static double R(double chance) => Math.Min(1, chance * Risk); + + /// 设置里的倍率档位(“×1.5”)。 + public static readonly string[] Choices = { "×0.5", "×1", "×1.5", "×2" }; + public const string Default = "×1"; + + public static double Parse(string s) + { + if (s != null && s.StartsWith("×") && double.TryParse(s.Substring(1), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v) && v >= 0) return v; + return 1; + } + } +} diff --git a/src/Decentralized.Domain/Scams.cs b/src/Decentralized.Domain/Scams.cs index 28c4c08..31ec8d4 100644 --- a/src/Decentralized.Domain/Scams.cs +++ b/src/Decentralized.Domain/Scams.cs @@ -75,7 +75,7 @@ namespace Decentralized.Domain /// 新空投。钓鱼的名字常是正规名字的仿冒(一个字符不同),要求对一种币无上限授权,奖励看起来更高。 public Airdrop NewAirdrop(int day, Rng rng, string[] assets) { - bool scam = rng.Chance(Tuning.AirdropScam); + bool scam = rng.Chance(Scales.R(Tuning.AirdropScam)); string name = AirdropNames[rng.Range(0, AirdropNames.Length - 1)]; if (scam && rng.Chance(0.6)) name = Typo(name, rng); string asset = assets[rng.Range(0, assets.Length - 1)]; @@ -183,7 +183,7 @@ namespace Decentralized.Domain } foreach (var p in Pools.Where(x => !x.Closed).ToList()) { - if (p.Scam && p.Total > 0 && day > p.OpenDay && rng.Chance(Tuning.YieldRugNightly)) + if (p.Scam && p.Total > 0 && day > p.OpenDay && rng.Chance(Scales.R(Tuning.YieldRugNightly))) { foreach (var kv in p.Deposits) evs.Add(new ScamEvent { Kind = "rug", Name = p.Name, Wallet = kv.Key, Asset = Assets.CRC, Amount = kv.Value }); foreach (var q in p.Pending) evs.Add(new ScamEvent { Kind = "rug", Name = p.Name, Wallet = q.Wallet, Asset = Assets.CRC, Amount = q.Amount }); diff --git a/src/Decentralized.Domain/UsedDealer.cs b/src/Decentralized.Domain/UsedDealer.cs index 0b05a41..086f6bb 100644 --- a/src/Decentralized.Domain/UsedDealer.cs +++ b/src/Decentralized.Domain/UsedDealer.cs @@ -24,7 +24,7 @@ namespace Decentralized.Domain /// 今天来的贩子的类型:骗子越往后越多。 public static DealerKind Kind(int day, Rng rng) { - double scam = Math.Min(Tuning.UsedScamMax, Tuning.UsedScamBase + day * Tuning.UsedScamPerDay); + double scam = Scales.R(Math.Min(Tuning.UsedScamMax, Tuning.UsedScamBase + day * Tuning.UsedScamPerDay)); double r = rng.NextDouble(); return r < scam ? DealerKind.Scammer : r < scam + Tuning.UsedMiddlemanShare ? DealerKind.Middleman : DealerKind.Honest; } diff --git a/src/Decentralized/BlackDelivery.cs b/src/Decentralized/BlackDelivery.cs index c7d65b2..7f02498 100644 --- a/src/Decentralized/BlackDelivery.cs +++ b/src/Decentralized/BlackDelivery.cs @@ -39,7 +39,7 @@ namespace Decentralized public static GameItem Make(string id, string model, string fakeOf, double health) { if (id.StartsWith("gpu:")) return Hardware.Specs[model].Asic ? AsicItems.New(model, health) : Items.NewCard(model, health, fakeOf, Session.Data.Rng); - if (id == Items.HwWallet) return Items.NewHwWallet(Session.Data.Rng.Chance(Tuning.WalletPoisonShare)); + if (id == Items.HwWallet) return Items.NewHwWallet(Session.Data.Rng.Chance(Scales.R(Tuning.WalletPoisonShare))); return ModHelper.SpawnItem(id); } diff --git a/src/Decentralized/CandleChart.cs b/src/Decentralized/CandleChart.cs index e6fcb04..05de3cd 100644 --- a/src/Decentralized/CandleChart.cs +++ b/src/Decentralized/CandleChart.cs @@ -58,7 +58,7 @@ namespace Decentralized public const int Days = 30, Step = 6, Body = 5, PriceH = 70, Gap = 3, VolH = 16, Pad = 2, AxisW = 22, DateH = 7; public const int W = Days * Step + Pad * 2 + AxisW, H = PriceH + Gap + VolH + Pad * 2 + DateH; - public const float Scale = 2f; + public static float Scale => TerminalWindow.ChartScale; private static readonly Color32 Bg = new Color32(30, 31, 34, 255), Grid = new Color32(48, 50, 54, 255); private static readonly Color32 Up = new Color32(108, 190, 112, 255), Down = new Color32(214, 94, 88, 255); diff --git a/src/Decentralized/ModEntry.cs b/src/Decentralized/ModEntry.cs index 9061ca1..7828dad 100644 --- a/src/Decentralized/ModEntry.cs +++ b/src/Decentralized/ModEntry.cs @@ -15,6 +15,7 @@ namespace Decentralized Items.OpenHandbook = HandbookWindow.Open; Items.AddWallet = TerminalWindow.AddWallet; Items.CanAddWallet = TerminalWindow.CanAddWallet; + Settings.DefineWhenReady(ModHelper.GetConfig(ModInfo.Id)); Unlock.Register(); Night.Subscribe(); DealerStock.Subscribe(); diff --git a/src/Decentralized/ModFiles/Localization/de.csv b/src/Decentralized/ModFiles/Localization/de.csv index 0c94d79..2f6f910 100644 --- a/src/Decentralized/ModFiles/Localization/de.csv +++ b/src/Decentralized/ModFiles/Localization/de.csv @@ -809,3 +809,14 @@ cart.partial,{0} Artikel bezahlt; {1} fehlgeschlagen (Bestand oder Guthaben). black.info.accepts,Akzeptiert: {0} black.info.advance.yes,Vorzeitige Freigabe akzeptiert black.info.advance.no,Keine vorzeitige Freigabe +settings.section.ui,Oberfläche +settings.section.story,Handlung +settings.section.difficulty,Schwierigkeit +settings.terminal_size,Größe des Handelsterminals +settings.terminal_size_desc,Fenstergröße in Oberflächeneinheiten (Breite × Höhe). Gilt beim nächsten Öffnen des Terminals. +settings.tumbler_story,Tumbler-Handlung +settings.tumbler_story_desc,Tumblers Besuche und der Auftrag mit der Schlüsselscheibe. Ausgeschaltet gibt es keine neuen Besuche; erreichter Fortschritt bleibt. +settings.risk_scale,Risikofaktor +settings.risk_scale_desc,"Multipliziert die Wahrscheinlichkeit von Betrug, Rug Pulls, Wallet-Vergiftung, Adressumleitung und Phishing. Gilt für alle Spielstände." +settings.mining_scale,Mining-Ertragsfaktor +settings.mining_scale_desc,Multipliziert die Blockbelohnung der Mining-Geräte. Gilt für alle Spielstände. diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 6392282..a680a4c 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -809,3 +809,14 @@ cart.partial,"Paid for {0} items; {1} failed (stock or balance)." black.info.accepts,Takes: {0} black.info.advance.yes,advance release accepted black.info.advance.no,no advance release +settings.section.ui,Interface +settings.section.story,Story +settings.section.difficulty,Difficulty +settings.terminal_size,Trading terminal size +settings.terminal_size_desc,Window size in interface units (width × height). Applies the next time the terminal opens. +settings.tumbler_story,Tumbler storyline +settings.tumbler_story_desc,Tumbler's visits and the keydisk errand. Turning this off stops new visits; progress already made is kept. +settings.risk_scale,Risk multiplier +settings.risk_scale_desc,"Multiplies the chance of scams, rug pulls, wallet poisoning, address hijacking and phishing. Applies to all saves." +settings.mining_scale,Mining reward multiplier +settings.mining_scale_desc,Multiplies the block reward paid to mining rigs. Applies to all saves. diff --git a/src/Decentralized/ModFiles/Localization/es.csv b/src/Decentralized/ModFiles/Localization/es.csv index 2f10b9c..02fcb53 100644 --- a/src/Decentralized/ModFiles/Localization/es.csv +++ b/src/Decentralized/ModFiles/Localization/es.csv @@ -809,3 +809,14 @@ cart.partial,Artículos pagados: {0}; fallidos: {1} (stock o saldo). black.info.accepts,Acepta: {0} black.info.advance.yes,acepta liberación anticipada black.info.advance.no,no acepta liberación anticipada +settings.section.ui,Interfaz +settings.section.story,Historia +settings.section.difficulty,Dificultad +settings.terminal_size,Tamaño del terminal de comercio +settings.terminal_size_desc,Tamaño de la ventana en unidades de interfaz (ancho × alto). Se aplica al abrir el terminal de nuevo. +settings.tumbler_story,Trama de Tumbler +settings.tumbler_story_desc,"Visitas de Tumbler y el encargo del disco de claves. Desactivada, no habrá visitas nuevas; el progreso logrado se conserva." +settings.risk_scale,Multiplicador de riesgo +settings.risk_scale_desc,"Multiplica la probabilidad de estafas, rug pulls, envenenamiento de billetera, secuestro de direcciones y phishing. Se aplica a todas las partidas." +settings.mining_scale,Multiplicador de minería +settings.mining_scale_desc,Multiplica la recompensa de bloque que reciben los equipos de minería. Se aplica a todas las partidas. diff --git a/src/Decentralized/ModFiles/Localization/fr.csv b/src/Decentralized/ModFiles/Localization/fr.csv index d4d1000..0f8d657 100644 --- a/src/Decentralized/ModFiles/Localization/fr.csv +++ b/src/Decentralized/ModFiles/Localization/fr.csv @@ -809,3 +809,14 @@ cart.partial,Articles payés : {0} ; {1} en échec (stock ou solde). black.info.accepts,Accepte : {0} black.info.advance.yes,déblocage anticipé accepté black.info.advance.no,pas de déblocage anticipé +settings.section.ui,Interface +settings.section.story,Histoire +settings.section.difficulty,Difficulté +settings.terminal_size,Taille du terminal de trading +settings.terminal_size_desc,Taille de la fenêtre en unités d'interface (largeur × hauteur). Appliquée à la prochaine ouverture du terminal. +settings.tumbler_story,Intrigue Tumbler +settings.tumbler_story_desc,"Visites de Tumbler et mission de la clé. Désactivée, plus aucune nouvelle visite ; la progression acquise est conservée." +settings.risk_scale,Multiplicateur de risque +settings.risk_scale_desc,"Multiplie la probabilité d'arnaques, de rug pulls, d'empoisonnement de portefeuille, de détournement d'adresse et d'hameçonnage. S'applique à toutes les sauvegardes." +settings.mining_scale,Multiplicateur de gains de minage +settings.mining_scale_desc,Multiplie la récompense de bloc versée aux machines de minage. S'applique à toutes les sauvegardes. diff --git a/src/Decentralized/ModFiles/Localization/ja.csv b/src/Decentralized/ModFiles/Localization/ja.csv index b6763e0..d65f0ec 100644 --- a/src/Decentralized/ModFiles/Localization/ja.csv +++ b/src/Decentralized/ModFiles/Localization/ja.csv @@ -809,3 +809,14 @@ cart.partial,{0} 件を決済、{1} 件が失敗(在庫または残高不足 black.info.accepts,対応通貨:{0} black.info.advance.yes,事前リリース対応 black.info.advance.no,事前リリース非対応 +settings.section.ui,インターフェース +settings.section.story,ストーリー +settings.section.difficulty,難易度 +settings.terminal_size,取引ターミナルのサイズ +settings.terminal_size_desc,ウィンドウサイズ(幅×高さ、UI 単位)。次にターミナルを開いたときに反映されます。 +settings.tumbler_story,Tumbler のストーリー +settings.tumbler_story_desc,Tumbler の訪問とキーディスクの依頼。オフにすると新たな訪問はなくなり、進行済みの分は保持されます。 +settings.risk_scale,リスク倍率 +settings.risk_scale_desc,詐欺、ラグプル、ウォレット汚染、アドレス乗っ取り、フィッシングの発生確率にかかる倍率。すべてのセーブに適用されます。 +settings.mining_scale,採掘報酬倍率 +settings.mining_scale_desc,採掘機器が受け取るブロック報酬にかかる倍率。すべてのセーブに適用されます。 diff --git a/src/Decentralized/ModFiles/Localization/pl.csv b/src/Decentralized/ModFiles/Localization/pl.csv index 929e2df..3fb0a2f 100644 --- a/src/Decentralized/ModFiles/Localization/pl.csv +++ b/src/Decentralized/ModFiles/Localization/pl.csv @@ -809,3 +809,14 @@ cart.partial,Opłacono: {0} szt.; nieudane: {1} szt. (brak zapasów lub salda). black.info.accepts,Akceptuje: {0} black.info.advance.yes,akceptowana wypłata zaliczkowa black.info.advance.no,brak wypłaty zaliczkowej +settings.section.ui,Interfejs +settings.section.story,Fabuła +settings.section.difficulty,Poziom trudności +settings.terminal_size,Rozmiar terminala handlowego +settings.terminal_size_desc,Rozmiar okna w jednostkach interfejsu (szerokość × wysokość). Obowiązuje po następnym otwarciu terminala. +settings.tumbler_story,Wątek Tumblera +settings.tumbler_story_desc,Wizyty Tumblera i zlecenie z dyskiem kluczy. Po wyłączeniu nie będzie nowych wizyt; dotychczasowy postęp zostaje. +settings.risk_scale,Mnożnik ryzyka +settings.risk_scale_desc,"Mnoży szanse na oszustwa, rug pulle, zatrucie portfela, podmianę adresu i phishing. Dotyczy wszystkich zapisów." +settings.mining_scale,Mnożnik wydobycia +settings.mining_scale_desc,Mnoży nagrodę za blok wypłacaną koparkom. Dotyczy wszystkich zapisów. diff --git a/src/Decentralized/ModFiles/Localization/ru.csv b/src/Decentralized/ModFiles/Localization/ru.csv index 36efc50..2213f16 100644 --- a/src/Decentralized/ModFiles/Localization/ru.csv +++ b/src/Decentralized/ModFiles/Localization/ru.csv @@ -809,3 +809,14 @@ cart.partial,Оплачено: {0} шт.; не удалось: {1} шт. (нет black.info.accepts,Принимает: {0} black.info.advance.yes,досрочная выплата принимается black.info.advance.no,без досрочной выплаты +settings.section.ui,Интерфейс +settings.section.story,Сюжет +settings.section.difficulty,Сложность +settings.terminal_size,Размер торгового терминала +settings.terminal_size_desc,Размер окна в единицах интерфейса (ширина × высота). Применяется при следующем открытии терминала. +settings.tumbler_story,Сюжет Tumbler +settings.tumbler_story_desc,Визиты Tumbler и поручение с ключ-диском. При отключении новых визитов не будет; достигнутый прогресс сохраняется. +settings.risk_scale,Множитель риска +settings.risk_scale_desc,"Умножает вероятность мошенничества, рагпулов, отравления кошелька, подмены адреса и фишинга. Действует для всех сохранений." +settings.mining_scale,Множитель добычи +settings.mining_scale_desc,"Умножает награду за блок, получаемую майнинг-устройствами. Действует для всех сохранений." diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 51752ad..f5d9375 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -808,3 +808,14 @@ cart.partial,"已结算 {0} 件,{1} 件没成(库存或余额不足)。" black.info.accepts,收币:{0} black.info.advance.yes,接受提前放款 black.info.advance.no,不接受提前放款 +settings.section.ui,界面 +settings.section.story,剧情 +settings.section.difficulty,难度 +settings.terminal_size,交易终端窗口大小 +settings.terminal_size_desc,窗口尺寸(宽×高,界面单位)。下次打开终端时生效。 +settings.tumbler_story,Tumbler 剧情 +settings.tumbler_story_desc,Tumbler 上门与藏密钥盘的支线。关闭后不再有新的上门,已有进度保留。 +settings.risk_scale,风险倍率 +settings.risk_scale_desc,骗局、跑路、钱包投毒、地址劫持和钓鱼的发生概率倍数。对所有存档生效。 +settings.mining_scale,挖矿产出倍率 +settings.mining_scale_desc,挖矿设备拿到的区块奖励倍数。对所有存档生效。 diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs index 3351c39..a7554d3 100644 --- a/src/Decentralized/Night.cs +++ b/src/Decentralized/Night.cs @@ -105,7 +105,7 @@ namespace Decentralized TxRules.Record(w.History, new TxRecord { Addr = fake, Asset = Assets.PSC, Amount = 0, Day = day + 1, In = true }); Log.Info($"地址投毒:{p.Target} 的相似地址 {fake} 向 {w.Address} 转入 0 币"); } - if (d.Rng.Chance(Tuning.TxPhishChance)) + if (d.Rng.Chance(Scales.R(Tuning.TxPhishChance))) { string fake = ChainAddress.Make(d.Rng.NextUInt()); if (Unlock.BlackUnlocked && d.Rng.Chance(0.5)) d.Black.Forum.Add("phish:" + fake); diff --git a/src/Decentralized/PledgerSkin.cs b/src/Decentralized/PledgerSkin.cs index 93a7f8e..8c60eec 100644 --- a/src/Decentralized/PledgerSkin.cs +++ b/src/Decentralized/PledgerSkin.cs @@ -184,7 +184,7 @@ namespace Decentralized } /// 应用整套 PLEDGER 皮肤:机身、边框、墨水屏、刻字、文字颜色、触控键、滚动条与数据表行。 - public static void Apply(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, ICollection picked, ICollection clear, IEnumerable rows = null) + public static void Apply(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, ICollection picked, ICollection clear, IEnumerable rows = null, float fontScale = 1f) { if (win?.root == null) return; Slice(win.root.GetComponent(), "ui_hw_shell", 10); @@ -210,6 +210,12 @@ namespace Decentralized if (content != null) content.childAlignment = TextAnchor.UpperCenter; if (screen == null) return; + if (fontScale > 1.001f) + foreach (var t in screen.GetComponentsInChildren(true)) + { + t.fontSize *= fontScale; + if (t.enableAutoSizing) { t.fontSizeMax *= fontScale; t.fontSizeMin *= fontScale; } + } // 收集所有表格行 root,避免按钮样式覆盖整行 var rowRoots = new HashSet(); diff --git a/src/Decentralized/Settings.cs b/src/Decentralized/Settings.cs new file mode 100644 index 0000000..b3b33ac --- /dev/null +++ b/src/Decentralized/Settings.cs @@ -0,0 +1,81 @@ +using Decentralized.Domain; +using UnityEngine; + +namespace Decentralized +{ + /// + /// mod 设置(游戏自带的模组配置:主菜单“改装件”里选中 mod 后点齿轮)。 + /// 文件在 persistentDataPath/ModConfigs/probably_decentralized.json,全局生效,不随存档。 + /// 分类(Section):界面、剧情、难度。 + /// + public static class Settings + { + /// 终端窗口尺寸档位(宽×高,界面单位)。第一项是旧版大小。 + public static readonly string[] TerminalSizes = { "828×680", "1200×900", "1400×1000" }; + public const string DefaultTerminalSize = "1200×900"; + + private static ConfigEntry _size, _risk, _mining; + private static ConfigEntry _tumbler; + + /// Tumbler 上门剧情开关:关掉后不再有新的上门,已有进度保留。 + public static bool TumblerStory => _tumbler?.Value ?? true; + + public static float TerminalW => Size().w; + public static float TerminalH => Size().h; + + private static (float w, float h) Size() + { + var s = _size?.Value ?? DefaultTerminalSize; + var p = s.Split('×'); + if (p.Length == 2 && float.TryParse(p[0], out var w) && float.TryParse(p[1], out var h)) return (w, h); + return (1200f, 900f); + } + + /// + /// 登记设置项。本地化表要等主菜单起来才读得到,而分类名是字符串(不是回调),所以等表可用了再登记。 + /// + public static void DefineWhenReady(ModConfig config) + { + var go = new GameObject("pd_settings_defer"); + Object.DontDestroyOnLoad(go); + go.AddComponent().Config = config; + } + + private sealed class Waiter : MonoBehaviour + { + public ModConfig Config; + private int _frames; + private void Update() + { + _frames++; + if (_frames < 300 || (L.T("settings.section.ui") == "settings.section.ui" && _frames < 1800)) return; + Log.Guard("登记 mod 设置", () => Define(Config)); + Destroy(gameObject); + } + } + + public static void Define(ModConfig config) + { + if (config == null) { Log.Warn("[Settings] 拿不到模组配置,用默认值"); return; } + string ui = L.T("settings.section.ui"); + _size = config.Define("terminal_size", DefaultTerminalSize, ui, () => L.T("settings.terminal_size"), TerminalSizes, + () => L.T("settings.terminal_size_desc")); + _size.OnChanged += _ => { if (CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(TerminalWindow.Id)) TerminalWindow.Open(); }; + + _tumbler = config.Define("tumbler_story", true, L.T("settings.section.story"), () => L.T("settings.tumbler_story"), () => L.T("settings.tumbler_story_desc")); + + string hard = L.T("settings.section.difficulty"); + _risk = config.Define("risk_scale", Scales.Default, hard, () => L.T("settings.risk_scale"), Scales.Choices, () => L.T("settings.risk_scale_desc")); + _mining = config.Define("mining_scale", Scales.Default, hard, () => L.T("settings.mining_scale"), Scales.Choices, () => L.T("settings.mining_scale_desc")); + _risk.OnChanged += _ => Apply(); + _mining.OnChanged += _ => Apply(); + Apply(); + } + + private static void Apply() + { + Scales.Risk = Scales.Parse(_risk?.Value); + Scales.Mining = Scales.Parse(_mining?.Value); + Log.Info($"[Settings] 风险 ×{Scales.Risk},挖矿 ×{Scales.Mining}"); } + } +} diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 3a6bce9..38ba854 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -16,8 +16,19 @@ namespace Decentralized public static class TerminalWindow { public const string Id = "pd_terminal"; - private const float WinW = 828f, WinH = 680f, ScreenW = 768f, ScreenH = 578f, Bezel = 6f; - private const float LeftW = 420f, RightW = 310f; + private const float Bezel = 6f; + // 窗口尺寸取自 mod 设置(828×680 是基准,各页布局按多出来的宽高等比放宽) + private static float WinW => Settings.TerminalW; + private static float WinH => Settings.TerminalH; + private static float ScreenW => WinW - 60f; + private static float ScreenH => WinH - 102f; + private static float Ext => ScreenW - 768f; + private static float LeftW => 420f + Ext * 0.6f; + private static float RightW => 310f + Ext * 0.4f; + /// 大窗口里文字放大(行高、卡片同步放大):基准窗口 1,1200 宽约 1.24,上限 1.3。 + private static float F => Mathf.Clamp(1f + Ext * 0.00065f, 1f, 1.3f); + public static float ChartScale => LeftW >= 634f ? 3f : 2f; + private static float[] Sc(float[] b) { float k = LeftW / 420f; return b.Select(x => x * k).ToArray(); } private enum Page { Home, Bank, Poinbase, Web3, Wallets } private static Page _page = Page.Home; @@ -69,7 +80,7 @@ namespace Decentralized b.End(); b.End(); var win = b.Show(); - Log.Guard("交易终端外观", () => PledgerSkin.Apply(win, screen, bezel, paper, _picked, _clear, _rows)); + Log.Guard("交易终端外观", () => PledgerSkin.Apply(win, screen, bezel, paper, _picked, _clear, _rows, F)); Log.Guard("购物车按钮", () => CartFab(win)); } @@ -84,7 +95,7 @@ namespace Decentralized private static void Entry(CustomUIBuilder b, string icon, string name, string line1, string line2, Page target) { - b.BeginPanel(230f, 250f, TextAnchor.UpperCenter, 10f, 6f); + b.BeginPanel((ScreenW - 80f) / 3f, 250f + (ScreenH - 578f) * 0.25f, TextAnchor.UpperCenter, 10f, 6f); var cardImg = b.Last.image; cardImg.sprite = MachineryStyle.White; cardImg.color = PledgerSkin.RowNormal; @@ -116,7 +127,7 @@ namespace Decentralized private static void Cell(CustomUIBuilder b, float w, string text, bool header = false, TextAnchor align = TextAnchor.MiddleLeft) { - b.BeginPanel(w, 26f, align, 0f, 0f); + b.BeginPanel(w, 26f * F, align, 0f, 0f); _clear.Add(b.Last.image); b.AddLabel(header ? "" + text + "" : text); PledgerSkin.FitCurrent(b); @@ -125,7 +136,7 @@ namespace Decentralized private static void TableRow(CustomUIBuilder b, float totalW, float[] widths, TextAnchor[] aligns, string[] cells, bool selected = false, Action onClick = null) { - b.BeginPanel(totalW, 28f, TextAnchor.MiddleLeft, 4f, 0f); + b.BeginPanel(totalW, 28f * F, TextAnchor.MiddleLeft, 4f, 0f); _rows.Add(new PledgerSkin.RowDef { Root = b.Last.root, Image = b.Last.image, Selected = selected, OnClick = onClick }); b.BeginRow(0f, TextAnchor.MiddleLeft); for (int i = 0; i < widths.Length; i++) @@ -142,7 +153,7 @@ namespace Decentralized private static void TableHeader(CustomUIBuilder b, float totalW, float[] widths, TextAnchor[] aligns, params string[] cells) { - b.BeginPanel(totalW, 26f, TextAnchor.MiddleLeft, 4f, 0f); + b.BeginPanel(totalW, 26f * F, TextAnchor.MiddleLeft, 4f, 0f); _clear.Add(b.Last.image); b.BeginRow(0f, TextAnchor.MiddleLeft); for (int i = 0; i < widths.Length; i++) @@ -199,10 +210,10 @@ namespace Decentralized AmlPreview(r); r.AddSpace(6f); r.AddButton(L.T("trade.deposit"), Deposit); - r.Last?.SetPreferredSize(RightW - 30f, 28f); + r.Last?.SetPreferredSize(RightW - 30f, 28f * F); r.AddSpace(4f); r.AddButton(L.T("trade.withdraw"), Withdraw); - r.Last?.SetPreferredSize(RightW - 30f, 28f); + r.Last?.SetPreferredSize(RightW - 30f, 28f * F); }); } @@ -216,7 +227,8 @@ namespace Decentralized b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)), taintRatio > 0.05 ? "aml_note" : "aml_dim"); } - private static readonly float[] StableW = { 80f, 100f, 220f }; + private static readonly float[] StableWBase = new[] { 80f, 100f, 220f }; + private static float[] StableW => Sc(StableWBase); private static string StableStatus(Stablecoins st, string asset, int day) { @@ -250,7 +262,8 @@ namespace Decentralized } } - private static readonly float[] HoldW = { 80f, 100f, 110f, 110f }; + private static readonly float[] HoldWBase = new[] { 80f, 100f, 110f, 110f }; + private static float[] HoldW => Sc(HoldWBase); private static readonly TextAnchor[] HoldAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight }; private static void Holdings(CustomUIBuilder b, Wallet w, Func show) @@ -323,7 +336,8 @@ namespace Decentralized private static string _perpProduct = "PSC"; private static bool _perpLong = true; private static int _perpLev = 3; - private static readonly float[] PerpW = { 80f, 70f, 110f, 60f, 70f }; + private static readonly float[] PerpWBase = new[] { 80f, 70f, 110f, 60f, 70f }; + private static float[] PerpW => Sc(PerpWBase); private static readonly TextAnchor[] PerpAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight }; private static double PerpSpot(string p) => p == "SPARK" ? Session.Data.Stable.Spark : Session.Data.Market.Price; @@ -409,7 +423,8 @@ namespace Decentralized }); } - private static readonly float[] PoolW = { 110f, 50f, 64f, 64f, 50f, 78f }; + private static readonly float[] PoolWBase = new[] { 110f, 50f, 64f, 64f, 50f, 78f }; + private static float[] PoolW => Sc(PoolWBase); private static readonly TextAnchor[] PoolAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight }; /// 第三方收益池:日息、储备、时间锁、审计、我的存款,存入 / 取出(合约调用,签名)。是不是骗局看不见。 @@ -804,7 +819,7 @@ namespace Decentralized { int tier = t; r.AddButton(L.T("intel.tip.buy", (int)Math.Round(Tuning.IntelTipAcc[t] * 100), Fmt.Num(Tuning.IntelTipPrice[t] * m.MaxLoss)), () => IntelBuyTip(mk, tier)); - r.Last?.SetPreferredSize(RightW - 30f, 28f); + r.Last?.SetPreferredSize(RightW - 30f, 28f * F); if (t < 2) r.AddSpace(2f); } } @@ -985,9 +1000,11 @@ namespace Decentralized { var d = Session.Data; b.AddLabel(L.T("wallets.count", d.Wallets.Count, Tuning.WalletMax), "wallets_count_dim"); - var cols = L.Wide ? new[] { 100f, 92f, 62f, 62f, 372f } : new[] { 100f, 110f, 85f, 70f, 375f }; + bool wide = L.Wide && Ext < 100f; + float k = Ext < 100f ? 1f : (ScreenW - 28f) / 740f; + var cols = (wide ? new[] { 100f, 92f, 62f, 62f, 372f } : new[] { 100f, 110f, 85f, 70f, 375f }).Select(x => x * k).ToArray(); var aligns = new[] { TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleLeft }; - TableHeader(b, 740f, cols, aligns, L.T("wallets.col.name"), L.T("wallets.col.addr"), L.T("col.value"), L.T("wallets.col.taint"), ""); + TableHeader(b, 740f * k, cols, aligns, L.T("wallets.col.name"), L.T("wallets.col.addr"), L.T("col.value"), L.T("wallets.col.taint"), ""); for (int i = 0; i < d.Wallets.Count; i++) { var w = d.Wallets[i]; @@ -995,7 +1012,7 @@ namespace Decentralized bool isCurrent = idx == d.Current; // 一行三块:数据格(整行反色的是这一块)、操作键(和顶栏页签一样的黑色触控键,放在纸面上)、连接状态靠右 b.BeginRow(4f, TextAnchor.MiddleLeft); - b.BeginPanel(cols[0] + cols[1] + cols[2] + cols[3] + 8f, 32f, TextAnchor.MiddleLeft, 4f, 0f); + b.BeginPanel(cols[0] + cols[1] + cols[2] + cols[3] + 8f, 32f * F, TextAnchor.MiddleLeft, 4f, 0f); _rows.Add(new PledgerSkin.RowDef { Root = b.Last.root, @@ -1010,7 +1027,7 @@ namespace Decentralized Cell(b, cols[3], Fmt.Credits(w.Taint), false, TextAnchor.MiddleRight); b.End(); b.End(); - b.BeginPanel(cols[4] - (L.Wide ? 112f : 150f), 32f, TextAnchor.MiddleLeft, 0f, 0f); + b.BeginPanel(cols[4] - (wide ? 112f : 150f) * k, 32f * F, TextAnchor.MiddleLeft, 0f, 0f); _clear.Add(b.Last.image); b.BeginRow(4f, TextAnchor.MiddleLeft); RowKey(b, L.T("wallets.open"), () => WalletWindow.Open(w)); @@ -1019,7 +1036,7 @@ namespace Decentralized if (idx > 0) RowKey(b, _destroyPick == w.Address ? L.T("wallets.destroy.sure") : L.T("wallets.destroy"), () => DestroyWallet(w)); b.End(); b.End(); - b.BeginPanel(L.Wide ? 92f : 84f, 32f, TextAnchor.MiddleRight, 0f, 0f); + b.BeginPanel((wide ? 92f : 84f) * k, 32f * F, TextAnchor.MiddleRight, 0f, 0f); _clear.Add(b.Last.image); b.BeginRow(0f, TextAnchor.MiddleRight); if (idx != d.Current) RowKey(b, L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); }); @@ -1038,7 +1055,7 @@ namespace Decentralized var go = b.Last.root; if (go == null) return; var le = go.GetComponent() ?? go.AddComponent(); - le.minHeight = 26f; le.preferredHeight = 26f; le.flexibleHeight = 0f; + le.minHeight = 26f * F; le.preferredHeight = 26f * F; le.flexibleHeight = 0f; } private static string _destroyPick, _renamePick, _renameText = ""; @@ -1230,7 +1247,8 @@ namespace Decentralized b.AddLabel(f.StartsWith("phish:") ? L.T("black.forum.phish", f.Substring(6)) : f.StartsWith("scam:") ? L.T("black.forum.scam", BlackDelivery.DisplayName(f.Substring(5))) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f), "forum_dim"); } - private const float SellerW = 176f, ShelfW = 560f; + private static float SellerW => 176f * F; + private static float ShelfW => 560f + Ext * 0.78f; private static void PlatformShelf(CustomUIBuilder b, SaveData d) { @@ -1242,7 +1260,7 @@ namespace Decentralized c.AddLabel(L.T("black.price", offer.Price)); c.AddFlexibleSpace(); c.AddButton(L.T("black.buy"), () => AddToCart(-1, offer)); - }, 3); + }); } // ——— 付款面板 ——— @@ -1425,7 +1443,7 @@ namespace Decentralized c.AddLabel(L.T("black.price", g.Price)); c.AddFlexibleSpace(); c.AddButton(L.T("black.sell.list"), () => BeginEdit(new EditPrice { Item = it, Id = g.Item, Ref = BlackDelivery.RefValue(it) }, g.Price)); - }, 3); + }); b.End(); b.End(); } @@ -1725,12 +1743,14 @@ namespace Decentralized }, 3); } - private const int ShelfCols = 4; - private const float CardW = 172f, CardH = 268f; + private static int ShelfCols => Ext < 100f ? 3 : Math.Max(3, (int)Math.Round(ShelfW / (172f * F))); + private static float CardW => Ext < 100f ? 172f : (ShelfW - 6f * (ShelfCols - 1)) / ShelfCols; + private static float CardH => 268f * F; /// 货架:一行 4 张商品卡。 - private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List goods, Action card, int cols = ShelfCols) + private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List goods, Action card, int cols = -1) { + if (cols <= 0) cols = ShelfCols; for (int i = 0; i < goods.Count; i += cols) { b.BeginRow(6f); @@ -1755,7 +1775,7 @@ namespace Decentralized var sprite = Icon(g.Item); if (sprite != null) c.AddImage(sprite, 64f, 64f); else c.AddSpace(64f); c.AddLabel("" + ItemName(g.Item) + ""); - PledgerSkin.FitBox(c, 52f); + PledgerSkin.FitBox(c, 52f * F); bool asic = g.Kind == GoodKind.Gpu && g.Model != null && Hardware.Specs.TryGetValue(g.Model, out var gs) && gs.Asic; c.AddLabel(asic ? (g.Health >= 100 ? L.T("black.kind.asicnew") : L.T("black.kind.asic", (int)Math.Round(g.Health))) : g.Kind == GoodKind.Gpu ? (g.Health >= 100 ? L.T("black.kind.gpunew") : L.T("black.kind.gpu", (int)Math.Round(g.Health))) : L.T("black.kind." + g.Kind + (g.Kind == GoodKind.Contraband ? "." + g.Level : "")), "good_kind_dim"); if (!string.IsNullOrEmpty(line2)) c.AddLabel(line2, "good_line2_dim"); @@ -1902,14 +1922,14 @@ namespace Decentralized { var d = Session.Data; b.BeginRow(8f, TextAnchor.UpperLeft); - b.BeginPanel(SellerW + 8f + 150f, -1f, TextAnchor.UpperLeft, 4f, 4f); + b.BeginPanel((SellerW + 8f + ShelfW) * 0.45f, -1f, TextAnchor.UpperLeft, 4f, 4f); _clear.Add(b.Last.image); b.AddHeader(L.T("black.orders.in")); if (d.Black.Orders.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_in_none_dim"); foreach (var o in d.Black.Orders.OrderByDescending(x => x.PlacedDay)) b.AddLabel(L.T("black.mine.line", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), BlackDelivery.DisplayName(o.Seller), L.T("black.mode." + o.Mode), L.T("black.state." + o.State + (o.Why != null && o.State != OrderState.Done ? "." + o.Why : "")), o.DueDay)); b.End(); - b.BeginPanel(ShelfW - 150f, -1f, TextAnchor.UpperLeft, 4f, 4f); + b.BeginPanel((SellerW + 8f + ShelfW) * 0.55f, -1f, TextAnchor.UpperLeft, 4f, 4f); _clear.Add(b.Last.image); b.AddHeader(L.T("black.orders.out")); if (d.Black.Sales.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_out_none_dim"); @@ -1979,7 +1999,8 @@ namespace Decentralized private static Web3Tab _web3Tab = Web3Tab.Trade; private static string _mixAsset = Assets.PSC; private static int _mixRounds = 2; - private static readonly float[] MixW = { 70f, 100f, 100f, 80f }; + private static readonly float[] MixWBase = new[] { 70f, 100f, 100f, 80f }; + private static float[] MixW => Sc(MixWBase); private static readonly TextAnchor[] MixAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight }; /// 混币器:把币存进去,几轮之后提回同一个地址,污点降低,每轮扣手续费。 @@ -2048,7 +2069,8 @@ namespace Decentralized Open(); } - private static readonly float[] StakeW = { 110f, 90f, 200f }; + private static readonly float[] StakeWBase = new[] { 110f, 90f, 200f }; + private static float[] StakeW => Sc(StakeWBase); private static readonly TextAnchor[] StakeAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight }; /// 质押页:革命军 CRA 质押池(日息、解锁排队)和平衡代币 SPARK 的铸销。都是合约调用,要在钱包上签名。 diff --git a/src/Decentralized/TumblerStory.cs b/src/Decentralized/TumblerStory.cs index 36705c4..934ac0c 100644 --- a/src/Decentralized/TumblerStory.cs +++ b/src/Decentralized/TumblerStory.cs @@ -18,7 +18,7 @@ namespace Decentralized private static void OnGenerate(StoreClientManager manager) { var d = Session.Data; - if (d == null || !Unlock.IsUnlocked) return; + if (d == null || !Unlock.IsUnlocked || !Settings.TumblerStory) return; Log.Guard("Tumbler", () => { int day = StoreStation.GetDayCounter(); diff --git a/src/Decentralized/WalletWindow.cs b/src/Decentralized/WalletWindow.cs index 6fc024e..bf923e2 100644 --- a/src/Decentralized/WalletWindow.cs +++ b/src/Decentralized/WalletWindow.cs @@ -273,7 +273,7 @@ namespace Decentralized { string clip = Clipboard.Paste(); // 被投毒的钱包:固件里的木马有时把粘贴的地址换成长得很像的骗子地址 - if (w.Poisoned && ChainAddress.Valid(clip) && Session.Data.Rng.Chance(Tuning.TxHijackChance)) { clip = ChainAddress.Lookalike(clip, Session.Data.Rng); Log.Info($"剪贴板木马:{Clipboard.Paste()} → {clip}"); } + if (w.Poisoned && ChainAddress.Valid(clip) && Session.Data.Rng.Chance(Scales.R(Tuning.TxHijackChance))) { clip = ChainAddress.Lookalike(clip, Session.Data.Rng); Log.Info($"剪贴板木马:{Clipboard.Paste()} → {clip}"); } dev.To = clip; dev.Note = null; Open(dev); }); b.End(); @@ -546,7 +546,7 @@ namespace Decentralized var mine = d.Own(tx.To); if (mine != null) TxRules.Record(mine.History, new TxRecord { Addr = w.Address, Asset = tx.Asset, Amount = amt, Day = day, In = true }); // 地址投毒:链上公开的转账被人盯上,过一两天会收到一笔来自相似地址的 0 币转入 - else if (d.Rng.Chance(Tuning.TxPoisonChance)) d.PoisonPlans.Add(new PoisonPlan { Wallet = w.Address, Target = tx.To, Day = day + d.Rng.Range(1, Tuning.TxPoisonDelayMax) }); + else if (d.Rng.Chance(Scales.R(Tuning.TxPoisonChance))) d.PoisonPlans.Add(new PoisonPlan { Wallet = w.Address, Target = tx.To, Day = day + d.Rng.Range(1, Tuning.TxPoisonDelayMax) }); } if (tx.Bill != null && !tx.Bill.Contract) { diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index d97ce69..51ebb66 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -1565,6 +1565,22 @@ internal static class Program } }); + Test("mod 设置倍率:解析、封顶、挖矿产出按倍率", () => + { + Check(Scales.Parse("×1.5") == 1.5 && Scales.Parse("×0.5") == 0.5 && Scales.Parse("x") == 1 && Scales.Parse(null) == 1, "解析档位文字"); + foreach (var c in Scales.Choices) Check(Scales.Parse(c) > 0, "档位都能解析:" + c); + try + { + Scales.Risk = 2; Check(Math.Abs(Scales.R(0.3) - 0.6) < 1e-9 && Scales.R(0.8) == 1, "风险倍率乘概率并封顶 1"); + Scales.Risk = 1; + var n = new Network(); + Scales.Mining = 1; double a = n.Mine(100); + Scales.Mining = 2; double b2 = n.Mine(100); + Check(a > 0 && Math.Abs(b2 - 2 * a) < 1e-9, "挖矿产出按倍率"); + } + finally { Scales.Risk = 1; Scales.Mining = 1; } + }); + Test("预测市场:先验概率", () => { Check(Math.Abs(PredictMath.PurityAtLeast(8500, 8000, 8999) - 0.5) < 1e-9, "纯度 8500 以上一半");