diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a95907..4cc5c50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,3 +6,4 @@
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
- mod 本体(第一阶段的第一部分):人脉网络"矿业渠道"(300)交付交易终端、矿架和一张 RZX 50;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSCoin;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSCoin 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。
- 物品图:art/sprites.py 生成显卡(Probidia、PMD、烧毁)、矿架、交易终端。
+- 矿架加入游戏原有的通用模块槽:性能、效率、品质三项百分比折算 1/4 后作用于算力、耗电和显卡损耗,算力最多 +20%、省电最多 25%、损耗最多 −30%;性能加成同时提高损耗与暴毙率。取代原计划的 mod 自带超频模块。
diff --git a/src/Decentralized.Domain/Mining.cs b/src/Decentralized.Domain/Mining.cs
index 83816ca..ea92bf9 100644
--- a/src/Decentralized.Domain/Mining.cs
+++ b/src/Decentralized.Domain/Mining.cs
@@ -48,6 +48,33 @@ namespace Decentralized.Domain
};
}
+ ///
+ /// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 后生效,
+ /// 并有上限,避免模块把挖矿收益放大到破坏经济:算力最多 +20%,省电最多 25%,损耗最多降 30%。
+ /// 性能加成同时按比例提高损耗与暴毙率。负数(例如节能模块的负性能、超频模块的负效率)照常生效,耗电增加不设上限。
+ ///
+ public struct ModuleBonus
+ {
+ public double Hash; // 算力乘数
+ public double Kj; // 耗电乘数
+ public double Wear; // 每日损耗乘数
+ public double Death; // 暴毙率乘数
+
+ public static ModuleBonus None => new ModuleBonus { Hash = 1, Kj = 1, Wear = 1, Death = 1 };
+
+ public static ModuleBonus From(int performance, int efficiency, int quality)
+ {
+ double r = Tuning.ModuleRatio * 100.0;
+ double hash = Clamp(performance / r, -0.5, Tuning.ModuleHashCap);
+ double save = Clamp(efficiency / r, -1.0, Tuning.ModuleKjCap);
+ double wearCut = Clamp(quality / r, -1.0, Tuning.ModuleWearCap);
+ double heat = 1 + Math.Max(0, hash);
+ return new ModuleBonus { Hash = 1 + hash, Kj = 1 - save, Wear = (1 - wearCut) * heat, Death = heat };
+ }
+
+ private static double Clamp(double v, double lo, double hi) => Math.Max(lo, Math.Min(hi, v));
+ }
+
/// 一张显卡或一台矿机的实例状态(存进物品标签)。
public sealed class Unit
{
@@ -62,29 +89,27 @@ namespace Decentralized.Domain
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model];
/// 当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。
- public double Hash(bool overclock)
+ public double Hash(ModuleBonus b)
{
if (Dead) return 0;
- double h = Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50);
- return overclock ? h * Tuning.OcHash : h;
+ return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash;
}
- public double Kj(bool overclock) => Dead ? 0 : Chip.Kj * (overclock ? Tuning.OcKj : 1);
+ public double Kj(ModuleBonus b) => Dead ? 0 : Chip.Kj * b.Kj;
/// 今晚的暴毙概率。
- public double DeathChance(bool overclock)
+ public double DeathChance(ModuleBonus b)
{
double p = Tuning.DeathBase + Tuning.DeathWorn * Math.Pow(1 - Health / 100, 2);
- p *= Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * (overclock ? Tuning.OcDeath : 1);
- return p;
+ return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death;
}
/// 一夜的损耗与暴毙判定;返回是否在今晚暴毙。
- public bool Night(bool overclock, Rng rng)
+ public bool Night(ModuleBonus b, Rng rng)
{
if (Dead) return false;
- Health = Math.Max(0, Health - Label.WearFactor * (overclock ? Tuning.OcWear : 1));
- if (rng.Chance(DeathChance(overclock)))
+ Health = Math.Max(0, Health - Label.WearFactor * b.Wear);
+ if (rng.Chance(DeathChance(b)))
{
Dead = true;
return true;
diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs
index bb858f4..0e52271 100644
--- a/src/Decentralized.Domain/Tuning.cs
+++ b/src/Decentralized.Domain/Tuning.cs
@@ -16,7 +16,8 @@ namespace Decentralized.Domain
public const double NpcAdj = 0.05;
public const double PoolFee = 0.03;
public const int RigPrice = 400, RigKj = 2, RigSlots = 4;
- public const double OcHash = 1.25, OcKj = 1.40, OcWear = 1.3, OcDeath = 1.2;
+ /// 矿架上模块百分比的折算系数(原版机器用 2–4),以及各项上限。
+ public const double ModuleRatio = 4, ModuleHashCap = 0.20, ModuleKjCap = 0.25, ModuleWearCap = 0.30;
public const double DeathBase = 0.001, DeathWorn = 0.03, FakeDeathFactor = 2;
public const double RefurbishHealth = 30;
diff --git a/src/Decentralized/Items.cs b/src/Decentralized/Items.cs
index cf03473..ea13fbf 100644
--- a/src/Decentralized/Items.cs
+++ b/src/Decentralized/Items.cs
@@ -120,9 +120,11 @@ namespace Decentralized
private static GameItem CreateRig()
{
var battery = new GameSlotInventory();
+ var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7);
var cards = new GameGridInventory(2, 4);
var window = MachineHelper.CreateMachineryWindow(true,
MachineHelper.PowerPanel(battery),
+ MachineHelper.Panel(L.T("rig.label.modules"), modules, "module"),
MachineHelper.Panel(L.T("rig.label.cards"), cards, "cards"));
var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window);
rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig");
@@ -132,41 +134,63 @@ namespace Decentralized
rig.EnableTag("CONTAINER_TAG");
rig.EnableTag("STANDARD_MACHINE_TAG");
MachineryHelper.InitMachinery(rig, Tuning.RigKj);
+ // 游戏的通用模块槽(含模块相邻效果);模块变动后游戏会按原版公式改写耗电,再由 UpdateRig 按本 mod 的规则重算
+ MachineHelper.SetupModuleBay(modules, rig, () => UpdateRig(rig, modules, cards));
cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag);
- cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, cards);
- cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, cards);
- rig.onLoaded = _ => UpdateRig(rig, cards);
- rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, cards));
+ cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards);
+ cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards);
+ rig.onLoaded = _ => UpdateRig(rig, modules, cards);
+ rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, modules, cards));
ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name);
- UpdateRig(rig, cards);
+ UpdateRig(rig, modules, cards);
return rig;
}
public static List Cards(GameInventory cards) => cards.childItems.Where(IsGpu).ToList();
- private static int RigKj(GameInventory cards) =>
- Tuning.RigKj + Mathf.CeilToInt((float)Cards(cards).Select(ToUnit).Sum(u => u.Kj(false)));
-
- private static void UpdateRig(GameItem rig, GameInventory cards)
+ /// 按模块槽里的模块重新汇总三项百分比(游戏自己的公式,含相邻效果),换算成本 mod 的模块效果。
+ public static ModuleBonus Bonus(GameItem rig, GameInventory modules)
{
- int kj = RigKj(cards);
- rig.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(kj));
- double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(false));
- rig.shortDescription = L.T("item.rig.desc", Fmt.Num(hash * 10), kj);
+ ModuleHelper.ComputeModuleEffect(modules.childItems, rig);
+ return ModuleBonus.From(
+ rig.GetTagReadonly(ModuleHelper.TOTAL_PERCENTAGE_PERFORMANCE_BONUS_INT).GetInt(),
+ rig.GetTagReadonly(ModuleHelper.TOTAL_PERCENTAGE_POWER_BONUS_INT).GetInt(),
+ rig.GetTagReadonly(ModuleHelper.TOTAL_PERCENTAGE_QUALITY_BONUS_INT).GetInt());
}
- /// 夜间早段:按矿架上的显卡总耗电从电池扣电;扣电成功,登记今晚参与挖矿的显卡。
- private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory cards)
+ private static int RigKj(GameInventory cards, ModuleBonus b) =>
+ Tuning.RigKj + Mathf.CeilToInt((float)Cards(cards).Select(ToUnit).Sum(u => u.Kj(b)));
+
+ private static void UpdateRig(GameItem rig, GameInventory modules, GameInventory cards)
+ {
+ var b = Bonus(rig, modules);
+ int kj = RigKj(cards, b);
+ rig.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(kj));
+ double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(b));
+ rig.shortDescription = L.T("item.rig.desc", Fmt.Num(hash * 10), kj);
+ if (b.Hash != 1 || b.Kj != 1 || b.Wear != 1)
+ rig.shortDescription += " " + L.T("item.rig.modules", Pct(b.Hash), Pct(b.Kj), Pct(b.Wear));
+ }
+
+ private static string Pct(double mult)
+ {
+ int p = (int)Math.Round((mult - 1) * 100);
+ return p > 0 ? "+" + p + "%" : p + "%";
+ }
+
+ /// 夜间早段:按矿架上的显卡总耗电(含模块效果)从电池扣电;扣电成功,登记今晚参与挖矿的显卡和模块效果。
+ private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory modules, GameInventory cards)
{
var list = Cards(cards).Where(c => !c.IsTag(DeadTag)).ToList();
Session.TonightRigs++;
if (list.Count == 0) return;
- int kj = RigKj(cards);
+ var b = Bonus(rig, modules);
+ int kj = RigKj(cards, b);
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return;
Session.TonightPoweredRigs++;
Session.TonightKj += kj;
- Session.TonightCards.AddRange(list);
- UpdateRig(rig, cards);
+ foreach (var c in list) Session.TonightCards.Add(new KeyValuePair(c, b));
+ UpdateRig(rig, modules, cards);
}
///
diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv
index b7d905f..ea08bd7 100644
--- a/src/Decentralized/ModFiles/Localization/en.csv
+++ b/src/Decentralized/ModFiles/Localization/en.csv
@@ -7,6 +7,8 @@ item.terminal.name,Trading Terminal
item.terminal.desc,Wallet, exchange and the nightly mining report.
item.rig.name,Mining Rig
item.rig.desc,"Holds up to four graphics cards. Hashrate {0} MH/s. Draws {1} kJ per day."
+rig.label.modules,Modules
+item.rig.modules,"Modules: hashrate {0}, power draw {1}, wear {2}."
rig.label.cards,Graphics cards
item.gpu.name,{0} {1}
item.gpu.desc,"Hashrate {0} MH/s. Draws {1} kJ per day. Condition {2}%."
diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv
index 75deaf7..b0a8d62 100644
--- a/src/Decentralized/ModFiles/Localization/zh.csv
+++ b/src/Decentralized/ModFiles/Localization/zh.csv
@@ -6,6 +6,8 @@ item.terminal.name,交易终端
item.terminal.desc,钱包、交易所与夜间挖矿报告。
item.rig.name,矿架
item.rig.desc,可装 4 张显卡。算力 {0} MH/s。耗电 {1} kJ/天。
+rig.label.modules,模块
+item.rig.modules,模块:算力 {0},耗电 {1},损耗 {2}。
rig.label.cards,显卡
item.gpu.name,{0} {1}
item.gpu.desc,算力 {0} MH/s。耗电 {1} kJ/天。健康度 {2}%。
diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs
index 245e922..2fd8a0c 100644
--- a/src/Decentralized/Night.cs
+++ b/src/Decentralized/Night.cs
@@ -86,8 +86,8 @@ namespace Decentralized
d.ActiveEvents = events;
d.Network.Availability = (events.Contains(MarketEvent.Blackout) ? 0.8 : 1) * (events.Contains(MarketEvent.LowerUnrest) ? 0.9 : 1);
- var units = Session.TonightCards.Select(c => (item: c, unit: Items.ToUnit(c))).ToList();
- double hash = units.Sum(x => x.unit.Hash(false));
+ var units = Session.TonightCards.Select(c => (item: c.Key, unit: Items.ToUnit(c.Key), bonus: c.Value)).ToList();
+ double hash = units.Sum(x => x.unit.Hash(x.bonus));
double mined = d.Network.Mine(hash);
if (mined > 0) d.Hot.Add(Assets.PSC, mined);
@@ -96,9 +96,9 @@ namespace Decentralized
Day = day, Rigs = Session.TonightRigs, PoweredRigs = Session.TonightPoweredRigs, Kj = Session.TonightKj,
Hash = hash, NetworkHash = d.Network.NpcHash * d.Network.Availability + hash, Mined = mined,
};
- foreach (var (item, unit) in units)
+ foreach (var (item, unit, bonus) in units)
{
- bool died = unit.Night(false, d.Rng);
+ bool died = unit.Night(bonus, d.Rng);
Items.Store(item, unit);
if (died) report.Burnt.Add(item.name);
}
diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs
index c53a5f0..6c9e7f5 100644
--- a/src/Decentralized/Session.cs
+++ b/src/Decentralized/Session.cs
@@ -37,7 +37,7 @@ namespace Decentralized
public static SaveData Data;
/// 今晚有电的矿架上的显卡(矿架在夜间早段扣电时登记,结算时用)。
- public static readonly List TonightCards = new List();
+ public static readonly List> TonightCards = new List>();
public static int TonightRigs, TonightPoweredRigs;
public static double TonightKj;
diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs
index 3151ae4..eaa56ef 100644
--- a/tests/Decentralized.Domain.Tests/Program.cs
+++ b/tests/Decentralized.Domain.Tests/Program.cs
@@ -115,22 +115,36 @@ internal static class Program
Check(Hardware.Specs["RZX70"].DisplayHash == 50 && Hardware.Specs["RZ6800"].DisplayHash == 40, "RZX70 50 MH/s,RZ6800 40 MH/s");
Check(Near(Hardware.Specs["M3"].DisplayHash, 3, 1e-9), "RatDigg M3 3 GH/s");
var u = new Unit { Model = "RZX70" };
- Check(u.Hash(false) == 5, "满健康 5 H");
+ Check(u.Hash(ModuleBonus.None) == 5, "满健康 5 H");
u.Health = 25;
- Check(Near(u.Hash(false), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H");
+ Check(Near(u.Hash(ModuleBonus.None), 4.0, 1e-9), "健康 25:5 × (0.6 + 0.4 × 0.5) = 4 H");
u.Health = 0;
- Check(Near(u.Hash(false), 3.0, 1e-9), "健康 0:60%");
- Check(Near(new Unit { Model = "RZX70" }.Hash(true), 6.25, 1e-9), "超频 +25%");
+ Check(Near(u.Hash(ModuleBonus.None), 3.0, 1e-9), "健康 0:60%");
+ Check(Near(new Unit { Model = "RZX70" }.Hash(ModuleBonus.From(36, -44, 0)), 5 * 1.09, 1e-9), "一个超频模块(+36 性能):算力 +9%");
var fake = new Unit { Model = "RZX90", FakeOf = "RZX60" };
- Check(fake.Hash(false) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60");
- Check(Near(fake.DeathChance(false), new Unit { Model = "RZX90" }.DeathChance(false) * 2, 1e-12), "假卡暴毙率 ×2");
+ Check(fake.Hash(ModuleBonus.None) == 1.5 && fake.Label.Model == "RZX90", "假卡标着 RZX90,算力按 RZX60");
+ Check(Near(fake.DeathChance(ModuleBonus.None), new Unit { Model = "RZX90" }.DeathChance(ModuleBonus.None) * 2, 1e-12), "假卡暴毙率 ×2");
var r = new Unit { Model = "RZX70", Health = 50 };
Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新");
});
+ Test("模块:折算系数 4,算力 +20%、省电 25%、损耗 −30% 封顶;性能加成提高损耗与暴毙", () =>
+ {
+ var perf = ModuleBonus.From(16, 0, 0);
+ Check(Near(perf.Hash, 1.04, 1e-9) && Near(perf.Wear, 1.04, 1e-9) && Near(perf.Death, 1.04, 1e-9), "一个性能模块(+16):算力 +4%,损耗和暴毙 +4%");
+ var cores = ModuleBonus.From(200 * 3, -100 * 3, 100 * 3);
+ Check(Near(cores.Hash, 1.20, 1e-9), $"塞满神经核心,算力也只 +20%:{cores.Hash:0.00}");
+ Check(Near(cores.Kj, 1.75, 1e-9), $"神经核心的负效率照常增加耗电:×{cores.Kj:0.00}");
+ Check(Near(cores.Wear, 0.70 * 1.20, 1e-9), "品质封顶 −30%,再乘性能带来的 +20%");
+ var eco = ModuleBonus.From(-16, 32 * 5, 0);
+ Check(Near(eco.Kj, 0.75, 1e-9) && Near(eco.Hash, 0.96, 1e-9), "省电封顶 25%;节能模块的负性能照常降低算力");
+ var none = ModuleBonus.From(0, 0, 0);
+ Check(none.Hash == 1 && none.Kj == 1 && none.Wear == 1 && none.Death == 1, "没有模块时不变");
+ });
+
Test("显卡:累计暴毙概率与模拟器一致(第 50 / 100 / 150 天)", () =>
{
- double[] Run(string model, bool oc, int n)
+ double[] Run(string model, ModuleBonus b, int n)
{
var rng = new Rng(2026);
var deadBy = new int[3];
@@ -139,7 +153,7 @@ internal static class Program
var u = new Unit { Model = model };
for (int d = 1; d <= 150; d++)
{
- u.Night(oc, rng);
+ u.Night(b, rng);
if (u.Dead)
{
if (d <= 50) deadBy[0]++;
@@ -151,10 +165,10 @@ internal static class Program
}
return deadBy.Select(x => (double)x / n).ToArray();
}
- var p = Run("RZX70", false, 20000);
+ var p = Run("RZX70", ModuleBonus.None, 20000);
Check(Near(p[0], 0.11, 0.02) && Near(p[1], 0.53, 0.02) && Near(p[2], 0.84, 0.02),
$"Probidia 11% / 53% / 84%:{p[0]:P0} / {p[1]:P0} / {p[2]:P0}");
- var q = Run("RZ6800", false, 20000);
+ var q = Run("RZ6800", ModuleBonus.None, 20000);
Check(Near(q[0], 0.20, 0.02) && Near(q[1], 0.76, 0.02) && Near(q[2], 0.95, 0.02),
$"PMD 20% / 76% / 95%:{q[0]:P0} / {q[1]:P0} / {q[2]:P0}");
});