diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd8081..d5f30e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,4 +12,6 @@ - 入口升级归入游戏原有的"服务"栏(和 Probably Automated 一样),图标用交易终端。 - 修复矿架、交易终端与别的物品重叠:游戏按 mod 物品图每格中心的一个像素判断这一格算不算占用,矿架的卡位之间有缝,缝正好落在中心行上,8 格被当成空格。矿架加了实心背板、终端支架拉通;art/sprites.py 生成时按游戏的方式检查每格中心,有透明格就报错。 - 矿架重画成和原版机器一样的略微俯视:顶面(带散热格栅和指示灯)、受光的左缘、暗的右侧面、正面三层显卡位和电源口、底部两只脚,轮廓削圆。 +- 交易终端和显卡重画成略微俯视:终端是带顶面的显示器加梯形键盘;显卡是一块真正的显卡(左端金属挡板带接口、两个风扇、品牌色条、底边金手指和 PCIe 缺口、顶上供电口),尺寸改为 2×2 格。 +- 矿架默认 2 个显卡槽,投入电子元件升级,最多 5 个:第 3 槽 4 个普通电子元件,第 4 槽 1 个稀有,第 5 槽 2 个稀有。窗口里新增"升级槽位",只收下一级需要的元件,放进去立即消耗并累计进度;显卡格子的形状随槽数变化,槽数与进度存在矿架标签上。 diff --git a/art/sprites.py b/art/sprites.py index bd9989a..02747c1 100644 --- a/art/sprites.py +++ b/art/sprites.py @@ -78,32 +78,75 @@ class Canvas: def gpu(accent, accent_dark, burnt=False): - """显卡(32×16,2×1 格):外壳、两个风扇、品牌色条、金手指。""" - c = Canvas(32, 16) - c.box(1, 2, 30, 12, 'm') - c.rect(2, 3, 29, 3, 'l') # 顶边受光 - c.rect(2, 11, 29, 11, 'd') # 底边暗面 - for cx in (9, 21): # 风扇 - for y in range(5, 11): - for x in range(cx - 4, cx + 5): - dx, dy = x - cx, y - 7.5 - if dx * dx + dy * dy <= 16: + """显卡(32×32,2×2 格),略微俯视:顶面是带两个风扇的散热罩,左端金属挡板(带接口), + 前沿露出 PCB 和金手指,右端一道暗侧面,顶上一排供电口。每格中心都不透明。""" + c = Canvas(32, 32) + TOP0, TOP1 = 4, 22 # 顶面行范围 + L, R = 6, 29 # 顶面左右(左边是挡板) + # 金属 I/O 挡板(左端竖条,带两个接口孔) + c.rect(1, 2, 5, 27, 'b') + c.rect(1, 2, 1, 27, 'h'); c.rect(5, 2, 5, 27, 'd') + for y in (6, 12): # 两个视频接口(深色矩形孔,带高光边) + c.rect(2, y, 4, y + 3, 'k') + c.rect(2, y, 4, y, 'g') + c.put(3, 20, 'k'); c.put(3, 22, 'k'); c.put(3, 24, 'k') # 下部散热孔 + # 散热罩顶面 + for y in range(TOP0, TOP1 + 1): + for x in range(L, R + 1): + c.put(x, y, 'm') + c.put(R, y, 'd'); c.put(R - 1, y, 'd') + for x in range(L, R + 1): + c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b') + # 品牌色条(沿罩顶前沿) + for x in range(L + 1, R - 1): + c.put(x, TOP1 - 1, accent) + c.put(L + 1, TOP1, accent_dark); c.put(R - 2, TOP1, accent_dark) + # 两个风扇(圆形,叶片用十字 + 斜线) + for cx in (11, 22): + cy = 13 + for y in range(cy - 6, cy + 7): + for x in range(cx - 6, cx + 7): + d2 = (x - cx) ** 2 + (y - cy) ** 2 + if d2 <= 36: c.put(x, y, 'k') - if 6 <= dx * dx + dy * dy <= 16: + if 20 <= d2 <= 30: c.put(x, y, 'g') - c.put(cx, 7, 'b'); c.put(cx, 8, 'b') - c.rect(2, 4, 29, 4, accent) # 品牌色条 - c.put(2, 5, accent_dark); c.put(29, 5, accent_dark) - c.rect(6, 13, 25, 13, 'Y') # 金手指 - for x in range(6, 26, 2): - c.put(x, 14, 'Y') - c.rect(30, 3, 30, 11, 'k') # 挡板 + for k in range(-4, 5): + if abs(k) >= 1: + c.put(cx + k, cy, 'g'); c.put(cx, cy + k, 'g') + c.put(cx - 2, cy - 2, 'g'); c.put(cx + 2, cy + 2, 'g'); c.put(cx + 2, cy - 2, 'g'); c.put(cx - 2, cy + 2, 'g') + c.rect(cx - 1, cy - 1, cx + 1, cy + 1, 'b') + c.put(cx, cy, 'h') + # 前沿:PCB 绿色薄边 + 金手指 + for x in range(L, R + 1): + c.put(x, TOP1 + 1, 'g2') + c.put(x, TOP1 + 2, 'g2') + for x in range(L, R + 1): # 金手指:连续金色带,中间一道缺口(PCIe 键位),带深色触点线 + c.put(x, TOP1 + 3, 'Y'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'e') + for y in (TOP1 + 3, TOP1 + 4): + c.put(L + 13, y, 'k'); c.put(L + 14, y, 'k') + # 顶上一排供电口(两个黑色插口) + for x0 in (L + 3, L + 9): + c.rect(x0, TOP0 - 1, x0 + 4, TOP0 - 1, 'k') + # 保证每格中心不透明:下半部(第二行格子,y 16..31 的中心 y=24)落在金手指/PCB 上 + for x in range(L, R + 1): + for y in range(TOP1 + 1, 28): + if c.px[y][x] is None: + c.put(x, y, 'k') if burnt: - for x, y in [(7, 5), (8, 6), (10, 9), (19, 6), (20, 7), (22, 9), (14, 8), (15, 7), (24, 5)]: + for x, y in [(9, 8), (10, 9), (13, 17), (24, 8), (25, 9), (27, 18), (17, 12), (18, 11)]: c.put(x, y, 'c') - c.rect(2, 4, 29, 4, 'e') - for x, y in [(12, 3), (13, 3), (17, 11), (26, 10)]: + for x in range(L + 1, R - 1): + c.put(x, TOP1 - 1, 'e') + for x, y in [(15, 5), (16, 5), (20, 21), (26, 14)]: c.put(x, y, 'c') + # 轮廓 + solid = lambda v: v is not None + get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None + edge = [(x, y) for y in range(c.h) for x in range(c.w) + if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))] + for x, y in edge: + c.put(x, y, 'O') return c @@ -185,20 +228,58 @@ def rig(): def terminal(): - """交易终端(32×32,2×2 格):小型显示器 + 键盘,屏幕上是 K 线。""" + """交易终端(32×32,2×2 格),略微俯视:显示器是一个带顶面的盒子(正面是屏幕和 K 线), + 下面是键盘的顶面(梯形,露出按键),左亮右暗。每格中心都不透明。""" c = Canvas(32, 32) - c.box(3, 2, 28, 21, 'm') - c.rect(4, 3, 27, 3, 'l') - c.box(5, 4, 26, 18, 's') - candles = [(8, 12, 15), (11, 10, 14), (14, 11, 13), (17, 7, 12), (20, 8, 11), (23, 5, 10)] - for x, top, bot in candles: - c.rect(x, top, x, bot, 'S') - c.put(x, top - 1, 'T') - c.rect(3, 22, 28, 23, 'd') # 支架(拉通,保证每格中心点不透明) - c.box(2, 24, 29, 30, 'b') # 键盘 - for y in (26, 28): - for x in range(4, 28, 2): + # 显示器:顶面(浅色窄条,略收窄)+ 正面 + for v in range(4): + y = 3 + v + x0, x1 = 4 + (3 - v) // 3, 27 - (3 - v) // 3 + for x in range(x0, x1 + 1): + c.put(x, y, 'l' if v else 'h') + c.put(x1, y, 'd') + for y in range(7, 21): + for x in range(4, 28): c.put(x, y, 'm') + c.put(4, y, 'b'); c.put(27, y, 'd'); c.put(26, y, 'd') + # 屏幕(深青底、K 线) + for y in range(9, 19): + for x in range(7, 25): + c.put(x, y, 's') + candles = [(9, 13, 16), (12, 11, 15), (15, 12, 14), (18, 9, 13), (21, 10, 12), (23, 9, 11)] + for x, top, bot in candles: + for y in range(top, bot + 1): + c.put(x, y, 'S') + c.put(x, top - 1 if top > 9 else top, 'T') + c.put(7, 9, 'T'); c.put(8, 9, 'T') + # 支架 + 底座(拉通到键盘,保证 y=24 的格子中心不透明) + c.rect(13, 21, 18, 22, 'd') + # 键盘顶面:梯形(后窄前宽),按键网格 + for v in range(7): + y = 24 + (6 - v) # v=0 是前沿(y=30) + w = v // 3 + x0, x1 = 2 + w, 29 - w + for x in range(x0, x1 + 1): + c.put(x, y, 'b') + c.put(x0, y, 'h'); c.put(x1, y, 'd') + for y in (25, 27, 29): + for x in range(5, 27, 2): + c.put(x, y, 'm') + c.rect(10, 29, 21, 29, 'm') # 空格键 + # 键盘前沿(正面薄边) + for x in range(2, 30): + c.put(x, 31, 'd') + # 键盘与显示器之间的缝隙用桌面阴影色填实,避免 y=24 行出现透明格 + for y in range(22, 24): + for x in range(4, 28): + if c.px[y][x] is None: + c.put(x, y, 'k') + solid = lambda v: v is not None + get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None + edge = [(x, y) for y in range(c.h) for x in range(c.w) + if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))] + for x, y in edge: + c.put(x, y, 'O') return c diff --git a/src/Decentralized.Domain/RigSlots.cs b/src/Decentralized.Domain/RigSlots.cs new file mode 100644 index 0000000..8f7a9a2 --- /dev/null +++ b/src/Decentralized.Domain/RigSlots.cs @@ -0,0 +1,58 @@ +using System; + +namespace Decentralized.Domain +{ + /// + /// 矿架的显卡槽:默认 2 个,投入电子元件逐个升级,最多 5 个。每张显卡占 2×2 格,槽在矿架窗口里是一列 2 格宽的格子, + /// 每个槽两行;未解锁的行是不可放的格子。游戏里电子元件的价值是 33 / 350 / 2,500(普通 / 稀有 / 超科技)。 + /// + public static class RigSlots + { + public const int Base = 2, Max = 5; + public const int CellWidth = 2, CellsPerSlot = 2; + + /// 升级到第 n 个槽需要的电子元件(物品 id,件数)。n 从 3 开始。 + public static (string Item, int Count) Cost(int slot) + { + switch (slot) + { + case 3: return ("common_electronic", 4); + case 4: return ("rare_electronic", 1); + case 5: return ("rare_electronic", 2); + default: throw new ArgumentOutOfRangeException(nameof(slot)); + } + } + + /// 当前槽数下,下一次升级需要什么;已满时返回 null。 + public static (string Item, int Count)? Next(int slots) => slots >= Max ? ((string, int)?)null : Cost(slots + 1); + + /// + /// 投入 count 件 item:只收下一级需要的那种;累计到够用就解锁一个槽,多出的留作下一级进度(若下一级是同一种元件)。 + /// 返回实际收下的件数;其余退回(不收)。 + /// + public static int Insert(ref int slots, ref int progress, string item, int count) + { + if (slots >= Max || count <= 0) return 0; + var need = Cost(slots + 1); + if (item != need.Item) return 0; + int take = Math.Min(count, need.Count - progress); + progress += take; + if (progress >= need.Count) + { + slots++; + progress = 0; + } + return take; + } + + /// 显卡格子的形状字符串:宽 2,高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。 + public static string Shape(int slots) + { + var sb = new System.Text.StringBuilder(); + for (int row = 0; row < Max * CellsPerSlot; row++) + for (int col = 0; col < CellWidth; col++) + sb.Append(row < slots * CellsPerSlot ? '0' : '1'); + return sb.ToString(); + } + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 0e52271..a6cc4eb 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -15,7 +15,7 @@ namespace Decentralized.Domain public const double NpcTargetRatio = 3.5; public const double NpcAdj = 0.05; public const double PoolFee = 0.03; - public const int RigPrice = 400, RigKj = 2, RigSlots = 4; + public const int RigPrice = 400, RigKj = 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; diff --git a/src/Decentralized/Items.cs b/src/Decentralized/Items.cs index ea13fbf..2c7888a 100644 --- a/src/Decentralized/Items.cs +++ b/src/Decentralized/Items.cs @@ -117,15 +117,22 @@ namespace Decentralized // ---------- 矿架 ---------- + public const string SlotsTag = "PD_SLOTS", ProgressTag = "PD_SLOT_PROGRESS"; + + private static int SlotsOf(GameItem rig) => rig.IsTag(SlotsTag) ? rig.GetTagReadonly(SlotsTag).GetInt() : RigSlots.Base; + private static GameItem CreateRig() { var battery = new GameSlotInventory(); + var upgrade = new GameSlotInventory(); var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7); - var cards = new GameGridInventory(2, 4); + // 显卡每张 2×2 格:一列 2 格宽的格子,每个槽两行;未解锁的行不可放 + var cards = new GameGridInventory(RigSlots.Shape(RigSlots.Base), RigSlots.CellWidth); 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")); + MachineHelper.Panel(L.T("rig.label.cards"), cards, "cards"), + MachineHelper.Panel(L.T("rig.label.upgrade"), upgrade, "upgrade")); var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window); rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig"); rig.name = L.T("item.rig.name"); @@ -139,13 +146,48 @@ namespace Decentralized cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag); cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards); cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards); - rig.onLoaded = _ => UpdateRig(rig, modules, cards); + // 升级槽:只收下一级需要的电子元件,放进去立刻投入(计入进度),攒够解锁一个显卡槽 + upgrade.mayInventoryAddItemFunc = (item, _) => + { + var need = RigSlots.Next(SlotsOf(rig)); + return GeneralHelper.IsItemOwned(item) && need != null && item.identifier == need.Value.Item; + }; + upgrade.onSlotAddItemEndFunc = (item, _, _) => Log.Guard("矿架升级槽位", () => TakeUpgrade(rig, upgrade, cards, modules, item)); + rig.onLoaded = _ => { ApplySlots(rig, cards); UpdateRig(rig, modules, cards); }; rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, modules, cards)); ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name); + ApplySlots(rig, cards); UpdateRig(rig, modules, cards); return rig; } + /// 按标签里的槽数设置显卡格子的形状(游戏自己扩展模块槽也是 SetShape 加 Validate)。 + private static void ApplySlots(GameItem rig, GameGridInventory cards) + { + cards.SetShape(RigSlots.Shape(SlotsOf(rig)), RigSlots.CellWidth); + rig.contentWindow?.Validate(); + } + + private static void TakeUpgrade(GameItem rig, GameSlotInventory upgradeSlot, GameGridInventory cards, GameInventory modules, GameItem item) + { + int slots = SlotsOf(rig); + int progress = rig.IsTag(ProgressTag) ? rig.GetTagReadonly(ProgressTag).GetInt() : 0; + int before = slots; + int took = RigSlots.Insert(ref slots, ref progress, item.identifier, item.unitCount); + if (took <= 0) return; + rig.ModifyTag(SlotsTag, s => s.Enable().SetInt(slots)); + rig.ModifyTag(ProgressTag, s => s.Enable().SetInt(progress)); + // 消耗收下的件数;放不下的部分留在升级槽里(玩家取回) + if (took >= item.unitCount) { upgradeSlot.Expel(item); item.Destroy(); } + else item.GetSplitItem(took).Destroy(); + if (slots != before) + { + ApplySlots(rig, cards); + EmporiumAudioPlayer.Instance?.ratchet?.Play(); + } + UpdateRig(rig, (GameGridInventory)modules, cards); + } + public static List Cards(GameInventory cards) => cards.childItems.Where(IsGpu).ToList(); /// 按模块槽里的模块重新汇总三项百分比(游戏自己的公式,含相邻效果),换算成本 mod 的模块效果。 @@ -167,7 +209,14 @@ namespace Decentralized 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); + int slots = SlotsOf(rig); + rig.shortDescription = L.T("item.rig.desc", slots, Fmt.Num(hash * 10), kj); + var next = RigSlots.Next(slots); + if (next != null) + { + int progress = rig.IsTag(ProgressTag) ? rig.GetTagReadonly(ProgressTag).GetInt() : 0; + rig.shortDescription += " " + L.T("item.rig.next", slots + 1, L.T("mat." + next.Value.Item), progress, next.Value.Count); + } 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)); } diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 559cdd2..32d824d 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -5,9 +5,13 @@ upgrade.desc,Members receive a trading terminal and an RZX 50 graphics card. The item.terminal.name,Trading Terminal item.terminal.desc,Wallet, the Poinbase 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." +item.rig.desc,"Holds up to {0} graphics cards. Hashrate {1} MH/s. Draws {2} kJ per day." rig.label.modules,Modules item.rig.modules,"Modules: hashrate {0}, power draw {1}, wear {2}." +rig.label.upgrade,Upgrade slot +item.rig.next,"Slot {0}: put in {1} ({2}/{3})." +mat.common_electronic,Common electronics +mat.rare_electronic,Rare electronics 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 3798c85..a9c8ca1 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -4,9 +4,13 @@ upgrade.desc,成员可获得一台交易终端和一张 RZX 50 显卡。终端 item.terminal.name,交易终端 item.terminal.desc,钱包、Poinbase 交易所与夜间挖矿报告。 item.rig.name,矿架 -item.rig.desc,可装 4 张显卡。算力 {0} MH/s。耗电 {1} kJ/天。 +item.rig.desc,最多装 {0} 张显卡。算力 {1} MH/s。耗电 {2} kJ/天。 rig.label.modules,模块 item.rig.modules,模块:算力 {0},耗电 {1},损耗 {2}。 +rig.label.upgrade,升级槽位 +item.rig.next,第 {0} 槽:投入{1}({2}/{3})。 +mat.common_electronic,普通电子元件 +mat.rare_electronic,稀有电子元件 rig.label.cards,显卡 item.gpu.name,{0} {1} item.gpu.desc,算力 {0} MH/s。耗电 {1} kJ/天。健康度 {2}%。 diff --git a/src/Decentralized/ModFiles/Textures/Items/gpu_burnt.png b/src/Decentralized/ModFiles/Textures/Items/gpu_burnt.png index c50a4e6..aeff9bf 100644 Binary files a/src/Decentralized/ModFiles/Textures/Items/gpu_burnt.png and b/src/Decentralized/ModFiles/Textures/Items/gpu_burnt.png differ diff --git a/src/Decentralized/ModFiles/Textures/Items/gpu_pmd.png b/src/Decentralized/ModFiles/Textures/Items/gpu_pmd.png index 9eaf842..7d340b4 100644 Binary files a/src/Decentralized/ModFiles/Textures/Items/gpu_pmd.png and b/src/Decentralized/ModFiles/Textures/Items/gpu_pmd.png differ diff --git a/src/Decentralized/ModFiles/Textures/Items/gpu_probidia.png b/src/Decentralized/ModFiles/Textures/Items/gpu_probidia.png index ba456e0..247a3fc 100644 Binary files a/src/Decentralized/ModFiles/Textures/Items/gpu_probidia.png and b/src/Decentralized/ModFiles/Textures/Items/gpu_probidia.png differ diff --git a/src/Decentralized/ModFiles/Textures/Items/terminal.png b/src/Decentralized/ModFiles/Textures/Items/terminal.png index b4db295..8a9bc6e 100644 Binary files a/src/Decentralized/ModFiles/Textures/Items/terminal.png and b/src/Decentralized/ModFiles/Textures/Items/terminal.png differ diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 61cb889..84555a2 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -128,6 +128,20 @@ internal static class Program Check(r.Refurbish() && r.Health == 80 && !r.Refurbish(), "翻新一次 +30,不能再翻新"); }); + Test("矿架槽位:默认 2 个,投入电子元件逐个升级到最多 5 个;只收下一级需要的;形状随槽数变化", () => + { + int slots = RigSlots.Base, prog = 0; + Check(slots == 2 && RigSlots.Next(2) == ("common_electronic", 4), "默认 2 槽,升到第 3 槽要 4 个普通电子元件"); + Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0 && slots == 2 && prog == 0, "第 3 槽不收稀有元件"); + Check(RigSlots.Insert(ref slots, ref prog, "common_electronic", 3) == 3 && slots == 2 && prog == 3, "先投 3 个:进度 3/4"); + Check(RigSlots.Insert(ref slots, ref prog, "common_electronic", 3) == 1 && slots == 3 && prog == 0, "再投 3 个只收 1 个,解锁第 3 槽,多的 2 个退回"); + Check(RigSlots.Next(3) == ("rare_electronic", 1) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 5) == 1 && slots == 4, "第 4 槽:1 个稀有元件"); + Check(RigSlots.Next(4) == ("rare_electronic", 2) && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 4 && prog == 1, "第 5 槽:2 个稀有元件,先投 1 个进度 1/2"); + Check(RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 1 && slots == 5, "投满解锁第 5 槽"); + Check(RigSlots.Next(5) == null && RigSlots.Insert(ref slots, ref prog, "rare_electronic", 1) == 0, "满 5 槽后不再收"); + Check(RigSlots.Shape(2) == "0000" + "0000" + new string('1', 12) && RigSlots.Shape(5) == new string('0', 20) && RigSlots.Shape(3).Length == 20, "形状:宽 2、共 10 行,前 2 × 槽数行可放"); + }); + Test("模块:折算系数 4,算力 +20%、省电 25%、损耗 −30% 封顶;性能加成提高损耗与暴毙", () => { var perf = ModuleBonus.From(16, 0, 0);