using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Decentralized.Domain; using UnityEngine; namespace Decentralized { /// /// mod 物品:交易终端、矿架、显卡(每个型号一个物品 id)。显卡的健康度、假卡、翻新、烧毁存在物品标签上,随存档保存。 /// 读档时游戏按 id 重新调用工厂,再把存档里的标签与子物品(矿架里的显卡、电池)恢复回去。 /// public static class Items { public const string Terminal = "pd_terminal"; public const string Rig = "pd_rig"; public const string GpuPrefix = "pd_gpu_"; public const string HealthTag = "PD_HEALTH"; public const string FakeTag = "PD_FAKE_OF"; public const string RefurbTag = "PD_REFURBISHED"; public const string DeadTag = "PD_DEAD"; public static Action OpenTerminal; public static void Subscribe() => ModHook.OnModItemDirectoryInit += Register; public static void Unsubscribe() => ModHook.OnModItemDirectoryInit -= Register; public static string GpuId(string model) => GpuPrefix + model.ToLowerInvariant(); public static bool IsGpu(GameItem item) => item != null && item.identifier != null && item.identifier.StartsWith(GpuPrefix); private static void Register(ModItemDirectory dir) { dir.Add(Terminal, CreateTerminal); dir.Add(Rig, CreateRig); foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic)) { var s = spec; dir.Add(GpuId(s.Model), () => CreateGpu(s)); } } // ---------- 交易终端 ---------- private static GameItem CreateTerminal() { var item = ItemDirectory.CreateEmptyItem(Terminal); item.SetSpriteAndShapeFromMod(ModInfo.Id, "terminal"); item.name = L.T("item.terminal.name"); item.shortDescription = L.T("item.terminal.desc"); item.unitValue = 0; item.EnableTag(GeneralHelper.IMPORTANT_TAG); item.EnableTag(TypeHelper.NOT_FOR_RESALE); item.mayActivateSlotItemFunc = (_, _, _) => true; item.canActivateSlotItemFunc = (_, _, _) => true; item.onActivateSlotItemFunc = (_, _, _) => Log.Guard("打开交易终端", () => OpenTerminal?.Invoke()); ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name); return item; } // ---------- 显卡 ---------- /// RZX70 → "RZX 70",RZX80Ti → "RZX 80 Ti",RZ6800 → "RZ 6800"。 public static string DisplayModel(string model) => Regex.Replace(Regex.Replace(model, @"(?<=[A-Za-z])(?=\d)", " "), @"(?<=\d)(?=[A-Z])", " "); private static GameItem CreateGpu(HardwareSpec spec) { var item = ItemDirectory.CreateEmptyItem(GpuId(spec.Model)); item.SetSpriteAndShapeFromMod(ModInfo.Id, spec.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd"); item.SetGameItemType("ELECTRONIC"); item.onLoaded = Refresh; ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name); Refresh(item); return item; } public static Unit ToUnit(GameItem item) { string model = item.identifier.Substring(GpuPrefix.Length); model = Hardware.Specs.Keys.First(k => k.ToLowerInvariant() == model); var u = new Unit { Model = model, Health = 100 }; if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble(); if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString(); u.Refurbished = item.IsTag(RefurbTag); u.Dead = item.IsTag(DeadTag); return u; } public static void Store(GameItem item, Unit u) { item.ModifyTag(HealthTag, s => s.Enable().SetDouble(u.Health)); if (u.FakeOf != null) item.ModifyTag(FakeTag, s => s.Enable().SetString(u.FakeOf)); if (u.Refurbished) item.EnableTag(RefurbTag); if (u.Dead) item.EnableTag(DeadTag); Refresh(item); } /// 按标签更新名字、说明、价值和图:显示的是标签上的型号(假卡也显示被冒充的型号)。 public static void Refresh(GameItem item) { var u = ToUnit(item); var label = u.Label; if (u.Dead) { item.name = L.T("item.gpu.burnt.name", label.Brand, DisplayModel(label.Model)); item.shortDescription = L.T("item.gpu.burnt.desc"); item.unitValue = Math.Max(1, label.Price / 20); item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_burnt"); return; } item.name = L.T("item.gpu.name", label.Brand, DisplayModel(label.Model)); item.shortDescription = L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), Mathf.RoundToInt((float)u.Health)); item.unitValue = Math.Max(1, (long)Math.Round(label.Price * u.Health / 100)); } // ---------- 矿架 ---------- 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); // 显卡每张 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.upgrade"), upgrade, "upgrade")); var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window); rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig"); rig.name = L.T("item.rig.name"); rig.unitValue = Tuning.RigPrice; MachineHelper.SetupBatterySlot(battery, rig); 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, modules, cards); cards.onSlotRemoveItemEndFunc = (_, _, _) => 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 的模块效果。 public static ModuleBonus Bonus(GameItem rig, GameInventory modules) { 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 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); // 基础耗电 = 机身 + 所有显卡(不含模块)。游戏装卸模块时用这个基础值重算耗电,所以它必须随显卡变化, // 否则游戏会把耗电改回"只有机身的 2 kJ"再按百分比折算。 int baseKj = RigKj(cards, ModuleBonus.None); rig.ModifyTag("MACHINERY_DRAWN_POWER_BASE", s => s.Enable().SetInt(baseKj)); rig.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(kj)); double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(b)); 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)); } 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; 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; foreach (var c in list) Session.TonightCards.Add(new KeyValuePair(c, b)); UpdateRig(rig, modules, cards); } /// /// 放进储藏区(游戏 EmporiumTryAdd 的默认目标,放不下时游戏自己找位置)。 /// 不放柜台:测试中柜台上的物品过夜后会消失(未在正常流程中确认);ModHelper.MoveItemTo* 放满时会静默失败。 /// public static void Give(GameItem item) { GeneralHelper.SetItemOwned(item, true); GraphUtils.EmporiumTryAdd(item); } } }