Files
probably-decentralized/src/Decentralized/Items.cs
T

595 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Decentralized.Domain;
using UnityEngine;
namespace Decentralized
{
/// <summary>
/// mod 物品:交易终端、矿架、显卡(每个型号一个物品 id)。显卡的健康度、假卡、翻新、烧毁存在物品标签上,随存档保存。
/// 读档时游戏按 id 重新调用工厂,再把存档里的标签与子物品(矿架里的显卡、电池)恢复回去。
/// </summary>
public static class Items
{
public const string Terminal = "pd_terminal";
public const string Rig = "pd_rig";
public const string Guide = "pd_gpu_guide";
public const string Web3Guide = "pd_web3_guide";
public const string GpuPrefix = "pd_gpu_";
public const string HwWallet = "pd_hw_wallet";
public const string KeyDrive = "pd_key_drive";
/// <summary>硬件钱包被动过手脚(供应链投毒)。外观、说明都不显示。</summary>
public const string PoisonTag = "PD_POISONED";
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 const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", ClaimTag = "PD_CLAIM", ExposedTag = "PD_EXPOSED", MarkupTag = "PD_MARKUP", EditedTag = "PD_EDITED";
public static Action OpenTerminal;
public static Action OpenHandbook, OpenGpuHandbook;
/// <summary>硬件钱包拖到交易终端上:添加一个钱包(参数:是否被投毒)。</summary>
public static Action<bool> AddWallet;
public static Func<bool> CanAddWallet;
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);
dir.Add(Guide, CreateGuide);
dir.Add(Web3Guide, CreateWeb3Guide);
dir.Add(HwWallet, CreateHwWallet);
dir.Add(KeyDrive, CreateKeyDrive);
AsicItems.Register(dir);
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap))
{
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;
}
/// <summary>显卡鉴定手册:档次对照、序列号规则、外观折扣。和游戏里的鉴定手册一样,是一件物品,说明里就是内容。</summary>
private static GameItem CreateGuide()
{
var item = ItemDirectory.CreateEmptyItem(Guide);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_guide");
item.name = L.T("item.guide.name");
item.shortDescription = L.T("item.guide.desc");
item.mayActivateSlotItemFunc = (_, _, _) => true;
item.canActivateSlotItemFunc = (_, _, _) => true;
item.onActivateSlotItemFunc = (_, _, _) => Log.Guard("打开鉴定手册", () => OpenGpuHandbook?.Invoke());
item.unitValue = 0;
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
/// <summary>Web3 手册:银行、交易、混币器、黑市与 Ponero 的规则。界面上不放说明,规则都在这里。</summary>
private static GameItem CreateWeb3Guide()
{
var item = ItemDirectory.CreateEmptyItem(Web3Guide);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_guide");
item.name = L.T("item.web3guide.name");
item.shortDescription = L.T("item.web3guide.desc");
item.mayActivateSlotItemFunc = (_, _, _) => true;
item.canActivateSlotItemFunc = (_, _, _) => true;
item.onActivateSlotItemFunc = (_, _, _) => Log.Guard("打开手册", () => OpenHandbook?.Invoke());
item.unitValue = 0;
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
/// <summary>
/// PLEDGER 硬件钱包:和游戏里的升级材料一样,拖到交易终端上用掉,钱包 +1。杰克逊博士卖的是正品;
/// Web3 商城卖的便宜一些,外观和说明完全一样,其中一部分被动过手脚(<see cref="PoisonTag"/>)。
/// </summary>
private static GameItem CreateHwWallet()
{
var item = ItemDirectory.CreateEmptyItem(HwWallet);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "hw_wallet");
item.name = L.T("item.hwwallet.name");
item.shortDescription = L.T("item.hwwallet.desc");
item.unitValue = Tuning.WalletJacksonPrice;
item.SetGameItemType("MATERIAL");
item.maySelectSlotItemFunc = (_, _, _) => true;
item.canSelectSlotItemFunc = (_, _, _) => true;
item.mayThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(item.mayThisTargetItemFunc, (Func<GameItem, GameItem, bool>)((s, t) => CanTarget(t)));
item.canThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(item.canThisTargetItemFunc, (Func<GameItem, GameItem, bool>)((s, t) => CanTarget(t)));
item.onThisTargetItemFunc = (Action<GameItem, GameItem>)Delegate.Combine(item.onThisTargetItemFunc, (Action<GameItem, GameItem>)((s, t) => Log.Guard("添加硬件钱包", () =>
{
if (!CanTarget(t)) return;
bool poisoned = s.IsTag(PoisonTag);
s.Destroy();
AddWallet?.Invoke(poisoned);
})));
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
/// <summary>Tumbler 的密钥盘:违禁品(高),搜查时被翻出来按价值记罪证。</summary>
private static GameItem CreateKeyDrive()
{
var item = ItemDirectory.CreateEmptyItem(KeyDrive);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "key_drive");
item.name = L.T("item.keydrive.name");
item.shortDescription = L.T("item.keydrive.desc");
item.unitValue = Tuning.TumblerDriveValue;
ContrabandHelper.InitContrabandItem(item, 3);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
private static bool CanTarget(GameItem t) => t != null && t.identifier == Terminal && (CanAddWallet?.Invoke() ?? false);
public static GameItem NewHwWallet(bool poisoned)
{
var item = ModHelper.SpawnItem(HwWallet);
if (item != null && poisoned) item.EnableTag(PoisonTag);
return item;
}
// ---------- 显卡 ----------
/// <summary>RTX70 → "RTX 70",RX800 → "RX 800"。</summary>
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.SetGameItemType("MATERIAL");
InspectableHelper.InitInspectableItem(item);
item.onLoaded = Refresh;
// 右键"切换"翻面;外观差异用叠加图画在同一张底图上
item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG);
item.mayToggleSlotItemFunc = (_, _, _) => true;
item.canToggleSlotItemFunc = (_, _, _) => true;
item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () =>
{
Flip(toggled);
});
item.getItemSpriteModifierForItemFunc = Overlays;
// 摆给顾客的时候,顾客可能看出说法与实物对不上
item.onSlotAddItemFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Combine(item.onSlotAddItemFunc,
(Action<GameItem, GameInventory, SlotMarker>)((it, _, __) => Log.Guard("顾客验货", () => OnOffered(it))));
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
Refresh(item);
return item;
}
/// <summary>按给定的状态造一张卡(二手卡、假卡):写入标签并生成外观。</summary>
public static GameItem NewCard(string model, double health, string fakeOf, Rng rng, DealerKind? seller = null, int markupPct = 0)
{
var item = ModHelper.SpawnItem(GpuId(model));
var u = new Unit { Model = model, FakeOf = fakeOf, Health = health, Look = CardLook.Generate(rng, health, fakeOf != null, Hardware.Specs[model].Tier, fakeOf != null ? Hardware.Specs[fakeOf].Tier : Hardware.Specs[model].Tier) };
if (health >= 100 && fakeOf == null) u.Look.Sticker = 0; // 全新卡:封条完好
Store(item, u);
if (seller != null)
{
GeneralHelper.SetItemOwned(item, false); // 卖家的货:买下之前不属于玩家,说法才有效
SetClaim(item, CardLabels.DealerClaim(u, seller.Value, rng), 0, markupPct);
}
return item;
}
// ---------- 标签栏:卖家的说法与指出 ----------
private static object checkedClient;
private static readonly HashSet<int> checkedItems = new HashSet<int>();
/// <summary>被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。</summary>
public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(Tuning.RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
/// <summary>
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来(型号档次不符的必看出来,供电口和风扇数一眼可见);看出来就拆穿这一项(价格回到真实情况),
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
/// </summary>
public static void OfferForTest(GameItem item) { checkedItems.Clear(); OnOffered(item, true); }
private static void OnOffered(GameItem item, bool force = false)
{
if (item.IsTag(DeadTag) || !GeneralHelper.IsItemOwned(item) || (!force && !item.IsRootedInBackInv())) return;
var ci = PlayerStore.Instance.currentClientInstance;
var client = ci?.GetClientBlueprint();
if (client == null || (client.clientIntent != StoreClient.ClientIntent.BUY && client.clientIntent != StoreClient.ClientIntent.SELLNBUY)) return;
var claim = GetClaim(item);
if (claim == null) return;
if (!ReferenceEquals(checkedClient, ci)) { checkedClient = ci; checkedItems.Clear(); }
if (!checkedItems.Add(item.uniqueId)) return;
var u = ToUnit(item);
int exposed = Exposed(item);
int lies = CardLabels.Lies(u, claim.Value, exposed);
if (lies == 0) return;
var rng = Session.Data.Rng;
int found = 0;
foreach (int r in CardLabels.Rows)
if ((lies & r) != 0 && rng.Chance((r == CardLabels.RowCount || r == CardLabels.RowPin) ? Tuning.CustomerSpotTierMismatch : Tuning.CustomerSpotChance))
{
found |= r;
if (r != CardLabels.RowSpec && CardLabels.IsEvidence(r, claim.Value, Claim.Truth(u))) found |= CardLabels.RowSpec | CardLabels.RowCount | CardLabels.RowPin | CardLabels.RowPrint | CardLabels.RowSerial;
}
if (found == 0) return;
long before = item.unitValue;
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed | found));
Refresh(item);
int rep = RepLoss(before - item.unitValue);
string faction = string.IsNullOrEmpty(client.clientFaction) || StoreReputation.GetStoreReputation(client.clientFaction) == null ? "FACTION_LOWER_LEVEL" : client.clientFaction;
StoreReputation.ModReputation(faction, -rep);
EmporiumAudioPlayer.Instance?.audioFail?.Play();
Log.Info($"顾客看出标签不实:{found},价格 {item.unitValue},信誉 −{rep}");
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T((found & CardLabels.RowSpec) != 0 ? "cust.fake." + rng.Range(1, 2) : "cust.worn." + rng.Range(1, 2))));
}
/// <summary>标签上的说法;没有说法的卡(自己挖出来、正规渠道的新卡)一律按真实情况算。</summary>
public static Claim? GetClaim(GameItem item)
{
if (!item.IsTag(ClaimTag)) return null;
var c = Claim.Decode(item.GetTagReadonly(ClaimTag).GetString());
return string.IsNullOrEmpty(c.Model) ? (Claim?)null : c;
}
/// <summary>已指出的标签。型号一旦被拆穿,四个真伪行(型号、供电与风扇、序列号、丝印)一起亮出真实情况。</summary>
public static int Exposed(GameItem item)
{
int e = item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
if ((e & CardLabels.RowSpec) != 0) e |= CardLabels.RowSpec | CardLabels.RowCount | CardLabels.RowPin | CardLabels.RowSerial | CardLabels.RowPrint;
return e;
}
public static void SetClaim(GameItem item, Claim claim, int exposed, int markupPct = 0)
{
item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (markupPct != 0) item.ModifyTag(MarkupTag, t => t.Enable().SetInt(markupPct));
Refresh(item);
}
/// <summary>现在标签上写的型号:说法里的型号,被拆穿之后是真实芯片。</summary>
public static string BelievedModel(GameItem item, Unit u)
{
var c = GetClaim(item);
if (c == null) return u.Model;
return (Exposed(item) & CardLabels.RowSpec) != 0 ? (u.FakeOf ?? u.Model) : c.Value.Model;
}
/// <summary>玩家指出一个标签:记下来,说法改回真实情况;返回 0 说法属实、1 隐瞒了不利的情况、2 拆穿假货。</summary>
public static int Point(GameItem item, int row)
{
var u = ToUnit(item);
var claim = GetClaim(item) ?? Claim.Truth(u);
int exposed = Exposed(item);
int kind = CardLabels.Point(u, claim, row, ref exposed);
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (!item.IsTag(ClaimTag)) item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
Refresh(item);
if (item.IsTag("APPRAISAL_ITEM")) AppraisalHelper.SetDefaultAppraisalValue(item, (int)item.unitValue);
return kind;
}
/// <summary>用标签机改说法(随便改):这一项重新算作"没人指出过"。</summary>
public static void EditClaim(GameItem item, int row, int value, string model = null)
{
var u = ToUnit(item);
var claim = GetClaim(item) ?? Claim.Truth(u);
claim = row == CardLabels.RowSpec ? new Claim { Model = model ?? claim.Model, Fan = claim.Fan, Fingers = claim.Fingers, Bracket = claim.Bracket, Print = claim.Print, Sticker = claim.Sticker } : claim.With(row, value);
int exposed = Exposed(item) & ~row;
item.EnableTag(EditedTag);
SetClaim(item, claim, exposed);
}
/// <summary>说明:算力和耗电按标称写;成色在归玩家之前只给粗略等级(要看外观自己判断),归玩家之后给精确健康度。</summary>
private static string Desc(GameItem item, Unit u)
{
var label = Hardware.Specs[BelievedModel(item, u)];
string cond = GeneralHelper.IsItemOwned(item)
? Mathf.RoundToInt((float)u.Health) + "%"
: L.T(u.Health >= 85 ? "item.gpu.grade.1" : u.Health >= 60 ? "item.gpu.grade.2" : u.Health >= 35 ? "item.gpu.grade.3" : "item.gpu.grade.4");
return L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), cond);
}
private static void SyncValue(GameItem item)
{
if (item.IsTag(DeadTag)) return;
// 买下之后卖家的说法作废:卡回到真实情况,除非玩家用标签机自己改过
if (GeneralHelper.IsItemOwned(item) && item.IsTag(ClaimTag) && !item.IsTag(EditedTag))
{
item.ModifyTag(ClaimTag, t => t.Disable());
item.ModifyTag(ExposedTag, t => t.Disable());
item.ModifyTag(MarkupTag, t => t.Disable());
}
var u = ToUnit(item);
string desc = Desc(item, u);
if (item.shortDescription != desc) item.shortDescription = desc;
string name = L.T("item.gpu.name", Hardware.Specs[BelievedModel(item, u)].Brand, DisplayModel(BelievedModel(item, u)));
if (item.name != name) item.name = name;
long value = CardLabels.Value(u, GetClaim(item), Exposed(item));
if (!GeneralHelper.IsItemOwned(item) && item.IsTag(MarkupTag)) value = Math.Max(1, (long)Math.Round(value * (1 + item.GetTagReadonly(MarkupTag).GetInt() / 100.0)));
if (item.unitValue != value) item.unitValue = value;
}
public static Unit ToUnit(GameItem item)
{
bool asic = AsicItems.Is(item);
string model = item.identifier.Substring(asic ? AsicItems.Prefix.Length : GpuPrefix.Length);
model = Hardware.Specs.Keys.FirstOrDefault(k => k.ToLowerInvariant() == model) ?? (asic ? "M1" : "RTX60");
var u = new Unit { Model = model, Health = 100 };
if (asic)
{
// 矿机没有外观、没有真假:不记外观种子,算力、损耗、暴毙都按规格表
if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
u.Dead = item.IsTag(DeadTag);
return u;
}
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);
if (item.IsTag(LookTag)) u.Look = CardLook.Decode(item.GetTagReadonly(LookTag).GetString());
// 外观种子:没有记录的卡(正规渠道的新卡)用物品编号当种子,之后同样随损耗变旧
u.Look.Seed = item.IsTag(SeedTag) ? (uint)item.GetTagReadonly(SeedTag).GetInt() : (uint)(item.uniqueId * 2654435761u) | 1u;
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);
if (!AsicItems.Is(item))
{
item.ModifyTag(LookTag, s => s.Enable().SetString(u.Look.Encode()));
item.ModifyTag(SeedTag, s => s.Enable().SetInt((int)u.Look.Seed));
}
Refresh(item);
}
/// <summary>按标签更新名字、说明、价值和图:显示的是标签上的型号(假卡也显示被冒充的型号)。</summary>
public static void Refresh(GameItem item)
{
if (AsicItems.Is(item)) { AsicItems.Refresh(item); return; }
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, item.IsTag(BackTag) ? "gpu_burnt_b" : "gpu_burnt_f");
return;
}
item.SetSpriteAndShapeFromMod(ModInfo.Id, LookKeys(item, new List<string>()));
SyncValue(item);
}
public static bool IsBack(GameItem item) => item.IsTag(BackTag);
public static void Flip(GameItem item)
{
if (IsBack(item)) item.ModifyTag(BackTag, t => t.Disable()); else item.EnableTag(BackTag);
Refresh(item);
}
/// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
/// <summary>实物上的风扇数:由真实芯片的档次决定(低档次的板子套高档次的壳,风扇数露馅)。</summary>
public static int FansOf(Unit u) => u.ObservedCount;
public static string LookKeys(GameItem item, List<string> overlays)
{
var u = ToUnit(item);
var l = u.Appearance;
bool back = IsBack(item);
string brand = u.Label.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd";
if (u.Dead) return back ? "gpu_burnt_b" : "gpu_burnt_f";
if (!back)
{
if (l.Fan > 0) overlays.Add($"gpu_ov_fan{l.Fan}_{FansOf(u)}");
if (l.Fingers > 0) overlays.Add("gpu_ov_fin" + l.Fingers);
if (l.Bracket > 0) overlays.Add("gpu_ov_brk1");
}
else
{
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = l.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model;
overlays.Add((l.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (l.Sticker == 2) overlays.Add("gpu_ov_stk2");
}
return brand + (back ? "_b" : $"_f_p{u.ObservedPin}n{u.ObservedCount}");
}
public static Sprite ModSprite(string key)
{
var mod = ModLoader.Mods.Find(m => m.Manifest.ID.Equals(ModInfo.Id));
return mod != null && mod.Sprites.TryGetValue(key, out var sprite) ? sprite : null;
}
/// <summary>物品栏里的外观叠加图。</summary>
private static List<ItemSpriteModifier> Overlays(GameItem item)
{
var list = new List<ItemSpriteModifier>();
SyncValue(item); // 归属变化(买下)后价格随之变回真实价值
if (item.IsTag(DeadTag)) return list;
var keys = new List<string>();
LookKeys(item, keys);
int priority = 0;
foreach (var k in keys)
{
var sprite = ModSprite(k);
if (sprite != null) list.Add(new ItemSpriteModifier(sprite, Vector2.zero, 0, false, priority++, false, Color.white));
}
return list;
}
// ---------- 矿架 ----------
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);
// 显卡每张 3×2 格:3 格宽的格子,每张卡 3×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;
}
/// <summary>按标签里的槽数设置显卡格子的形状(游戏自己扩展模块槽也是 SetShape 加 Validate)。</summary>
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<GameItem> Cards(GameInventory cards) => cards.childItems.Where(IsGpu).ToList();
/// <summary>按模块槽里的模块重新汇总三项百分比(游戏自己的公式,含相邻效果),换算成本 mod 的模块效果。</summary>
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 + "%";
}
/// <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;
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<GameItem, ModuleBonus>(c, b));
UpdateRig(rig, modules, cards);
}
/// <summary>
/// 放进储藏区(游戏 EmporiumTryAdd 的默认目标,放不下时游戏自己找位置)。
/// 不放柜台:测试中柜台上的物品过夜后会消失(未在正常流程中确认);ModHelper.MoveItemTo* 放满时会静默失败。
/// </summary>
public static void Give(GameItem item)
{
GeneralHelper.SetItemOwned(item, true);
GraphUtils.EmporiumTryAdd(item);
}
}
}