feat(equipment): persist mech loadouts and recover ejection safe case loot

This commit is contained in:
2026-11-12 18:02:26 +08:00
parent 0857e291d5
commit e1d38c5a6d
23 changed files with 1371 additions and 204 deletions
@@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
namespace Ironspore
{
public sealed partial class ProfileStore
{
static bool Live(RunRecord run) => run.active && !run.destructionPending && run.armor > 0;
static List<CargoStack> Items(RunRecord r, Hold hold) => hold == Hold.Cargo ? r.cargo : r.safeCase;
static int Capacity(Hold hold) => hold == Hold.Cargo ? InventoryGrid.CargoSize : InventoryGrid.CaseSize;
static bool ValidHold(Hold hold) => hold == Hold.Cargo || hold == Hold.SafeCase;
static bool Valid(GameSave s)
{
if (s?.run == null || s.receipts == null || s.blueprints == null || s.unlocked == null ||
s.warehouse == null || s.run.cargo == null || s.run.safeCase == null || s.run.installed == null ||
s.run.searched == null || s.run.equipment?.Length != 4 || s.version != GameSave.CurrentVersion) return false;
var ids = new HashSet<string>();
var slots = new HashSet<int>();
bool Check(CargoStack i) => i != null && !string.IsNullOrEmpty(i.uid) && ids.Add(i.uid) &&
InventoryGrid.Known(i.itemId) && i.count > 0 && i.count <= (InventoryGrid.IsMaterial(i.itemId) ? 10 : 1);
foreach (var i in s.warehouse)
if (!Check(i) || i.borrowed || InventoryGrid.Slot(i.itemId) < 0) return false;
foreach (var hold in new[] { Hold.Cargo, Hold.SafeCase })
{
var list = Items(s.run, hold);
foreach (var i in list)
if (!Check(i) || !InventoryGrid.Fits(list, Capacity(hold), i, i.x, i.y, i.rotated)) return false;
}
foreach (var i in s.run.installed)
{
if (!Check(i)) return false;
int slot = InventoryGrid.Slot(i.itemId);
if (slot < 0 || !slots.Add(slot) || InventoryGrid.EquipmentId(slot, s.run.equipment[slot]) != i.itemId) return false;
}
if (s.run.active)
{
if (string.IsNullOrEmpty(s.run.id)) return false;
for (int slot = 0; slot < 4; slot++)
if (!slots.Contains(slot) && s.run.equipment[slot] != -1) return false;
}
else if (s.run.destructionPending || s.run.installed.Count + s.run.cargo.Count + s.run.safeCase.Count != 0) return false;
return new HashSet<string>(s.receipts).Count == s.receipts.Count;
}
public bool Manufacture(string blueprint) => Commit(s =>
{
var recipe = InventoryGrid.Recipe(blueprint);
if (s.run.active || recipe.item == null || !s.unlocked.Contains(blueprint) ||
s.alloy < recipe.alloy || s.cells < recipe.cells) return false;
s.alloy -= recipe.alloy;
s.cells -= recipe.cells;
s.warehouse.Add(new CargoStack(recipe.item, 1));
return true;
});
public bool AddCargo(string id, int count, string sourceKey = "", string offeredPart = null)
{
if (!InventoryGrid.Known(id) || count < 1 || count > (InventoryGrid.IsMaterial(id) ? 10 : 1)) return false;
return Commit(s =>
{
var r = s.run;
if (!Live(r) || (!string.IsNullOrEmpty(sourceKey) && r.searched.Contains(sourceKey))) return false;
int remaining = count;
if (InventoryGrid.IsMaterial(id))
{
foreach (var stack in r.cargo)
{
if (stack.itemId != id) continue;
int added = Math.Min(10 - stack.count, remaining);
stack.count += added;
remaining -= added;
if (remaining == 0) break;
}
}
if (remaining > 0 && !InventoryGrid.AutoPlace(r.cargo, InventoryGrid.CargoSize, new CargoStack(id, remaining))) return false;
if (offeredPart != null && (InventoryGrid.Slot(offeredPart) < 0 ||
!InventoryGrid.AutoPlace(r.cargo, InventoryGrid.CargoSize, new CargoStack(offeredPart, 1)))) return false;
if (!string.IsNullOrEmpty(sourceKey)) r.searched.Add(sourceKey);
return true;
});
}
// Omitting coordinates requests first-fit placement. Explicit placement also rotates in place.
public bool MoveItem(string uid, Hold from, Hold to, int x = -1, int y = -1, bool rotated = false, int quantity = 0)
{
if (!ValidHold(from) || !ValidHold(to)) return false;
return Commit(s =>
{
if (!Live(s.run)) return false;
var source = Items(s.run, from);
var item = source.Find(i => i.uid == uid);
if (item == null || quantity < 0 || quantity > item.count) return false;
if (quantity == 0 || quantity == item.count) source.Remove(item);
else
{
item.count -= quantity;
item = new CargoStack(item.itemId, quantity);
}
var target = Items(s.run, to);
return x == -1 && y == -1 ? InventoryGrid.AutoPlace(target, Capacity(to), item)
: InventoryGrid.Place(target, Capacity(to), item, x, y, rotated);
});
}
public bool SwapItems(string uid, Hold from, string otherUid, Hold to, bool rotated)
{
if (!ValidHold(from) || !ValidHold(to) || uid == otherUid) return false;
return Commit(s =>
{
if (!Live(s.run)) return false;
var source = Items(s.run, from);
var target = Items(s.run, to);
var first = source.Find(i => i.uid == uid);
var second = target.Find(i => i.uid == otherUid);
if (first == null || second == null) return false;
int x = first.x, y = first.y;
source.Remove(first);
target.Remove(second);
return InventoryGrid.Place(target, Capacity(to), first, second.x, second.y, rotated) &&
InventoryGrid.Place(source, Capacity(from), second, x, y, second.rotated);
});
}
public bool MergeMaterial(string sourceUid, Hold from, string targetUid, Hold to, int quantity)
{
if (!ValidHold(from) || !ValidHold(to) || sourceUid == targetUid || quantity < 1) return false;
return Commit(s =>
{
if (!Live(s.run)) return false;
var source = Items(s.run, from).Find(i => i.uid == sourceUid);
var target = Items(s.run, to).Find(i => i.uid == targetUid);
if (source == null || target == null || source.itemId != target.itemId || !InventoryGrid.IsMaterial(source.itemId)
|| quantity > source.count || quantity > 10 - target.count) return false;
source.count -= quantity;
target.count += quantity;
if (source.count == 0) Items(s.run, from).Remove(source);
return true;
});
}
public bool Uninstall(int slot, Hold to, int x, int y, bool rotated)
{
if (!ValidHold(to)) return false;
return Commit(s =>
{
var r = s.run;
if (!Live(r) || slot < 0 || slot > 3) return false;
var item = r.installed.Find(i => InventoryGrid.Slot(i.itemId) == slot);
if (item == null || !InventoryGrid.Place(Items(r, to), Capacity(to), item, x, y, rotated)) return false;
r.installed.Remove(item);
r.equipment[slot] = -1;
return true;
});
}
public bool InstallItem(string uid, Hold from, Func<int[], bool> fitsBudget)
{
if (!ValidHold(from) || fitsBudget == null) return false;
return Commit(s =>
{
var r = s.run;
if (!Live(r)) return false;
var source = Items(r, from);
var item = source.Find(i => i.uid == uid);
if (item == null) return false;
int slot = InventoryGrid.Slot(item.itemId);
if (slot < 0) return false;
r.equipment[slot] = InventoryGrid.Index(slot, item.itemId);
if (!fitsBudget(r.equipment)) return false;
source.Remove(item);
var old = r.installed.Find(i => InventoryGrid.Slot(i.itemId) == slot);
if (old != null)
{
if (!InventoryGrid.AutoPlace(source, Capacity(from), old)) return false;
r.installed.Remove(old);
}
r.installed.Add(item);
return true;
});
}
static void Bank(GameSave s, CargoStack item)
{
if (item.borrowed) return;
if (InventoryGrid.Slot(item.itemId) >= 0) s.warehouse.Add(item);
else if (item.itemId == "alloy") s.alloy += item.count;
else if (item.itemId == "cells") s.cells += item.count;
else if (!s.blueprints.Contains(item.itemId)) s.blueprints.Add(item.itemId);
else s.alloy += 2;
}
static List<CargoStack> Manifest(RunRecord r, IReadOnlyCollection<string> selection)
{
if (!Live(r) || r.podUsed || selection == null || selection.Count == 0 || selection.Count > 4) return null;
var ids = new HashSet<string>(selection);
var picked = r.cargo.FindAll(i => ids.Contains(i.uid));
return picked.Count == selection.Count && InventoryGrid.PodFits(picked) ? picked : null;
}
public bool CanSendCargo(IReadOnlyCollection<string> selection) => Manifest(Data.run, selection) != null;
public bool SendCargo(IReadOnlyCollection<string> selection) => Commit(s =>
{
string receipt = s.run.id + ":cargo";
var picked = Manifest(s.run, selection);
if (picked == null || s.receipts.Contains(receipt)) return false;
foreach (var item in picked)
{
Bank(s, item);
s.run.cargo.Remove(item);
}
s.run.podUsed = true;
s.receipts.Add(receipt);
return true;
});
public bool MarkDestroyed() => Commit(s =>
{
if (!s.run.active) return false;
s.run.armor = 0;
s.run.destructionPending = true;
return true;
});
public bool Finish(bool destroyed)
{
if (destroyed && !Data.run.destructionPending && !MarkDestroyed()) return false;
return Write(s =>
{
var r = s.run;
string receipt = r.id + ":return";
if (!r.active || r.destructionPending != destroyed || s.receipts.Contains(receipt)) return false;
if (destroyed) s.receipts.Add(r.id + ":secure_eject");
else
{
if (r.armor <= 0) return false;
foreach (var item in r.cargo) Bank(s, item);
foreach (var item in r.installed) Bank(s, item);
s.extractions++;
}
foreach (var item in r.safeCase) Bank(s, item);
r.safeCase.Clear();
r.cargo.Clear();
r.installed.Clear();
r.active = false;
r.destructionPending = false;
s.receipts.Add(receipt);
return true;
});
}
}
}