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,113 @@
using System;
using System.Collections.Generic;
namespace Ironspore
{
public enum Hold { Cargo, SafeCase }
// Stable content IDs also resolve the existing four gameplay slot indices.
public static class InventoryGrid
{
public const int CargoSize = 4, CaseSize = 2, PodSize = 2;
static readonly string[][] Slots = {
new[] { "rapid", "coil", "siege" }, new[] { "guided", "pulse" },
new[] { "efficient", "vector" }, new[] { "composite_guard", "reactive_guard" }
};
public static string EquipmentId(int slot, int index) =>
slot >= 0 && slot < Slots.Length && index >= 0 && index < Slots[slot].Length ? Slots[slot][index] : null;
public static int Slot(string id) => Array.FindIndex(Slots, group => Array.IndexOf(group, id) >= 0);
public static int Index(int slot, string id) => Array.IndexOf(Slots[slot], id);
public static bool IsMaterial(string id) => id == "alloy" || id == "cells";
public static bool IsBlueprint(string id) => id != null && id.StartsWith("blueprint_", StringComparison.Ordinal) && id.Length > 10;
public static bool Known(string id) => IsMaterial(id) || IsBlueprint(id) || Slot(id) >= 0;
public static (string item, int alloy, int cells) Recipe(string blueprint)
{
switch (blueprint)
{
case "blueprint_coil_capacitor": return ("coil", 4, 2);
case "blueprint_boost_efficient": return ("vector", 3, 2);
case "blueprint_reactive_guard": return ("reactive_guard", 4, 1);
default: return (null, 0, 0);
}
}
public static void Size(string id, bool rotated, out int width, out int height)
{
width = 1;
height = 1;
switch (id)
{
case "rapid": height = 3; break;
case "coil": width = 2; height = 3; break;
case "siege": width = 2; height = 4; break;
case "pulse": case "reactive_guard": case "vector": width = 2; height = 2; break;
case "guided": case "efficient": case "composite_guard": height = 2; break;
}
if (rotated) (width, height) = (height, width);
}
public static int Area(CargoStack item)
{
Size(item.itemId, false, out int w, out int h);
return w * h;
}
public static bool Fits(List<CargoStack> items, int size, CargoStack item, int x, int y, bool rotated)
{
Size(item.itemId, rotated, out int w, out int h);
if (x < 0 || y < 0 || x > size - w || y > size - h) return false;
foreach (var other in items)
{
if (other == null) return false;
if (ReferenceEquals(item, other)) continue;
Size(other.itemId, other.rotated, out int ow, out int oh);
if (x < other.x + ow && x + w > other.x && y < other.y + oh && y + h > other.y)
return false;
}
return true;
}
public static bool Place(List<CargoStack> items, int size, CargoStack item, int x, int y, bool rotated)
{
if (items.Contains(item) || !Fits(items, size, item, x, y, rotated)) return false;
item.x = x;
item.y = y;
item.rotated = rotated;
items.Add(item);
return true;
}
public static bool AutoPlace(List<CargoStack> items, int size, CargoStack item)
{
for (int turn = 0; turn < 2; turn++)
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
if (Place(items, size, item, x, y, turn == 1)) return true;
return false;
}
// The pod is small. Try alternate placements so a greedy choice cannot reject a valid manifest.
public static bool PodFits(List<CargoStack> selection)
{
if (selection.Count == 0 || selection.Count > PodSize * PodSize) return false;
return Pack(0, new List<CargoStack>());
bool Pack(int index, List<CargoStack> placed)
{
if (index == selection.Count) return true;
var next = new CargoStack(selection[index].itemId, selection[index].count) { uid = selection[index].uid };
for (int turn = 0; turn < 2; turn++)
for (int y = 0; y < PodSize; y++)
for (int x = 0; x < PodSize; x++)
{
if (!Place(placed, PodSize, next, x, y, turn == 1)) continue;
if (Pack(index + 1, placed)) return true;
placed.RemoveAt(placed.Count - 1);
}
return false;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2e9cb0228d5ccb643a654d0e82090d2a
@@ -7,7 +7,11 @@ namespace Ironspore
{
public static class GameLocalization
{
#if UNITY_EDITOR
public static string PreferenceKey => "settings.locale." + Hash128.Compute(Application.dataPath);
#else
public const string PreferenceKey = "settings.locale";
#endif
public const string FallbackCode = "en";
public static event Action Changed;
public static string CurrentCode { get; private set; } = FallbackCode;
@@ -40,6 +44,7 @@ namespace Ironspore
tables.Add(locale.code, ReadTable(locale.table));
if (!tables.ContainsKey(FallbackCode))
throw new InvalidOperationException("English fallback table is missing.");
if (catalog.uiFont) PrepareGlyphs(catalog);
string system = CultureInfo.CurrentUICulture.Name;
if (Application.systemLanguage == SystemLanguage.ChineseSimplified || Application.systemLanguage == SystemLanguage.ChineseTraditional || Application.systemLanguage == SystemLanguage.Chinese)
system = "zh-Hans";
@@ -48,6 +53,23 @@ namespace Ironspore
CurrentCode = FallbackCode;
}
static void PrepareGlyphs(LocalizationCatalog catalog)
{
// A clean import can start with an empty dynamic atlas.
var missingGlyphs = new SortedSet<char>();
foreach (var table in tables.Values)
foreach (string value in table.Values)
foreach (char glyph in value)
if (!char.IsControl(glyph) && !catalog.uiFont.HasCharacter(glyph)) missingGlyphs.Add(glyph);
if (missingGlyphs.Count > 0)
{
var glyphs = new char[missingGlyphs.Count];
missingGlyphs.CopyTo(glyphs);
catalog.uiFont.TryAddCharacters(new string(glyphs), out string absent);
if (!string.IsNullOrEmpty(absent)) Debug.LogWarning("Missing UI font glyphs: " + absent);
}
}
public static Dictionary<string, string> ReadTable(TextAsset source)
{
if (!source)
+6 -5
View File
@@ -92,12 +92,12 @@ namespace Ironspore
pitch = Mathf.Clamp(pitch - look.y * .105f, -72, 75);
if (game.Action("View").WasPressedThisFrame())
FirstPerson = !FirstPerson;
if (game.Action("Guard").WasPressedThisFrame() && Energy >= 20)
if (game.Action("Guard").WasPressedThisFrame() && game.Run.equipment[3] >= 0 && Energy >= 20)
{
Energy -= 20;
Guard = 2.5f;
}
if (game.Action("Boost").WasPressedThisFrame() && boostCooldown <= 0 && Energy >= 18)
if (game.Action("Boost").WasPressedThisFrame() && game.Run.equipment[2] >= 0 && boostCooldown <= 0 && Energy >= 18)
{
boostDirection = Quaternion.Euler(0, yaw, 0) * new Vector3(movement.x, 0, movement.y == 0 && movement.x == 0 ? 1 : movement.y);
boostDirection.Normalize();
@@ -116,7 +116,7 @@ namespace Ironspore
}
}
Vector3 velocity = Quaternion.Euler(0, yaw, 0) * new Vector3(movement.x, 0, movement.y);
float speed = frame.speed * game.Part(2, game.Run.equipment[2]).speedFactor;
float speed = frame.speed * (game.Part(2, game.Run.equipment[2])?.speedFactor ?? 1f);
velocity = Vector3.ClampMagnitude(velocity, 1) * speed;
if (boostRemaining > 0)
{
@@ -141,7 +141,7 @@ namespace Ironspore
freshTrigger = false;
if (!freshTrigger && game.Action("Fire").IsPressed() && shotCooldown <= 0)
Shoot(false);
if (game.Action("Secondary").WasPressedThisFrame() && sideCooldown <= 0 && Energy >= 15)
if (game.Action("Secondary").WasPressedThisFrame() && game.Run.equipment[1] >= 0 && sideCooldown <= 0 && Energy >= 15)
{
Energy -= 15;
Shoot(true);
@@ -171,6 +171,7 @@ namespace Ironspore
void Shoot(bool secondary)
{
var part = game.Part(secondary ? 1 : 0, game.Run.equipment[secondary ? 1 : 0]);
if (!part) return;
shotCooldown = secondary ? shotCooldown : 1 / part.fireRate;
Ray ray = view.ViewportPointToRay(new Vector3(.5f, .5f));
Vector3 target = ray.GetPoint(180);
@@ -211,7 +212,7 @@ namespace Ironspore
if (!game.InField || Health <= 0)
return;
LastDamageFrame = Time.frameCount;
float armorFactor = game.Part(3, game.Run.equipment[3]).id == "reactive_guard" ? .72f : 1;
float armorFactor = game.Part(3, game.Run.equipment[3])?.id == "reactive_guard" ? .72f : 1;
Health = Mathf.Max(0, Health - amount * armorFactor * (Guard > 0 ? .2f : 1));
game.Run.armor = Health;
}
+61 -35
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.InputSystem;
@@ -53,6 +52,7 @@ namespace Ironspore
get; private set;
}
public int SelectedChassis = 1;
public readonly string[] SelectedInstances = new string[4];
public int[] InitialEquipment = { 0, 0, 0, 0 };
LocalizedMessage noticeMessage;
public string Notice => noticeMessage?.ToString() ?? "";
@@ -76,7 +76,7 @@ namespace Ironspore
Active = this;
GameLocalization.Initialize(localization);
Application.targetFrameRate = 120;
mainStore = new ProfileStore(Path.Combine(Application.persistentDataPath, "Ironspore", "profile.json"));
mainStore = new ProfileStore(ProfileStore.SavePath(false));
Store = mainStore;
Input = Instantiate(inputTemplate);
Input.Enable();
@@ -90,14 +90,16 @@ namespace Ironspore
World.BuildHangar();
Hud.ShowHangar();
SetMenu(true);
if (Run.active && (Run.destructionPending || Run.armor <= 0))
EndSortie(true);
if (startInRange)
StartTrial();
}
public EquipmentDefinition Part(int slot, int index)
{
var list = Array.FindAll(equipment, x => x.slot == slot);
return list[Mathf.Clamp(index, 0, list.Length - 1)];
string id = InventoryGrid.EquipmentId(slot, index);
return Array.Find(equipment, x => x.id == id);
}
public int PartCount(int slot) => Array.FindAll(equipment, x => x.slot == slot).Length;
public bool Fits(int frame, int[] parts)
@@ -106,6 +108,8 @@ namespace Ironspore
for (int i = 0; i < 4; i++)
{
var p = Part(i, parts[i]);
if (parts[i] == -1) continue;
if (!p) return false;
mass += p.mass;
power += p.power;
}
@@ -113,10 +117,13 @@ namespace Ironspore
}
public bool StartingPartsUnlocked()
{
var unlocked = mainStore.Data.unlocked;
return (InitialEquipment[0] != 1 || unlocked.Contains("blueprint_coil_capacitor"))
&& (InitialEquipment[2] != 1 || unlocked.Contains("blueprint_boost_efficient"))
&& (InitialEquipment[3] != 1 || unlocked.Contains("blueprint_reactive_guard"));
for (int slot = 0; slot < 4; slot++)
{
if (InitialEquipment[slot] == 0 && SelectedInstances[slot] == null) continue;
string id = InventoryGrid.EquipmentId(slot, InitialEquipment[slot]);
if (!mainStore.Data.warehouse.Exists(i => i.itemId == id && (SelectedInstances[slot] == null || i.uid == SelectedInstances[slot]))) return false;
}
return true;
}
public void BeginSortie()
@@ -138,7 +145,7 @@ namespace Ironspore
}
Trial = false;
Store = mainStore;
if (!Store.Begin(SelectedChassis, InitialEquipment, chassis[SelectedChassis].armor, Environment.TickCount))
if (!Store.Begin(SelectedChassis, InitialEquipment, chassis[SelectedChassis].armor, Environment.TickCount, selected: SelectedInstances))
{
NotifySaveFailure();
return;
@@ -147,17 +154,23 @@ namespace Ironspore
}
public void StartTrial()
{
var range = new ProfileStore(ProfileStore.SavePath(true));
if ((range.Data.run.active && !range.Finish(true)) ||
!range.Begin(SelectedChassis, InitialEquipment, chassis[SelectedChassis].armor, 1739, trial: true))
{
Notify("notice.saveFailed");
return;
}
Trial = true;
Store = new ProfileStore(Path.Combine(Application.persistentDataPath, "Ironspore", "range.json"));
Store.Begin(SelectedChassis, InitialEquipment, chassis[SelectedChassis].armor, 1739);
Store = range;
EnterField();
}
public void Resume()
{
Trial = false;
Store = mainStore;
if (Run.active)
EnterField();
if (Run.active && (Run.destructionPending || Run.armor <= 0)) EndSortie(true);
else if (Run.active) EnterField();
}
void EnterField()
{
@@ -211,6 +224,11 @@ namespace Ironspore
noticeMessage = null;
if (!InField || !Pilot || ending)
return;
if (Pilot.Health <= 0 || Run.destructionPending)
{
EndSortie(true);
return;
}
if (Action("Pause").WasPressedThisFrame())
{
if (MenuOpen)
@@ -239,11 +257,6 @@ namespace Ironspore
if (!MenuOpen && Action("Interact").WasPressedThisFrame() && World.Focused)
World.Focused.Activate();
TickCargo();
if (Pilot.Health <= 0)
{
EndSortie(true);
return;
}
autosaveTimer += Time.deltaTime;
if (autosaveTimer >= 10)
{
@@ -281,10 +294,12 @@ namespace Ironspore
{
if (SelectedCargo.Contains(uid))
SelectedCargo.Remove(uid);
else if (SelectedCargo.Count < 2)
SelectedCargo.Add(uid);
else
Notify("notice.twoSlots");
{
var next = new List<string>(SelectedCargo) { uid };
if (Store.CanSendCargo(next)) SelectedCargo.Add(uid);
else Notify("notice.twoSlots");
}
Hud.ShowCargo();
}
public void SnapshotVitals()
@@ -296,7 +311,9 @@ namespace Ironspore
}
public void Search(WorldInteractable target)
{
if (!Store.AddCargo(target.itemId, target.amount, target.key))
int index = target.offersPart ? Mathf.Clamp(target.partIndex, 0, PartCount(target.partSlot) - 1) : -1;
string offered = target.offersPart ? Part(target.partSlot, index).id : null;
if (!Store.AddCargo(target.itemId, target.amount, target.key, offered))
{
NotifySaveFailure("notice.holdFull");
return;
@@ -308,22 +325,27 @@ namespace Ironspore
if (target.offersPart)
{
SetMenu(true);
Hud.ShowPart(target.partSlot, target.partIndex);
Hud.ShowPart(target.partSlot, index);
}
}
public void Install(int slot, int index)
{
var next = (int[])Run.equipment.Clone();
next[slot] = index;
if (!Fits(Run.chassis, next))
{
Notify("notice.partBudget");
return;
}
Store.Commit(s => { s.run.equipment = next; return true; });
Pilot.ApplyLoadout();
var item = Run.cargo.FindLast(i => i.itemId == InventoryGrid.EquipmentId(slot, index));
if (item == null || !InstallItem(item, Hold.Cargo)) return;
CloseMenu();
Notify("notice.refit", GameLocalization.Message("equipment." + Part(slot, index).id + ".name"));
}
public bool InstallItem(CargoStack item, Hold from)
{
if (!Store.InstallItem(item.uid, from, parts => Fits(Run.chassis, parts)))
{
NotifySaveFailure("notice.installFailed");
return false;
}
SelectedCargo.Remove(item.uid);
Pilot.ApplyLoadout();
Notify("notice.refit", GameLocalization.Message("equipment." + item.itemId + ".name"));
return true;
}
public void CompleteObjective()
{
@@ -398,15 +420,19 @@ namespace Ironspore
return;
SnapshotVitals();
bool complete = Run.routeCompleted;
int lost = Run.cargo.Count;
int lost = Run.cargo.Count + Run.installed.Count;
int protectedCount = Run.safeCase.FindAll(i => !i.borrowed).Count;
if (!Store.Finish(destroyed))
{
NotifySaveFailure();
return;
}
ending = true;
reportMessage = GameLocalization.Message(Trial ? "report.range" : destroyed ? "report.destroyed" : complete ? "report.complete" : "report.extracted", lost);
reportMessage = GameLocalization.Message(Trial ? "report.range" : destroyed ? "report.destroyed" : complete ? "report.complete" : "report.extracted", lost, protectedCount);
InField = false;
Paused = false;
Time.timeScale = 1;
Array.Clear(SelectedInstances, 0, SelectedInstances.Length);
Trial = false;
Store = mainStore;
Pilot = null;
@@ -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;
});
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 988a620c32c371a4da682504221e3651
+58 -98
View File
@@ -11,12 +11,14 @@ namespace Ironspore
public string uid = Guid.NewGuid().ToString("N");
public string itemId;
public int count;
public int x, y;
public bool rotated, borrowed;
public CargoStack(string id, int amount)
{
itemId = id;
count = amount;
}
public string Name => GameLocalization.ItemName(itemId);
public string Name => InventoryGrid.Slot(itemId) >= 0 ? GameLocalization.Text("equipment." + itemId + ".name") : GameLocalization.ItemName(itemId);
}
[Serializable]
@@ -39,13 +41,18 @@ namespace Ironspore
public float elapsed;
public float escortArmor = 250;
public List<CargoStack> cargo = new List<CargoStack>();
public List<CargoStack> safeCase = new List<CargoStack>();
public List<CargoStack> installed = new List<CargoStack>();
public bool destructionPending;
public List<string> searched = new List<string>();
}
[Serializable]
public sealed class GameSave
{
public int version = 1;
public const int CurrentVersion = 2;
public int version = CurrentVersion;
public List<CargoStack> warehouse = new List<CargoStack>();
public int alloy;
public int cells;
public int sorties;
@@ -57,7 +64,7 @@ namespace Ironspore
public RunRecord run = new RunRecord();
}
public sealed class ProfileStore
public sealed partial class ProfileStore
{
readonly string path;
public GameSave Data
@@ -69,10 +76,28 @@ namespace Ironspore
get; private set;
}
public static string SavePath(bool trial)
{
#if UNITY_EDITOR
string root = Path.Combine(Path.GetDirectoryName(Application.dataPath), "UserSettings");
#else
string root = Application.persistentDataPath;
#endif
return Path.Combine(root, "Ironspore", trial ? "range-v2.json" : "profile-v2.json");
}
public ProfileStore(string savePath)
{
path = savePath;
Data = Load(path) ?? Load(path + ".bak") ?? new GameSave();
Data = Load(path) ?? Load(path + ".bak");
if (Data == null)
{
if (File.Exists(path) || File.Exists(path + ".bak"))
throw new InvalidDataException("No valid equipment snapshot: " + path);
Data = new GameSave();
for (int slot = 0; slot < 4; slot++)
Data.warehouse.Add(new CargoStack(InventoryGrid.EquipmentId(slot, 0), 1));
}
}
static GameSave Load(string file)
@@ -82,19 +107,25 @@ namespace Ironspore
if (!File.Exists(file))
return null;
var save = JsonUtility.FromJson<GameSave>(File.ReadAllText(file));
if (save == null || save.version != 1 || save.run == null || save.receipts == null || save.blueprints == null || save.unlocked == null)
return null;
if (save.run.cargo == null || save.run.equipment == null || save.run.equipment.Length != 4)
return null;
if (save != null && save.version != GameSave.CurrentVersion)
throw new NotSupportedException("Unsupported equipment save version: " + save.version);
if (!Valid(save)) return null;
return save;
}
catch (Exception) { return null; }
catch (ArgumentException) { return null; }
}
public bool Commit(Func<GameSave, bool> change)
{
if (Data.run.destructionPending) return false;
return Write(change);
}
bool Write(Func<GameSave, bool> change)
{
Error = null;
var candidate = JsonUtility.FromJson<GameSave>(JsonUtility.ToJson(Data));
if (!change(candidate))
if (!change(candidate) || !Valid(candidate))
return false;
try
{
@@ -107,7 +138,7 @@ namespace Ironspore
stream.Flush(true);
}
if (File.Exists(path))
File.Replace(temp, path, path + ".bak");
File.Replace(temp, path, Load(path) == null ? null : path + ".bak");
else
File.Move(temp, path);
Data = candidate;
@@ -117,102 +148,31 @@ namespace Ironspore
catch (Exception ex) { Error = ex.Message; return false; }
}
public bool Begin(int chassis, int[] equipment, float armor, int seed)
public bool Begin(int chassis, int[] equipment, float armor, int seed, bool trial = false, string[] selected = null)
{
return Commit(s =>
{
if ((selected != null && selected.Length != 4) || s.run.active || equipment == null || equipment.Length != 4 || armor <= 0) return false;
s.run = new RunRecord { id = Guid.NewGuid().ToString("N"), active = true, seed = seed, chassis = chassis, equipment = (int[])equipment.Clone(), armor = armor };
for (int slot = 0; slot < 4; slot++)
{
string id = InventoryGrid.EquipmentId(slot, equipment[slot]);
if (id == null) return false;
var requested = selected?[slot];
var item = s.warehouse.Find(part => part.itemId == id && (requested == null || requested == part.uid));
if (item == null)
{
if (requested != null || (!trial && equipment[slot] != 0)) return false;
item = new CargoStack(id, 1) { borrowed = true };
}
else s.warehouse.Remove(item);
s.run.installed.Add(item);
}
s.sorties++;
return true;
});
}
public bool AddCargo(string id, int count, string sourceKey = "")
{
if (count <= 0)
return false;
return Commit(s =>
{
if (!s.run.active)
return false;
if (sourceKey.Length > 0 && s.run.searched.Contains(sourceKey))
return false;
foreach (var stack in s.run.cargo)
{
if (id.StartsWith("blueprint_") || stack.itemId != id || stack.count + count > 10)
continue;
stack.count += count;
if (sourceKey.Length > 0)
s.run.searched.Add(sourceKey);
return true;
}
if (s.run.cargo.Count >= 4 || (id.StartsWith("blueprint_") ? count != 1 : count > 10))
return false;
s.run.cargo.Add(new CargoStack(id, count));
if (sourceKey.Length > 0)
s.run.searched.Add(sourceKey);
return true;
});
}
static void Bank(GameSave s, CargoStack stack)
{
if (stack.itemId == "alloy")
s.alloy += stack.count;
else if (stack.itemId == "cells")
s.cells += stack.count;
else if (!s.blueprints.Contains(stack.itemId))
s.blueprints.Add(stack.itemId);
else
s.alloy += 2;
}
public bool SendCargo(IReadOnlyCollection<string> selection)
{
if (selection == null || selection.Count == 0 || selection.Count > 2)
return false;
var ids = new HashSet<string>(selection);
if (ids.Count != selection.Count)
return false;
return Commit(s =>
{
string receipt = s.run.id + ":cargo";
if (!s.run.active || s.run.armor <= 0 || s.run.podUsed || s.receipts.Contains(receipt))
return false;
var picked = s.run.cargo.FindAll(x => ids.Contains(x.uid));
if (picked.Count != ids.Count)
return false;
foreach (var stack in picked)
{
Bank(s, stack);
s.run.cargo.Remove(stack);
}
s.run.podUsed = true;
s.receipts.Add(receipt);
return true;
});
}
public bool Finish(bool destroyed)
{
return Commit(s =>
{
string receipt = s.run.id + ":return";
if (!s.run.active || s.receipts.Contains(receipt))
return false;
if (!destroyed)
{
foreach (var stack in s.run.cargo)
Bank(s, stack);
s.extractions++;
}
s.run.cargo.Clear();
s.run.active = false;
s.receipts.Add(receipt);
return true;
});
}
public bool CompleteRoute()
{
return Commit(s =>
+25 -17
View File
@@ -1,11 +1,12 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
namespace Ironspore
{
public sealed class GameHud : MonoBehaviour
public sealed partial class GameHud : MonoBehaviour
{
GameSession game;
VisualElement root;
@@ -37,6 +38,9 @@ namespace Ironspore
root.styleSheets.Add(game.uiStyle);
root.pickingMode = PickingMode.Ignore;
root.style.unityFontDefinition = FontDefinition.FromSDFFont(game.localization.uiFont);
root.RegisterCallback<PointerMoveEvent>(DragMove, TrickleDown.TrickleDown);
root.RegisterCallback<PointerUpEvent>(DragEnd, TrickleDown.TrickleDown);
root.RegisterCallback<PointerCancelEvent>(_ => ResetDrag(), TrickleDown.TrickleDown);
root.RegisterCallback<KeyDownEvent>(HandleLanguageKey, TrickleDown.TrickleDown);
GameLocalization.Changed += RefreshLocale;
}
@@ -106,6 +110,7 @@ namespace Ironspore
}
void Clear()
{
ResetDrag();
root.Clear();
mission = prompt = status = notice = telemetry = null;
armor = energy = cargo = null;
@@ -225,7 +230,7 @@ namespace Ironspore
{
int s = slot;
var p = game.Part(s, game.InitialEquipment[s]);
Button(content, L("ui.home.part", L("ui.slot." + s), p.LocalizedName, p.mass, p.power), () => { game.InitialEquipment[s] = (game.InitialEquipment[s] + 1) % game.PartCount(s); ShowHangar(); }, "loadout");
Button(content, L("ui.home.part", L("ui.slot." + s), p.LocalizedName, p.mass, p.power), () => { game.SelectedInstances[s] = null; game.InitialEquipment[s] = (game.InitialEquipment[s] + 1) % game.PartCount(s); ShowHangar(); }, "loadout");
}
float mass = 0, power = 0;
for (int i = 0; i < 4; i++)
@@ -249,12 +254,27 @@ namespace Ironspore
var save = game.Store.Data;
Text(content, L("ui.home.inventory"), "section");
Text(content, L("ui.home.stock", save.alloy, save.cells, save.blueprints.Count, save.extractions, save.completed), "budget");
foreach (var item in save.warehouse)
{
int slot = InventoryGrid.Slot(item.itemId);
Button(content, L("ui.home.chooseItem", item.Name, item.uid.Substring(0, Math.Min(6, item.uid.Length))), () =>
{
game.SelectedInstances[slot] = item.uid;
game.InitialEquipment[slot] = InventoryGrid.Index(slot, item.itemId);
ShowHangar();
}, game.SelectedInstances[slot] == item.uid ? "loadout selected" : "loadout");
}
foreach (string blueprint in save.blueprints)
{
string id = blueprint;
bool unlocked = save.unlocked.Contains(id);
var b = Button(content, L(unlocked ? "ui.home.researched" : "ui.home.research", GameLocalization.ItemName(id)), () => game.Unlock(id), "loadout");
b.SetEnabled(!unlocked);
var recipe = InventoryGrid.Recipe(id);
if (unlocked && recipe.item != null)
Button(content, L("ui.home.manufacture", L("equipment." + recipe.item + ".name"), recipe.alloy, recipe.cells),
() => { if (!game.Store.Manufacture(id)) game.Notify("notice.saveFailed"); ShowHangar(); }, "loadout")
.SetEnabled(!save.run.active && save.alloy >= recipe.alloy && save.cells >= recipe.cells);
}
hangarNotice = Text(content, game.Notice, "warning");
RefreshHangarNotice();
@@ -314,19 +334,6 @@ namespace Ironspore
Text(panel, body, "body");
return panel;
}
public void ShowCargo()
{
var panel = Overlay(L("ui.cargo.title"), L(game.Run.podUsed ? "ui.cargo.used" : "ui.cargo.help"));
view = View.Cargo;
foreach (var stack in game.Run.cargo)
{
string uid = stack.uid;
var b = Button(panel, L("ui.cargo.stack", game.SelectedCargo.Contains(uid) ? L("ui.cargo.selected") : "[ ]", stack.Name, stack.count), () => game.ToggleCargo(uid), "loadout");
b.SetEnabled(!game.Run.podUsed);
}
Text(panel, L("ui.cargo.capacity", game.Run.cargo.Count, game.SelectedCargo.Count), "budget");
Button(panel, L("ui.return"), game.CloseMenu, "primary");
}
public void ShowPart(int slot, int index)
{
index = Mathf.Clamp(index, 0, game.PartCount(slot) - 1);
@@ -337,7 +344,7 @@ namespace Ironspore
var next = game.Part(slot, index);
var panel = Overlay(L("ui.refit.title"), L("ui.refit.help"));
view = View.Part;
Text(panel, L("ui.refit.equipped", old.LocalizedName, old.mass, old.power), "body");
Text(panel, L("ui.refit.equipped", old?.LocalizedName ?? "—", old?.mass ?? 0, old?.power ?? 0), "body");
Text(panel, L("ui.refit.recovered", next.LocalizedName, next.mass, next.power), "budget");
Button(panel, L("ui.refit.install"), () => game.Install(slot, partIndex), "primary");
Button(panel, L("ui.refit.keep"), game.CloseMenu);
@@ -378,6 +385,7 @@ namespace Ironspore
ActivateFocusedLanguage();
}
}
if (view == View.Cargo && selectedUid != null && Keyboard.current?.rKey.wasPressedThisFrame == true) RotateSelection();
if (!game.InField || !game.Pilot)
return;
var run = game.Run;
@@ -409,7 +417,7 @@ namespace Ironspore
if (notice != null)
notice.text = game.Notice;
if (telemetry != null)
telemetry.text = L("ui.hud.telemetry", game.chassis[run.chassis].LocalizedName, L(p.FirstPerson ? "ui.hud.cockpit" : "ui.hud.shoulder"), game.Part(0, run.equipment[0]).LocalizedName, run.cargo.Count, game.World.ExtractionRemaining >= 0 ? game.World.TransportReady ? L("ui.hud.transportReady") : L("ui.hud.transportEta", Mathf.CeilToInt(game.World.ExtractionRemaining)) : L("ui.hud.extractionAvailable"));
telemetry.text = L("ui.hud.telemetry", game.chassis[run.chassis].LocalizedName, L(p.FirstPerson ? "ui.hud.cockpit" : "ui.hud.shoulder"), game.Part(0, run.equipment[0])?.LocalizedName ?? "—", run.cargo.Sum(InventoryGrid.Area), game.World.ExtractionRemaining >= 0 ? game.World.TransportReady ? L("ui.hud.transportReady") : L("ui.hud.transportEta", Mathf.CeilToInt(game.World.ExtractionRemaining)) : L("ui.hud.extractionAvailable"));
if (cross != null)
cross.style.opacity = p.RecentHit ? 1 : .65f;
}
+241
View File
@@ -0,0 +1,241 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
namespace Ironspore
{
public sealed partial class GameHud
{
string selectedUid;
Hold selectedHold;
int selectedSlot = -1;
bool rotated;
int quantity;
const int CellWidth = 103, CellHeight = 55;
sealed class GridCell
{
public Hold hold;
public int x, y;
public CargoStack occupant;
}
CargoStack dragItem;
Hold dragHold;
int dragSlot, dragPointer;
Vector2 dragStart;
VisualElement dragPreview;
CargoStack Selection => selectedSlot >= 0 ? game.Run.installed.Find(i => i.uid == selectedUid) :
Contents(selectedHold).Find(i => i.uid == selectedUid);
List<CargoStack> Contents(Hold hold) => hold == Hold.Cargo ? game.Run.cargo : game.Run.safeCase;
void SelectInventoryItem(CargoStack item, Hold hold, int slot = -1)
{
selectedUid = item?.uid;
selectedSlot = slot;
selectedHold = hold;
rotated = item?.rotated ?? false;
quantity = item?.count ?? 0;
ShowCargo();
}
void RotateSelection()
{
rotated = !rotated;
if (dragPreview == null) ShowCargo();
else SizePreview();
}
public void ShowCargo()
{
var panel = Overlay(L("ui.cargo.title"), L(game.Run.podUsed ? "ui.cargo.used" : "ui.cargo.help"));
view = View.Cargo;
var installed = Element(panel, "row");
foreach (var part in game.Run.installed)
{
int slot = InventoryGrid.Slot(part.itemId);
var button = Button(installed, part.Name, () => SelectInventoryItem(part, Hold.Cargo, slot),
part.uid == selectedUid ? "loadout selected" : "loadout");
EnableDrag(button, part, Hold.Cargo, slot);
}
var grids = Element(panel, "row");
DrawHold(grids, Hold.Cargo, InventoryGrid.CargoSize);
DrawHold(grids, Hold.SafeCase, InventoryGrid.CaseSize);
Text(panel, L("ui.cargo.capacity", game.Run.cargo.Sum(InventoryGrid.Area),
game.Run.cargo.Where(i => game.SelectedCargo.Contains(i.uid)).Sum(InventoryGrid.Area)), "budget");
var item = Selection;
if (item != null)
{
InventoryGrid.Size(item.itemId, rotated, out int w, out int h);
Text(panel, L("ui.cargo.selection", item.Name, w, h, quantity), "body");
var actions = Element(panel, "row");
Button(actions, L("ui.cargo.rotate"), RotateSelection);
if (item.count > 1)
{
Button(actions, "−", () => { quantity--; ShowCargo(); }).SetEnabled(quantity > 1);
Button(actions, "+", () => { quantity++; ShowCargo(); }).SetEnabled(quantity < item.count);
}
if (selectedSlot < 0)
{
if (selectedHold == Hold.Cargo && !game.Run.podUsed)
Button(actions, L(game.SelectedCargo.Contains(item.uid) ? "ui.cargo.unmark" : "ui.cargo.mark"), () => game.ToggleCargo(item.uid));
if (InventoryGrid.Slot(item.itemId) >= 0)
Button(actions, L("ui.cargo.install"), () =>
{
if (game.InstallItem(item, selectedHold)) selectedUid = null;
ShowCargo();
});
}
Button(actions, L("ui.cargo.cancel"), () => SelectInventoryItem(null, Hold.Cargo));
}
Text(panel, game.Notice, "warning");
Button(panel, L("ui.return"), game.CloseMenu, "primary");
}
void DrawHold(VisualElement parent, Hold hold, int size)
{
var column = Element(parent, "inventory-column");
Text(column, L(hold == Hold.Cargo ? "ui.cargo.hold" : "ui.cargo.case"), "section");
var grid = new VisualElement();
column.Add(grid);
grid.style.width = size * CellWidth;
grid.style.height = size * CellHeight;
var items = Contents(hold);
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
int cx = x, cy = y;
var occupant = items.Find(i =>
{
InventoryGrid.Size(i.itemId, i.rotated, out int w, out int h);
return cx >= i.x && cy >= i.y && cx < i.x + w && cy < i.y + h;
});
var cell = Button(grid, "", () => InventoryClick(hold, cx, cy, occupant), "inventory-cell");
cell.name = hold + "-" + x + "-" + y;
cell.tooltip = occupant?.Name ?? "";
cell.userData = new GridCell { hold = hold, x = x, y = y, occupant = occupant };
if (occupant != null) EnableDrag(cell, occupant, hold, -1);
PositionInGrid(cell, x, y, 1, 1);
}
foreach (var item in items)
{
InventoryGrid.Size(item.itemId, item.rotated, out int w, out int h);
string prefix = game.SelectedCargo.Contains(item.uid) ? "✓ " : "";
var label = Text(grid, prefix + item.Name + (item.count > 1 ? " ×" + item.count : ""), "inventory-item");
label.pickingMode = PickingMode.Ignore;
if (item.uid == selectedUid) label.AddToClassList("selected");
PositionInGrid(label, item.x, item.y, w, h);
}
}
void EnableDrag(Button button, CargoStack item, Hold hold, int slot)
{
// Captured pointer events can be dispatched directly to the pressed button.
button.RegisterCallback<PointerMoveEvent>(DragMove, TrickleDown.TrickleDown);
button.RegisterCallback<PointerUpEvent>(DragEnd, TrickleDown.TrickleDown);
button.RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button != 0) return;
dragItem = item;
dragHold = hold;
dragSlot = slot;
dragStart = evt.position;
dragPointer = evt.pointerId;
}, TrickleDown.TrickleDown);
}
void DragMove(PointerMoveEvent evt)
{
if (dragItem == null || evt.pointerId != dragPointer) return;
Vector2 pos = evt.position;
if (dragPreview == null)
{
if ((pos - dragStart).sqrMagnitude < 36) return;
if (selectedUid != dragItem.uid)
{
quantity = dragItem.count;
rotated = dragItem.rotated;
}
selectedUid = dragItem.uid;
selectedSlot = dragSlot;
selectedHold = dragHold;
dragPreview = Text(root, dragItem.Name, "inventory-item selected");
dragPreview.pickingMode = PickingMode.Ignore;
dragPreview.style.position = UnityEngine.UIElements.Position.Absolute;
SizePreview();
}
var target = root.panel.Pick(pos);
Vector2 anchor = target?.userData is GridCell ? target.worldBound.min : pos;
Vector2 local = root.WorldToLocal(anchor);
dragPreview.style.left = local.x;
dragPreview.style.top = local.y;
}
void SizePreview()
{
InventoryGrid.Size(dragItem.itemId, rotated, out int w, out int h);
dragPreview.style.width = CellWidth * w - 2;
dragPreview.style.height = CellHeight * h - 2;
}
void DragEnd(PointerUpEvent evt)
{
if (dragItem == null || evt.pointerId != dragPointer) return;
bool dragged = dragPreview != null;
ResetDrag();
if (!dragged) return;
evt.StopImmediatePropagation();
var owner = root.panel.GetCapturingElement(evt.pointerId) as VisualElement;
owner?.ReleasePointer(evt.pointerId);
var target = root.panel.Pick(evt.position)?.userData as GridCell;
if (target != null) InventoryClick(target.hold, target.x, target.y, target.occupant);
else ShowCargo();
}
void ResetDrag()
{
dragPreview?.RemoveFromHierarchy();
dragPreview = null;
dragItem = null;
}
static void PositionInGrid(VisualElement element, int x, int y, int w, int h)
{
element.style.position = UnityEngine.UIElements.Position.Absolute;
element.style.left = x * CellWidth;
element.style.top = y * CellHeight;
element.style.width = w * CellWidth - 2;
element.style.height = h * CellHeight - 2;
}
void InventoryClick(Hold hold, int x, int y, CargoStack occupant)
{
var item = Selection;
if (item == null)
{
SelectInventoryItem(occupant, hold);
return;
}
bool ok;
if (selectedSlot >= 0)
ok = game.Store.Uninstall(selectedSlot, hold, x, y, rotated);
else if (occupant == null || item.uid == occupant.uid)
ok = game.Store.MoveItem(item.uid, selectedHold, hold, x, y, rotated, quantity);
else if (item.itemId == occupant.itemId && InventoryGrid.IsMaterial(item.itemId))
ok = game.Store.MergeMaterial(item.uid, selectedHold, occupant.uid, hold, quantity);
else
ok = quantity == item.count && game.Store.SwapItems(item.uid, selectedHold, occupant.uid, hold, rotated);
if (ok)
{
selectedUid = null;
game.SelectedCargo.RemoveAll(uid => !game.Run.cargo.Exists(i => i.uid == uid));
game.Pilot.ApplyLoadout();
}
else game.Notify(string.IsNullOrEmpty(game.Store.Error) ? "notice.placement" : "notice.saveFailed");
ShowCargo();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6faff651d0fe95448b2711a92bce81d3