Rig: game module bay with damped, capped effects

The rig gets the game's universal module bay (module adjacency included).
The machine's summed performance, efficiency and quality percentages are
divided by 4, like the ratios vanilla machines use, and capped: hashrate
at most +20%, power saving at most 25%, card wear at most -30%. Performance
also raises wear and burn-out chance by the same amount; negative
efficiency (overclock module, neural cores) raises power draw. Replaces the
planned mod-specific overclock module. Tested in game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-03 09:16:46 +08:00
1 parent 750b26e4b3
commit 79d4c686a0
9 files changed
+113 -44

No files matched your search

+1
View File
@@ -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 自带超频模块。
+35 -10
View File
@@ -48,6 +48,33 @@ namespace Decentralized.Domain
};
}
/// <summary>
/// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 <see cref="Tuning.ModuleRatio"/> 后生效,
/// 并有上限,避免模块把挖矿收益放大到破坏经济:算力最多 +20%,省电最多 25%,损耗最多降 30%。
/// 性能加成同时按比例提高损耗与暴毙率。负数(例如节能模块的负性能、超频模块的负效率)照常生效,耗电增加不设上限。
/// </summary>
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));
}
/// <summary>一张显卡或一台矿机的实例状态(存进物品标签)。</summary>
public sealed class Unit
{
@@ -62,29 +89,27 @@ namespace Decentralized.Domain
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model];
/// <summary>当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。</summary>
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;
/// <summary>今晚的暴毙概率。</summary>
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;
}
/// <summary>一夜的损耗与暴毙判定;返回是否在今晚暴毙。</summary>
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;
+2 -1
View File
@@ -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;
/// <summary>矿架上模块百分比的折算系数(原版机器用 2–4),以及各项上限。</summary>
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;
+42 -18
View File
@@ -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<GameItem> 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)
/// <summary>按模块槽里的模块重新汇总三项百分比(游戏自己的公式,含相邻效果),换算成本 mod 的模块效果。</summary>
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());
}
/// <summary>夜间早段:按矿架上的显卡总耗电从电池扣电;扣电成功,登记今晚参与挖矿的显卡。</summary>
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 + "%";
}
/// <summary>夜间早段:按矿架上的显卡总耗电(含模块效果)从电池扣电;扣电成功,登记今晚参与挖矿的显卡和模块效果。</summary>
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<GameItem, ModuleBonus>(c, b));
UpdateRig(rig, modules, cards);
}
/// <summary>
@@ -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}%."
1 # Probably Decentralized localization table: English (en)
7 item.terminal.desc
8 item.rig.name
9 item.rig.desc
10 rig.label.modules
11 item.rig.modules
12 rig.label.cards
13 item.gpu.name
14 item.gpu.desc
@@ -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}%。
1 # Probably Decentralized 本地化表:简体中文(zh)
6 item.terminal.desc,钱包、交易所与夜间挖矿报告。
7 item.rig.name,矿架
8 item.rig.desc,可装 4 张显卡。算力 {0} MH/s。耗电 {1} kJ/天。
9 rig.label.modules,模块
10 item.rig.modules,模块:算力 {0},耗电 {1},损耗 {2}。
11 rig.label.cards,显卡
12 item.gpu.name,{0} {1}
13 item.gpu.desc,算力 {0} MH/s。耗电 {1} kJ/天。健康度 {2}%。
+4 -4
View File
@@ -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);
}
+1 -1
View File
@@ -37,7 +37,7 @@ namespace Decentralized
public static SaveData Data;
/// <summary>今晚有电的矿架上的显卡(矿架在夜间早段扣电时登记,结算时用)。</summary>
public static readonly List<GameItem> TonightCards = new List<GameItem>();
public static readonly List<KeyValuePair<GameItem, ModuleBonus>> TonightCards = new List<KeyValuePair<GameItem, ModuleBonus>>();
public static int TonightRigs, TonightPoweredRigs;
public static double TonightKj;
+24 -10
View File
@@ -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}");
});