Make loadouts authored catalog presets with an editor capture command
Loadouts are ship presets (variants) for spawners, not a player feature. - Route Loadout to the catalog; the player store no longer holds it. - Materialize takes no credits: affordability, the charge and Loadouts.Price are deleted, since only the player menu priced loadouts. Availability, key-order product choice, all-or-nothing failures and the weapon-group rules stay. - Delete the InventoryPanel Save Loadout and Restore Loadout entries and RestoreLoadout, which also removes its docking-bay overwrite. - The editor opens the catalog writable in the one process cache, and the editor-only console command capturepreset "<name>" [replace] captures the piloted ship. Loadouts.Commit keys a preset by name, refuses a same-name replace unless asked, and commits conditionally on that key. The single-file store then lands only that record onto the file as it is on disk. An unconditional commit would re-serialize every live catalog instance, including any that play has mutated. - Tests: capture -> commit -> fresh reopen round-trip, explicit replace with no duplicate, and a preset commit that does not persist a mutated live Faction. The money-only tests are removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e7dd62d02a
commit
db83cd290d
6 changed files with 124 additions and 166 deletions
|
|
@ -5,9 +5,10 @@ using GameCult.Caching;
|
|||
using MessagePack;
|
||||
using CultMath;
|
||||
|
||||
// A ship build as item designs only: the hull's design, and per hull cell a design and its rotation. A design names no
|
||||
// manufacturer; which product builds a slot is a fact of the galaxy the loadout is materialized in, which keeps a
|
||||
// loadout portable across galaxies. Player-store state: a loadout outlives runs.
|
||||
// A ship preset (a variant): item designs only, the hull's design, and per hull cell a design and its rotation. A design
|
||||
// names no manufacturer; which product builds a slot is a fact of the galaxy the preset is materialized in, which keeps
|
||||
// a preset portable across galaxies. Authored catalog data: written in CultCache Studio or by the editor capture
|
||||
// command, read at runtime.
|
||||
[CultDocument("aetheria.loadout", "1"), MessagePackObject]
|
||||
public class Loadout
|
||||
{
|
||||
|
|
@ -56,24 +57,34 @@ public static class Loadouts
|
|||
};
|
||||
}
|
||||
|
||||
// One Commit to the player store. A loadout with the same name is replaced under its existing key; names are
|
||||
// unique by construction because GetByName throws on duplicates.
|
||||
public static void Save(CultCache cache, Loadout loadout)
|
||||
// A preset's key derives from its name, so capturing the same name again addresses the same record.
|
||||
public static CultRecordKey KeyOf(string name) => new CultRecordKey("loadout:" + name);
|
||||
|
||||
// Writes the preset to the catalog under KeyOf(Name). An existing preset of that name is replaced only when replace
|
||||
// is set; otherwise this throws and writes nothing. The commit is conditional on the record at that key being what
|
||||
// this cache holds, so it lands only this record onto the catalog file as it is on disk: no other in-memory catalog
|
||||
// state is written, and a preset changed on disk since load (Studio) is not clobbered. Returns false on that conflict.
|
||||
public static bool Commit(CultCache cache, Loadout loadout, bool replace)
|
||||
{
|
||||
var existing = cache.GetByName<Loadout>(loadout.Name);
|
||||
var key = existing == null ? (CultRecordKey?) null : cache.TryGetHandle(existing)?.Key;
|
||||
cache.Commit(batch => batch.Upsert(typeof(Loadout), loadout, key));
|
||||
var key = KeyOf(loadout.Name);
|
||||
var existing = cache.Get<Loadout>(key);
|
||||
if (existing != null && !replace)
|
||||
throw new InvalidOperationException($"Preset '{loadout.Name}' already exists; replace it explicitly.");
|
||||
return cache.Commit(batch =>
|
||||
{
|
||||
batch.Expect(key, existing);
|
||||
batch.Upsert(typeof(Loadout), loadout, key);
|
||||
});
|
||||
}
|
||||
|
||||
// All-or-nothing: returns a ship and charges Price only when credits cover Price, every design resolved, had an
|
||||
// available product and fitted, the loadout has at most WeaponGroupCount weapon groups, and every group index names
|
||||
// a weapon slot. Affordability is decided here and nowhere else. Every failure is listed; on any failure nothing is
|
||||
// returned and nothing is charged. The ship, credits, zone and cache are unchanged, but a failed fit has already
|
||||
// drawn from itemManager.Random. A design is built by its first available product in record-key order. The game
|
||||
// passes LoadoutGenerator.IsAvailable as isAvailable, with no fallback to any manufacturer. The ship always gets
|
||||
// exactly WeaponGroupCount groups; a loadout with fewer is padded with empty groups.
|
||||
// All-or-nothing: returns a ship only when every design resolved, had an available product and fitted, the loadout
|
||||
// has at most WeaponGroupCount weapon groups, and every group index names a weapon slot. Every failure is listed; on
|
||||
// any failure nothing is returned and the zone and cache are unchanged, but a failed fit has already drawn from
|
||||
// itemManager.Random. A design is built by its first available product in record-key order. The game passes
|
||||
// LoadoutGenerator.IsAvailable as isAvailable, with no fallback to any manufacturer. The ship always gets exactly
|
||||
// WeaponGroupCount groups; a loadout with fewer is padded with empty groups.
|
||||
public static Ship Materialize(ItemManager itemManager, Zone liveZone, Loadout loadout,
|
||||
Predicate<FactionProductData> isAvailable, ref int credits, List<string> failures)
|
||||
Predicate<FactionProductData> isAvailable, List<string> failures)
|
||||
{
|
||||
var cache = itemManager.ItemData;
|
||||
var products = cache.GetAll<FactionProductData>().OrderBy(p => cache.RefOf(p).Key.Value, StringComparer.Ordinal).ToArray();
|
||||
|
|
@ -104,8 +115,6 @@ public static class Loadouts
|
|||
else if (cache.Get(loadout.Slots[index].Design) is { } design && !design.Behaviors.Any(behavior => behavior is WeaponData))
|
||||
failures.Add($"weapon group: {Cell(loadout.Slots[index])}: {design.Name} is not a weapon");
|
||||
}
|
||||
var price = Price(itemManager, loadout);
|
||||
if (credits < price) failures.Add($"cannot afford {price:n0} credits");
|
||||
if (failures.Count > reported) return null;
|
||||
|
||||
var hull = (EquippableItem) itemManager.CreateInstance(hullProduct);
|
||||
|
|
@ -136,17 +145,8 @@ public static class Loadouts
|
|||
built[g] = (items.Select(item => item.GetBehavior<Weapon>()).ToList(), items);
|
||||
}
|
||||
ship.WeaponGroups = built;
|
||||
credits -= (int) price;
|
||||
return ship;
|
||||
}
|
||||
|
||||
// The hull design's price plus each slot design's price. Summed as long so no loadout wraps to a negative price;
|
||||
// a price beyond any int credit balance is simply unaffordable.
|
||||
public static long Price(ItemManager itemManager, Loadout loadout)
|
||||
{
|
||||
var cache = itemManager.ItemData;
|
||||
return (long) (cache.Get(loadout.Hull)?.Price ?? 0) + loadout.Slots.Sum(slot => (long) (cache.Get(slot.Design)?.Price ?? 0));
|
||||
}
|
||||
|
||||
private static string Cell(LoadoutSlot slot) => $"slot {slot.Position.x},{slot.Position.y}";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue