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);