2026-09-14 22:27:03 +02:00
|
|
|
using System;
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using GameCult.Caching;
|
|
|
|
|
using MessagePack;
|
|
|
|
|
using CultMath;
|
|
|
|
|
|
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>
2026-09-14 23:09:43 +02:00
|
|
|
// 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.
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
[CultDocument("aetheria.loadout", "1"), MessagePackObject]
|
|
|
|
|
public class Loadout
|
|
|
|
|
{
|
|
|
|
|
[CultName, Key(0)] public string Name;
|
|
|
|
|
[Key(1)] public CultRecordRef<HullData> Hull;
|
|
|
|
|
[Key(2)] public List<LoadoutSlot> Slots = new List<LoadoutSlot>();
|
|
|
|
|
[Key(3)] public int[][] WeaponGroups; // indices into Slots
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MessagePackObject]
|
|
|
|
|
public class LoadoutSlot
|
|
|
|
|
{
|
|
|
|
|
[Key(0)] public int2 Position; // hull cell passed to Entity.TryEquip(item, int2)
|
|
|
|
|
[Key(1)] public ItemRotation Rotation;
|
|
|
|
|
[Key(2)] public CultRecordRef<EquippableItemData> Design;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The owner of loadouts: capture from a live ship, the only writer, and the only builder of a ship from a loadout.
|
|
|
|
|
public static class Loadouts
|
|
|
|
|
{
|
|
|
|
|
// Records the hull design and one slot per equipped item (gear, then cargo bays, then docking bays) with its cell,
|
|
|
|
|
// rotation and design. Entity.Equipment also holds the hull's own unit; that is Hull, not a slot. Nothing about the
|
|
|
|
|
// units themselves: no product, quality, ingredients, cargo or state.
|
|
|
|
|
public static Loadout Capture(ItemManager itemManager, Entity entity, string name)
|
|
|
|
|
{
|
|
|
|
|
var cache = itemManager.ItemData;
|
|
|
|
|
var items = entity.Equipment
|
|
|
|
|
.Concat<EquippedItem>(entity.CargoBays)
|
|
|
|
|
.Concat(entity.DockingBays)
|
|
|
|
|
.Where(item => item.EquippableItem != entity.Hull)
|
|
|
|
|
.Distinct()
|
|
|
|
|
.ToList();
|
|
|
|
|
return new Loadout
|
|
|
|
|
{
|
|
|
|
|
Name = name,
|
|
|
|
|
Hull = cache.RefOf((HullData) itemManager.GetData(entity.Hull)),
|
|
|
|
|
Slots = items.Select(item => new LoadoutSlot
|
|
|
|
|
{
|
|
|
|
|
Position = item.Position,
|
|
|
|
|
Rotation = item.EquippableItem.Rotation,
|
|
|
|
|
Design = cache.RefOf(itemManager.GetData(item.EquippableItem))
|
|
|
|
|
}).ToList(),
|
|
|
|
|
WeaponGroups = entity.WeaponGroups
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
Materialize now reports a weapon group index whose slot design has no
WeaponData, and a loadout with more groups than WeaponGroupCount. The ship
always gets exactly WeaponGroupCount groups, and a shorter loadout is padded
with empty groups. Both checks run before anything is built.
Price is summed as long, so the hull add can no longer wrap to a negative
price that pays the player. A price beyond int credits is simply unaffordable.
Deleted Capture's index >= 0 filter and its test. Every production writer of
WeaponGroups items (WeaponGroupAssignment, GenerateWeaponGroups, Unpack replay
of Pack, Materialize) groups only weapons, and TryUnequip removes weapons from
groups, so nothing produces an ungrouped index any more.
The Materialize comment now says a failed fit may already have advanced
itemManager.Random.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:51:37 +02:00
|
|
|
.Select(group => group.items.Select(item => items.IndexOf(item)).ToArray())
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
.ToArray()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-09-14 23:09:43 +02:00
|
|
|
// 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)
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
{
|
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>
2026-09-14 23:09:43 +02:00
|
|
|
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);
|
|
|
|
|
});
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
}
|
|
|
|
|
|
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>
2026-09-14 23:09:43 +02:00
|
|
|
// 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.
|
2026-09-14 22:27:03 +02:00
|
|
|
public static Ship Materialize(ItemManager itemManager, Zone liveZone, Loadout loadout,
|
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>
2026-09-14 23:09:43 +02:00
|
|
|
Predicate<FactionProductData> isAvailable, List<string> failures)
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
{
|
|
|
|
|
var cache = itemManager.ItemData;
|
2026-09-14 22:27:03 +02:00
|
|
|
var products = cache.GetAll<FactionProductData>().OrderBy(p => cache.RefOf(p).Key.Value, StringComparer.Ordinal).ToArray();
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
var reported = failures.Count;
|
|
|
|
|
|
|
|
|
|
FactionProductData Resolve<T>(CultRecordRef<T> design, string where) where T : EquippableItemData
|
|
|
|
|
{
|
|
|
|
|
var data = cache.Get(design);
|
|
|
|
|
if (data == null)
|
|
|
|
|
{
|
|
|
|
|
failures.Add($"{where}: design {design.Key} is not in the catalog");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 22:27:03 +02:00
|
|
|
var product = products.FirstOrDefault(p => p.Design.Key.Equals(design.Key) && isAvailable(p));
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
if (product == null) failures.Add($"{where}: no available product of {data.Name}");
|
|
|
|
|
return product;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hullProduct = Resolve(loadout.Hull, "hull");
|
|
|
|
|
var slotProducts = loadout.Slots.Select(slot => Resolve(slot.Design, Cell(slot))).ToArray();
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
Materialize now reports a weapon group index whose slot design has no
WeaponData, and a loadout with more groups than WeaponGroupCount. The ship
always gets exactly WeaponGroupCount groups, and a shorter loadout is padded
with empty groups. Both checks run before anything is built.
Price is summed as long, so the hull add can no longer wrap to a negative
price that pays the player. A price beyond int credits is simply unaffordable.
Deleted Capture's index >= 0 filter and its test. Every production writer of
WeaponGroups items (WeaponGroupAssignment, GenerateWeaponGroups, Unpack replay
of Pack, Materialize) groups only weapons, and TryUnequip removes weapons from
groups, so nothing produces an ungrouped index any more.
The Materialize comment now says a failed fit may already have advanced
itemManager.Random.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:51:37 +02:00
|
|
|
var groups = loadout.WeaponGroups ?? new int[0][];
|
|
|
|
|
var groupCount = itemManager.GameplaySettings.WeaponGroupCount;
|
|
|
|
|
if (groups.Length > groupCount) failures.Add($"weapon groups: {groups.Length} groups, at most {groupCount}");
|
|
|
|
|
foreach (var index in groups.SelectMany(group => group))
|
|
|
|
|
{
|
2026-09-14 22:28:24 +02:00
|
|
|
if (index < 0 || index >= loadout.Slots.Count) failures.Add($"weapon group: no slot {index}");
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
Materialize now reports a weapon group index whose slot design has no
WeaponData, and a loadout with more groups than WeaponGroupCount. The ship
always gets exactly WeaponGroupCount groups, and a shorter loadout is padded
with empty groups. Both checks run before anything is built.
Price is summed as long, so the hull add can no longer wrap to a negative
price that pays the player. A price beyond int credits is simply unaffordable.
Deleted Capture's index >= 0 filter and its test. Every production writer of
WeaponGroups items (WeaponGroupAssignment, GenerateWeaponGroups, Unpack replay
of Pack, Materialize) groups only weapons, and TryUnequip removes weapons from
groups, so nothing produces an ungrouped index any more.
The Materialize comment now says a failed fit may already have advanced
itemManager.Random.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:51:37 +02:00
|
|
|
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");
|
|
|
|
|
}
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
if (failures.Count > reported) return null;
|
|
|
|
|
|
|
|
|
|
var hull = (EquippableItem) itemManager.CreateInstance(hullProduct);
|
|
|
|
|
var ship = new Ship(itemManager, liveZone, hull, itemManager.GameplaySettings.DefaultEntitySettings);
|
|
|
|
|
var equipped = new EquippedItem[loadout.Slots.Count];
|
|
|
|
|
for (var i = 0; i < loadout.Slots.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var slot = loadout.Slots[i];
|
|
|
|
|
var unit = (EquippableItem) itemManager.CreateInstance(slotProducts[i]);
|
|
|
|
|
unit.Rotation = slot.Rotation;
|
|
|
|
|
if (!ship.TryEquip(unit, slot.Position))
|
|
|
|
|
{
|
|
|
|
|
failures.Add($"{Cell(slot)}: {cache.Get(slot.Design).Name} does not fit");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
equipped[i] = ship.Equipment
|
|
|
|
|
.Concat<EquippedItem>(ship.CargoBays)
|
|
|
|
|
.Concat(ship.DockingBays)
|
|
|
|
|
.First(item => item.EquippableItem == unit);
|
|
|
|
|
}
|
|
|
|
|
if (failures.Count > reported) return null;
|
|
|
|
|
|
Refuse non-weapon groups, fix the group count, and widen loadout price (Cut 10 fix)
Materialize now reports a weapon group index whose slot design has no
WeaponData, and a loadout with more groups than WeaponGroupCount. The ship
always gets exactly WeaponGroupCount groups, and a shorter loadout is padded
with empty groups. Both checks run before anything is built.
Price is summed as long, so the hull add can no longer wrap to a negative
price that pays the player. A price beyond int credits is simply unaffordable.
Deleted Capture's index >= 0 filter and its test. Every production writer of
WeaponGroups items (WeaponGroupAssignment, GenerateWeaponGroups, Unpack replay
of Pack, Materialize) groups only weapons, and TryUnequip removes weapons from
groups, so nothing produces an ungrouped index any more.
The Materialize comment now says a failed fit may already have advanced
itemManager.Random.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:51:37 +02:00
|
|
|
var built = new (List<Weapon> weapons, List<EquippedItem> items)[groupCount];
|
|
|
|
|
for (var g = 0; g < groupCount; g++)
|
|
|
|
|
{
|
|
|
|
|
var items = g < groups.Length ? groups[g].Select(i => equipped[i]).ToList() : new List<EquippedItem>();
|
|
|
|
|
built[g] = (items.Select(item => item.GetBehavior<Weapon>()).ToList(), items);
|
|
|
|
|
}
|
|
|
|
|
ship.WeaponGroups = built;
|
Make loadouts player-store design documents built all-or-nothing (Cut 10)
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:06:06 +02:00
|
|
|
return ship;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string Cell(LoadoutSlot slot) => $"slot {slot.Position.x},{slot.Position.y}";
|
|
|
|
|
}
|