2026-08-10 14:57:50 +01:00
|
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
2020-12-22 03:13:43 +00:00
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
Replace the Database Tools with the Studio and cut Unity over
Cut 8b. The Unity project compiles with 0 errors in 6000.3.24f1 batchmode.
Packages: manifest.json pins org.gamecult.cultlib at cultlib-unity-v1.0.57
and org.gamecult.caching.unity at caching-unity-v1.2.0, next to the 8a
CultMath tag. Aetheria.Shared.Unity references GameCult.CultLib and keeps
noEngineReferences.
The Database Tools (Assets/Scripts/CultCache/Editor) are deleted in this
commit, alongside their five ported drawers in
Assets/Scripts/Editor/CultCacheDrawers.cs, each an
[CultInspectorDrawer] ICultInspectorDrawer:
- InspectableType: popup of the attribute type's [Union] member names,
writing the string.
- InspectableColor: float3 and float4 through ColorField.
- InspectableAnimationCurve: float4[] and BezierCurve through CurveField.
- InspectableTemperature: Kelvin, Celsius and Fahrenheit fields over one
value.
- InspectableSchematicShape: size and cell toggles. The old drawer also
drew the item's schematic texture and tinted hull hardpoints, which need
the parent document. The tagged Studio hands a drawer only the value, so
those wait on the Studio exposing the open record.
Unity call sites:
- ActionGameManager opens the catalog through AetheriaStores; Cut 10 adds
the run and player stores.
- Refs resolve through ItemManager.GetData or Get(ref); orbit, planet and
belt maps key by CultRecordKey; identity is compared by reference or key.
- The PlayerSettings.msgpack reader and writer, which Cut 10 deletes, pass
CultDocumentMessagePackSerialization.OptionsFor, since the embedded run
reaches CultMath values and refs. Keyboard layouts need no resolver and
keep MessagePack's defaults until Cut 10.
- The input map uses the nested action to binding shape.
- PropertiesPanel reads CultInspectorRangeAttribute.
- NameTools loses its legacy name-file writer.
- Slime compares serialized settings instead of the deleted SHA1 helper.
- FieldDriver drops InspectorHeader.
- .gitignore covers the test project's build output.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6), Unity
batchmode (0 errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:15:20 +02:00
|
|
|
|
using GameCult.Caching;
|
2020-12-22 03:13:43 +00:00
|
|
|
|
using System;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
2020-11-29 22:50:12 +00:00
|
|
|
|
using Cinemachine;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using Ink;
|
|
|
|
|
|
using Ink.Runtime;
|
|
|
|
|
|
using MessagePack;
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using TMPro;
|
|
|
|
|
|
using UniRx;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
using UnityEngine;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using UnityEngine.InputSystem;
|
|
|
|
|
|
using UnityEngine.Rendering.PostProcessing;
|
2021-03-15 11:40:46 +00:00
|
|
|
|
using UnityEngine.Serialization;
|
2020-12-14 02:56:35 +00:00
|
|
|
|
using UnityEngine.UI;
|
2026-09-14 17:39:12 +02:00
|
|
|
|
using CultMath;
|
Convert engine types through CultMath.UnityBridge
CultMath has no implicit conversions to or from Vector*, Quaternion or
Color, so Assembly-CSharp converts explicitly with ToCultMath/ToUnity at
each engine boundary: FieldDriver, GuidedProjectile(Manager),
EntityInstance, ActionGameManager, HullCollider, GridObject, ShipInstance,
ShieldManager, ItemPickup, FieldTester, PlaceUIElementWorldspace,
MapMenuInput, MapRenderer, SectorRenderer and ZoneRenderer. CultMath math
calls that were handed Unity vectors now take converted values;
Vector3.Scale in FieldDriver becomes a float3 product, and ItemPickup's
Vector3.Dot on float3s becomes dot. packages-lock.json records the
cultmath git package at f0605367 and demotes com.unity.mathematics to a
transitive dependency.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:47:33 +02:00
|
|
|
|
using CultMath.UnityBridge;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using UnityEngine.EventSystems;
|
2026-09-14 17:39:12 +02:00
|
|
|
|
using static CultMath.math;
|
|
|
|
|
|
using float2 = CultMath.float2;
|
|
|
|
|
|
using float3 = CultMath.float3;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using Path = System.IO.Path;
|
2026-09-14 17:39:12 +02:00
|
|
|
|
using quaternion = CultMath.quaternion;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
using Random = UnityEngine.Random;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
|
|
|
|
|
public class ActionGameManager : MonoBehaviour
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
// Always check for null if accessing from anywhere that might not be in-game (e.g. menu UI)
|
|
|
|
|
|
public static ActionGameManager Instance { get; private set; }
|
|
|
|
|
|
private static DirectoryInfo _gameDataDirectory;
|
|
|
|
|
|
public static DirectoryInfo GameDataDirectory
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _gameDataDirectory ??= new DirectoryInfo(Application.dataPath).Parent.CreateSubdirectory("GameData");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static CultCache _cultCache;
|
|
|
|
|
|
|
Ship S2 fixes: structural anchors are not mounts, mod presets refused, one bad mod never bricks the boot
- Validate: a hardpoint's anchor role is fixed by its type (thruster-emitter, radiator-mesh, else articulation); ShipModPlan's
duplicate role check is deleted.
- Loadouts.Commit refuses a preset naming a mod design (mod-hull:/mod-ship:), so the shipped catalog never references a mod.
- Compose quarantines each package: a bad package is excluded and named, a collision excludes every package involved; the
game boots on the rest (or the shipped catalog) and the main menu lists the exclusions.
- RequireDesigns refuses a save with a null item or lot instead of throwing NullReferenceException.
- Tests: per structural role, muzzle id ties, every slot/zone/lot in RequireDesigns, listing order, exclusion cases.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:30:52 +02:00
|
|
|
|
// Mod packages left out of this boot (a bad package, or a name collision), for the main menu to name.
|
|
|
|
|
|
public static ShipModCatalog.Exclusion[] ModExclusions { get; private set; } = Array.Empty<ShipModCatalog.Exclusion>();
|
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:32:52 +02:00
|
|
|
|
private static string CatalogPath => Path.Combine(GameDataDirectory.FullName, "Aetheria.cc");
|
2026-10-16 21:48:57 +02:00
|
|
|
|
public static string ModsPath => Path.Combine(GameDataDirectory.FullName, "Mods");
|
2026-09-30 06:43:52 +02:00
|
|
|
|
// Disposable: composed at every boot from the shipped catalog and the mod packages, never authored.
|
|
|
|
|
|
private static string DerivedCatalogPath => Path.Combine(Application.persistentDataPath, "Aetheria.modded.cc");
|
Capture presets through their own cache; keep the catalog read-only
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:32:52 +02:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public static CultCache CultCache
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
2026-06-25 18:09:33 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (_cultCache != null) return _cultCache;
|
|
|
|
|
|
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
// All three stores attach once and stay attached until the process exits. The run lifecycle is record-level
|
|
|
|
|
|
// inside this cache; reopening would replace the catalog instances the galaxy and live entities hold.
|
2026-09-30 06:43:52 +02:00
|
|
|
|
// The catalog is read-only in every build; capturepreset writes through its own cache (Loadouts.Commit) to the
|
|
|
|
|
|
// shipped file. With mod packages installed the game reads the derived catalog instead.
|
Ship S2 fixes: structural anchors are not mounts, mod presets refused, one bad mod never bricks the boot
- Validate: a hardpoint's anchor role is fixed by its type (thruster-emitter, radiator-mesh, else articulation); ShipModPlan's
duplicate role check is deleted.
- Loadouts.Commit refuses a preset naming a mod design (mod-hull:/mod-ship:), so the shipped catalog never references a mod.
- Compose quarantines each package: a bad package is excluded and named, a collision excludes every package involved; the
game boots on the rest (or the shipped catalog) and the main menu lists the exclusions.
- RequireDesigns refuses a save with a null item or lot instead of throwing NullReferenceException.
- Tests: per structural role, muzzle id ties, every slot/zone/lot in RequireDesigns, listing order, exclusion cases.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:30:52 +02:00
|
|
|
|
var (catalog, excluded) = ShipModCatalog.ResolveCatalog(CatalogPath, DerivedCatalogPath, ModsPath);
|
|
|
|
|
|
ModExclusions = excluded;
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
_cultCache = AetheriaStores.Open(
|
Ship S2 fixes: structural anchors are not mounts, mod presets refused, one bad mod never bricks the boot
- Validate: a hardpoint's anchor role is fixed by its type (thruster-emitter, radiator-mesh, else articulation); ShipModPlan's
duplicate role check is deleted.
- Loadouts.Commit refuses a preset naming a mod design (mod-hull:/mod-ship:), so the shipped catalog never references a mod.
- Compose quarantines each package: a bad package is excluded and named, a collision excludes every package involved; the
game boots on the rest (or the shipped catalog) and the main menu lists the exclusions.
- RequireDesigns refuses a save with a null item or lot instead of throwing NullReferenceException.
- Tests: per structural role, muzzle id ties, every slot/zone/lot in RequireDesigns, listing order, exclusion cases.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:30:52 +02:00
|
|
|
|
catalog,
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
runPath: Path.Combine(GameDataDirectory.FullName, "run.cc"),
|
Capture presets through their own cache; keep the catalog read-only
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:32:52 +02:00
|
|
|
|
playerPath: Path.Combine(GameDataDirectory.FullName, "player.cc"));
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
|
2026-09-30 06:43:52 +02:00
|
|
|
|
// Mod ship prototypes import asynchronously from here; entering a game waits on ShipModShips.Loading.
|
|
|
|
|
|
ShipModShips.Preload(_cultCache, ModsPath);
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return _cultCache;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
// The only creator of PlayerSettings: an absent global means first launch, and the defaults are committed.
|
|
|
|
|
|
public static PlayerSettings PlayerSettings
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = CultCache.GetGlobal<PlayerSettings>();
|
|
|
|
|
|
if (settings != null) return settings;
|
|
|
|
|
|
settings = GetDefaultPlayerSettings();
|
|
|
|
|
|
CultCache.Commit(batch => batch.Upsert(settings));
|
|
|
|
|
|
return settings;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void SavePlayerSettings()
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = PlayerSettings;
|
|
|
|
|
|
CultCache.Commit(batch => batch.Upsert(settings));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private static PlayerSettings GetDefaultPlayerSettings()
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = new PlayerSettings();
|
|
|
|
|
|
settings.Name = Environment.UserName;
|
|
|
|
|
|
settings.InputSettings.ActionBarInputs.Add("<Keyboard>/leftShift");
|
|
|
|
|
|
settings.InputSettings.ActionBarInputs.Add("<Mouse>/leftButton");
|
|
|
|
|
|
settings.InputSettings.ActionBarInputs.Add("<Mouse>/rightButton");
|
|
|
|
|
|
settings.InputSettings.ActionBarInputs.Add("<Mouse>/middleButton");
|
|
|
|
|
|
for (int i = 1; i < 6; i++) settings.InputSettings.ActionBarInputs.Add($"<Keyboard>/{i}");
|
|
|
|
|
|
return settings;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static Galaxy CurrentGalaxy;
|
Every new run is a scenario: the script harness, the tutorial and main galaxies as scenarios, the New Game submenu and six test arenas
A Scenario is an abstract C# class compiled with the game: Generate picks the galaxy through GalaxyStage (Main or
Prelude, at the scenario's seed, the background noise drawn from the same seed), Stage places the run's first
entities through ScenarioStage (Fit, Bare, Generated, Preset, Player, Place, Cargo), which collects named failures
and admits nothing. RunStart generates the galaxy, the arena under Ambient and admits everything through Zone.Admit,
all or nothing. The aetheria.scenario record, ScenarioShip, ScenarioEntity, RunStart.Check and the null-scenario
branch are cut.
New Game opens a submenu: Tutorial Galaxy and Main Galaxy, then (editor and development builds) Djinni Shakedown,
Starved Reactor, Arcs, Duel, Launcher Angles and Long Haul, each with its brief. MainMenu.Launch is the one way into a
new run; ActionGameManager.PendingScenario carries the scenario to StartGame once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:33:55 +02:00
|
|
|
|
// The scenario a new run starts from: MainMenu.Launch sets it, and StartGame's new-run branch consumes it once.
|
|
|
|
|
|
public static Scenario PendingScenario;
|
2021-04-18 05:13:20 +01:00
|
|
|
|
|
2021-03-10 09:18:07 +00:00
|
|
|
|
public GameSettings Settings;
|
2021-03-18 22:48:33 +00:00
|
|
|
|
//public string StarterShipTemplate = "Longinus";
|
2026-10-06 19:30:34 +02:00
|
|
|
|
// The run's credits: seeded from the scenario stage on a new game and from SavedGame on Continue, written by SaveRun.
|
|
|
|
|
|
[NonSerialized] public int Credits;
|
2026-10-13 07:13:55 +02:00
|
|
|
|
// Presentation scale: the player's own Visibility at which the target panel's VisibilityToTargetFill reads full.
|
|
|
|
|
|
public float OwnSignatureFullScale = 10;
|
|
|
|
|
|
// The music last asked for: Update decides it each frame from what the player's own instruments hold and calls
|
|
|
|
|
|
// PlayMusic only when it changes.
|
|
|
|
|
|
private MusicType? _music;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2021-03-10 09:18:07 +00:00
|
|
|
|
[Header("Postprocessing")]
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public float DeathPostTransitionTime;
|
|
|
|
|
|
public PostProcessVolume DeathPost;
|
|
|
|
|
|
public PostProcessVolume HeatstrokePost;
|
|
|
|
|
|
public PostProcessVolume HypothermiaPost;
|
|
|
|
|
|
public PostProcessVolume SevereHeatstrokePost;
|
|
|
|
|
|
public PostProcessVolume SevereHypothermiaPost;
|
2021-03-20 04:20:17 +00:00
|
|
|
|
|
2021-03-10 09:18:07 +00:00
|
|
|
|
[Header("Scene Links")]
|
2021-05-08 07:35:16 +01:00
|
|
|
|
public GameObject UiRoot;
|
|
|
|
|
|
public GameObject HelpScreen;
|
|
|
|
|
|
public InputDisplayLayout InputDisplayLayout;
|
2021-04-18 05:13:20 +01:00
|
|
|
|
public Transform ActionBar;
|
|
|
|
|
|
public ActionBarSlot ActionBarSlot;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public Transform EffectManagerParent;
|
2021-04-09 14:01:34 +01:00
|
|
|
|
public ZoneRenderer ZoneRenderer;
|
2020-12-22 03:13:43 +00:00
|
|
|
|
public CinemachineVirtualCamera DockCamera;
|
2020-11-29 22:50:12 +00:00
|
|
|
|
public CinemachineVirtualCamera FollowCamera;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public CinemachineVirtualCamera WormholeCamera;
|
|
|
|
|
|
//public SectorRenderer SectorRenderer;
|
|
|
|
|
|
public SectorMap SectorMap;
|
|
|
|
|
|
|
2021-04-29 00:21:33 +01:00
|
|
|
|
[Header("Menu UI")]
|
|
|
|
|
|
public MainMenu MainMenu;
|
|
|
|
|
|
public MenuPanel Menu;
|
|
|
|
|
|
public MapRenderer MenuMap;
|
|
|
|
|
|
public TradeMenu TradeMenu;
|
2020-12-14 02:56:35 +00:00
|
|
|
|
public InventoryMenu Inventory;
|
2020-12-27 22:18:22 +00:00
|
|
|
|
public InventoryPanel ShipPanel;
|
2021-01-11 02:56:37 +00:00
|
|
|
|
public InventoryPanel TargetShipPanel;
|
2021-03-27 10:03:24 +00:00
|
|
|
|
public ConfirmationDialog Dialog;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public ContextMenu Context;
|
|
|
|
|
|
public DropdownMenu Dropdown;
|
|
|
|
|
|
|
2021-04-29 00:21:33 +01:00
|
|
|
|
[Header("Gameplay UI")]
|
|
|
|
|
|
public CanvasGroup GameplayUI;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public EventLog EventLog;
|
2021-04-29 00:21:33 +01:00
|
|
|
|
public Prototype HostileTargetIndicator;
|
2021-05-11 04:45:42 +01:00
|
|
|
|
public Prototype FriendlyTargetIndicator;
|
2021-04-29 00:21:33 +01:00
|
|
|
|
public PlaceUIElementWorldspace ViewDot;
|
|
|
|
|
|
public Prototype LockIndicator;
|
2026-10-10 19:25:22 +02:00
|
|
|
|
// Presentation tuning: how far from the ship, toward the locker, the incoming-lock indicator sits.
|
|
|
|
|
|
public float IncomingLockIndicatorDistance = 30;
|
2026-10-14 13:54:43 +02:00
|
|
|
|
// Presentation tuning: how far from the ship, toward a remembered hostile's bearing, its marker sits; how long a
|
|
|
|
|
|
// sweep flash takes to fade.
|
|
|
|
|
|
public float BearingMarkerDistance = 40;
|
|
|
|
|
|
public float SearchFlashSeconds = .6f;
|
2021-04-29 00:21:33 +01:00
|
|
|
|
public PlaceUIElementWorldspace[] Crosshairs;
|
|
|
|
|
|
public GameObject HitMarker;
|
|
|
|
|
|
public float HitMarkerDuration;
|
|
|
|
|
|
public SchematicDisplay SchematicDisplay;
|
|
|
|
|
|
public SchematicDisplay TargetSchematicDisplay;
|
2026-09-20 21:58:44 +02:00
|
|
|
|
public TextMeshProUGUI DebugInfoText;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2021-04-29 00:21:33 +01:00
|
|
|
|
[Header("Target Indicator")]
|
|
|
|
|
|
public PlaceUIElementWorldspace TargetIndicator;
|
|
|
|
|
|
public Image TargetHitpointsFill;
|
|
|
|
|
|
public Image TargetVisibilityFill;
|
|
|
|
|
|
public Image VisibilityToTargetFill;
|
|
|
|
|
|
public Image TargetShieldsBackground;
|
|
|
|
|
|
public Image TargetShieldsFill;
|
|
|
|
|
|
public Image TargetShieldsIcon;
|
|
|
|
|
|
public Color ShieldColor;
|
|
|
|
|
|
public Color NoShieldColor;
|
|
|
|
|
|
public Sprite ShieldIcon;
|
|
|
|
|
|
public Sprite NoShieldIcon;
|
2021-03-27 10:03:24 +00:00
|
|
|
|
|
2020-12-02 16:42:09 +00:00
|
|
|
|
//public PlayerInput Input;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2020-12-22 03:13:43 +00:00
|
|
|
|
// private CinemachineFramingTransposer _transposer;
|
|
|
|
|
|
// private CinemachineComposer _composer;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
private bool _paused;
|
|
|
|
|
|
private float _time;
|
|
|
|
|
|
private int _zoomLevelIndex;
|
2021-03-10 09:18:07 +00:00
|
|
|
|
private Entity _currentEntity;
|
2021-01-14 23:47:08 +00:00
|
|
|
|
|
2021-01-05 02:56:17 +00:00
|
|
|
|
// private ShipInput _shipInput;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private float2 _entityYawPitch;
|
2020-12-22 03:13:43 +00:00
|
|
|
|
private float3 _viewDirection;
|
2026-10-07 12:33:22 +02:00
|
|
|
|
// The player's face-the-aim state (Left Shift held, Caps Lock latched); Ship.Turn is its only output.
|
|
|
|
|
|
private readonly HelmInput _helm = new HelmInput();
|
|
|
|
|
|
private TextMeshProUGUI _faceAimMark;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private (HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)[] _articulationGroups;
|
2026-10-14 21:11:37 +02:00
|
|
|
|
// The outgoing progress indicators on the player's target: one per lock weapon (its Lock, shown only on a hostile target)
|
|
|
|
|
|
// and one per scanner (its scan Progress, shown on any target: a scan reads no stance).
|
|
|
|
|
|
private (Func<float> progress, bool hostileOnly, PlaceUIElementWorldspace indicator, Rotate spin)[] _progressIndicators;
|
2026-10-10 19:25:22 +02:00
|
|
|
|
private (PlaceUIElementWorldspace indicator, Rotate spin) _incomingLockIndicator;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private Dictionary<Entity, VisibleTargetIndicator> _visibleHostileIndicators = new Dictionary<Entity, VisibleTargetIndicator>();
|
|
|
|
|
|
private Dictionary<Entity, VisibleTargetIndicator> _visibleFriendlyIndicators = new Dictionary<Entity, VisibleTargetIndicator>();
|
2026-10-14 13:54:43 +02:00
|
|
|
|
// Display-only: a marker per remembered hostile (shares HostileTargetIndicator's pool), the Search warnings the last
|
|
|
|
|
|
// ticks delivered (bearing, zone time; a per-tick buffer), and the sweep flashes drawn from them.
|
|
|
|
|
|
private readonly Dictionary<Entity, VisibleTargetIndicator> _bearingMarkers = new Dictionary<Entity, VisibleTargetIndicator>();
|
|
|
|
|
|
private readonly List<(float2 bearing, float time)> _pendingSweeps = new List<(float2, float)>();
|
|
|
|
|
|
private readonly List<(PlaceUIElementWorldspace indicator, Image image, float2 bearing, float time)> _sweepFlashes =
|
|
|
|
|
|
new List<(PlaceUIElementWorldspace, Image, float2, float)>();
|
|
|
|
|
|
private readonly List<Entity> _markerScratch = new List<Entity>();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private List<IDisposable> _shipSubscriptions = new List<IDisposable>();
|
|
|
|
|
|
private List<IDisposable> _targetSubscriptions = new List<IDisposable>();
|
2026-09-20 21:58:44 +02:00
|
|
|
|
private readonly Dictionary<EquippedItem, ShotOutcome> _debugLastShots = new Dictionary<EquippedItem, ShotOutcome>();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private float _severeHeatstrokePhase;
|
|
|
|
|
|
private bool _uiHidden;
|
|
|
|
|
|
private bool _menuShown;
|
|
|
|
|
|
private List<ActionBarSlot> _actionBarSlots = new List<ActionBarSlot>();
|
|
|
|
|
|
private List<InputAction> _actionBarActions = new List<InputAction>();
|
|
|
|
|
|
private float _hitMarkerTime;
|
|
|
|
|
|
|
2021-03-27 10:03:24 +00:00
|
|
|
|
public AetheriaInput Input { get; private set; }
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public EquippedDockingBay DockingBay { get; private set; }
|
|
|
|
|
|
public Entity DockedEntity { get; private set; }
|
|
|
|
|
|
//if better place, please move
|
|
|
|
|
|
public Entity TowingStation { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
public ZoneEnvironment CurrentEnvironment
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
return Settings.DefaultEnvironment;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-07-21 21:40:44 +01:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public Entity CurrentEntity
|
2021-03-06 02:42:19 +00:00
|
|
|
|
{
|
2021-03-10 09:18:07 +00:00
|
|
|
|
get => _currentEntity;
|
|
|
|
|
|
set => _currentEntity = value;
|
2021-03-06 02:42:19 +00:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
public ItemManager ItemManager { get; private set; }
|
|
|
|
|
|
public Zone Zone { get; private set; }
|
2026-10-08 23:39:30 +02:00
|
|
|
|
public static SimClock Clock { get; } = new SimClock(1f / 60f);
|
|
|
|
|
|
private bool _zoneEntered;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
private readonly (float2 direction, string name)[] _directions = {
|
|
|
|
|
|
(float2(0, 1), "Front"),
|
|
|
|
|
|
(float2(1, 0), "Right"),
|
|
|
|
|
|
(float2(-1, 0), "Left"),
|
|
|
|
|
|
(float2(0, -1), "Rear")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
public DragObject DragObject { get; private set; }
|
|
|
|
|
|
private Func<DragObject, bool> _endDragCallback;
|
|
|
|
|
|
|
|
|
|
|
|
private List<Story> _stories = new List<Story>();
|
|
|
|
|
|
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
private void OnApplicationQuit()
|
|
|
|
|
|
{
|
|
|
|
|
|
SaveRun();
|
|
|
|
|
|
SavePlayerSettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Wormhole arrival and quit save the run; a dead run has no galaxy, so it is not saved.
|
|
|
|
|
|
public void SaveRun()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CurrentGalaxy != null)
|
|
|
|
|
|
{
|
2026-10-03 20:26:30 +02:00
|
|
|
|
var (game, zones) = RunSave.Capture(CultCache, CurrentGalaxy, Zone, DockedEntity ?? CurrentEntity,
|
2026-10-06 19:30:34 +02:00
|
|
|
|
_actionBarSlots.Select(s => s.Save()).ToArray(), Credits);
|
2026-09-17 04:16:23 +02:00
|
|
|
|
RunSave.Commit(CultCache, game, zones, ItemManager.Lots);
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
private void OnDisable()
|
2026-06-21 08:56:07 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Input.Dispose();
|
|
|
|
|
|
ConsoleController.ClearCommands();
|
|
|
|
|
|
EntityInstance.ClearWeaponManagers();
|
2026-06-21 08:56:07 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
void Start()
|
2026-06-16 20:42:42 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Instance = this;
|
|
|
|
|
|
EntityInstance.EffectManagerParent = EffectManagerParent;
|
|
|
|
|
|
ConsoleController.MessageReceiver = this;
|
|
|
|
|
|
|
A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:03:50 +02:00
|
|
|
|
// A new run mints into an empty ledger; the saved run's stays in the store until the new run has started.
|
|
|
|
|
|
ItemManager = new ItemManager(CultCache, PendingScenario != null ? new ProvenanceLedger() : RunSave.Lots(CultCache),
|
|
|
|
|
|
Settings.GameplaySettings, Debug.Log);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
ZoneRenderer.ItemManager = ItemManager;
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
// TODO: Process Stories
|
2026-06-16 20:42:42 +01:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
#region Input Handling
|
|
|
|
|
|
|
|
|
|
|
|
Input = new AetheriaInput();
|
Replace the Database Tools with the Studio and cut Unity over
Cut 8b. The Unity project compiles with 0 errors in 6000.3.24f1 batchmode.
Packages: manifest.json pins org.gamecult.cultlib at cultlib-unity-v1.0.57
and org.gamecult.caching.unity at caching-unity-v1.2.0, next to the 8a
CultMath tag. Aetheria.Shared.Unity references GameCult.CultLib and keeps
noEngineReferences.
The Database Tools (Assets/Scripts/CultCache/Editor) are deleted in this
commit, alongside their five ported drawers in
Assets/Scripts/Editor/CultCacheDrawers.cs, each an
[CultInspectorDrawer] ICultInspectorDrawer:
- InspectableType: popup of the attribute type's [Union] member names,
writing the string.
- InspectableColor: float3 and float4 through ColorField.
- InspectableAnimationCurve: float4[] and BezierCurve through CurveField.
- InspectableTemperature: Kelvin, Celsius and Fahrenheit fields over one
value.
- InspectableSchematicShape: size and cell toggles. The old drawer also
drew the item's schematic texture and tinted hull hardpoints, which need
the parent document. The tagged Studio hands a drawer only the value, so
those wait on the Studio exposing the open record.
Unity call sites:
- ActionGameManager opens the catalog through AetheriaStores; Cut 10 adds
the run and player stores.
- Refs resolve through ItemManager.GetData or Get(ref); orbit, planet and
belt maps key by CultRecordKey; identity is compared by reference or key.
- The PlayerSettings.msgpack reader and writer, which Cut 10 deletes, pass
CultDocumentMessagePackSerialization.OptionsFor, since the embedded run
reaches CultMath values and refs. Keyboard layouts need no resolver and
keep MessagePack's defaults until Cut 10.
- The input map uses the nested action to binding shape.
- PropertiesPanel reads CultInspectorRangeAttribute.
- NameTools loses its legacy name-file writer.
- Slime compares serialized settings instead of the deleted SHA1 helper.
- FieldDriver drops InspectorHeader.
- .gitignore covers the test project's build output.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6), Unity
batchmode (0 errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:15:20 +02:00
|
|
|
|
foreach (var action in PlayerSettings.InputSettings.InputActionMap) foreach (var binding in action.Value) Input.asset[action.Key].ApplyBindingOverride(binding.Key, binding.Value);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
InputDisplayLayout.Input = Input.asset;
|
|
|
|
|
|
Input.Global.Enable();
|
|
|
|
|
|
|
2026-10-07 12:33:22 +02:00
|
|
|
|
// The latch is the game's own, so it can disagree with the keyboard's Caps Lock light: show it.
|
|
|
|
|
|
Input.Player.ToggleFaceAim.performed += context => _helm.Toggle();
|
|
|
|
|
|
var markParent = GameplayUI != null ? GameplayUI.transform : UiRoot.transform;
|
|
|
|
|
|
var markObject = new GameObject("Face Aim Mark", typeof(RectTransform));
|
|
|
|
|
|
markObject.transform.SetParent(markParent, false);
|
|
|
|
|
|
_faceAimMark = markObject.AddComponent<TextMeshProUGUI>();
|
|
|
|
|
|
_faceAimMark.text = "FACE AIM";
|
|
|
|
|
|
_faceAimMark.fontSize = 24;
|
|
|
|
|
|
_faceAimMark.alignment = TextAlignmentOptions.Center;
|
|
|
|
|
|
_faceAimMark.raycastTarget = false;
|
|
|
|
|
|
_faceAimMark.rectTransform.anchorMin = _faceAimMark.rectTransform.anchorMax = new Vector2(.5f, .12f);
|
|
|
|
|
|
_faceAimMark.rectTransform.sizeDelta = new Vector2(300, 40);
|
|
|
|
|
|
_faceAimMark.enabled = false;
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
_zoomLevelIndex = Settings.DefaultMinimapZoom;
|
|
|
|
|
|
Input.Player.MinimapZoom.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_zoomLevelIndex = (_zoomLevelIndex + 1) % Settings.MinimapZoomLevels.Length;
|
|
|
|
|
|
ZoneRenderer.MinimapDistance = Settings.MinimapZoomLevels[_zoomLevelIndex];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Global.ZoneMap.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
ToggleMenuTab(MenuTab.Map);
|
2026-10-16 00:50:29 +02:00
|
|
|
|
MenuMap.Position = DrawAhead.Position(CurrentEntity, Clock.Lead).xz;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Global.Inventory.performed += context => ToggleMenuTab(MenuTab.Inventory);
|
|
|
|
|
|
|
|
|
|
|
|
Input.Global.GalaxyMap.performed += context => ToggleMenuTab(MenuTab.Galaxy);
|
|
|
|
|
|
|
|
|
|
|
|
Input.Global.Interact.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (EventSystem.current.currentSelectedGameObject != null && EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != null) return;
|
|
|
|
|
|
if (MainMenu.gameObject.activeSelf) return;
|
|
|
|
|
|
if (CurrentEntity == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// TODO: SFX: Fail
|
|
|
|
|
|
Dialog.Clear();
|
|
|
|
|
|
Dialog.Title.text = "Can't undock. You dont have a ship!";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (CurrentEntity.Parent == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var wormhole in ZoneRenderer.WormholeInstances.Keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!(length(wormhole.Position - CurrentEntity.Position.xz) < Settings.GameplaySettings.WormholeExitRadius)) continue;
|
2026-10-07 01:13:09 +02:00
|
|
|
|
if (wormhole.Exit)
|
|
|
|
|
|
{
|
2026-10-16 21:18:52 +02:00
|
|
|
|
// The exit gate leads nowhere: TryWin consults RunGoal and ends the run when the gate is open.
|
|
|
|
|
|
if (!TryWin())
|
2026-10-07 01:13:09 +02:00
|
|
|
|
{
|
|
|
|
|
|
Dialog.Clear();
|
|
|
|
|
|
Dialog.Title.text = $"The exit gate is sealed. {RunGoal.Boss(Zone)?.Name ?? "Its guardian"} holds it shut.";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
EnterWormhole(wormhole);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dock();
|
|
|
|
|
|
}
|
|
|
|
|
|
else Undock();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Global.MainMenu.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if(Menu.gameObject.activeSelf)
|
|
|
|
|
|
ToggleMenuTab(Menu.CurrentTab);
|
|
|
|
|
|
else
|
|
|
|
|
|
ToggleFullscreenMenu(MainMenu.gameObject);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Global.InputScreen.performed += context => ToggleFullscreenMenu(HelpScreen);
|
|
|
|
|
|
|
|
|
|
|
|
Input.Player.HideUI.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_uiHidden = !_uiHidden;
|
|
|
|
|
|
GameplayUI.alpha = _uiHidden ? 0 : 1;
|
|
|
|
|
|
ActionBar.gameObject.SetActive(!_uiHidden);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Player.OverrideShutdown.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentEntity.OverrideShutdown = !CurrentEntity.OverrideShutdown;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Player.Ping.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentEntity.Sensor?.Ping();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Player.ToggleHeatsinks.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentEntity.HeatsinksEnabled = !CurrentEntity.HeatsinksEnabled;
|
|
|
|
|
|
// TODO: SFX: Success/Fail
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Input.Player.ToggleShield.performed += context =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CurrentEntity.Shield != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentEntity.Shield.Item.Enabled.Value = !CurrentEntity.Shield.Item.Enabled.Value;
|
|
|
|
|
|
// TODO: SFX: Success/Fail
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-17 14:01:15 +02:00
|
|
|
|
// Flips the player's own stance on the current target between hostile and neutral. Going
|
|
|
|
|
|
// neutral mid-fight safes weapons (Weapon.StanceAllowsFire); an unmarked/undetected target
|
|
|
|
|
|
// reads as non-hostile here, so the first press declares hostile.
|
|
|
|
|
|
Input.Player.ToggleStance.performed += context =>
|
|
|
|
|
|
{
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
var target = CurrentEntity.Target.Value.Entity;
|
2026-09-17 14:01:15 +02:00
|
|
|
|
if (target == null) return;
|
|
|
|
|
|
CurrentEntity.SetIff(target, !CurrentEntity.IsHostileTo(target));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
#region Targeting
|
2026-06-17 01:50:15 +01:00
|
|
|
|
|
2026-10-05 23:38:20 +02:00
|
|
|
|
// The reticle, nearest, next and previous presses each choose through Entity (mining target-queries cut):
|
|
|
|
|
|
// the rule lives with the entity, so the player, the test agent and any director pick alike.
|
|
|
|
|
|
Input.Player.TargetReticle.performed += context => CurrentEntity.TargetUnderReticle();
|
|
|
|
|
|
Input.Player.TargetNearest.performed += context => CurrentEntity.TargetNearestEnemy();
|
|
|
|
|
|
Input.Player.TargetNext.performed += context => CurrentEntity.TargetNext();
|
|
|
|
|
|
Input.Player.TargetPrevious.performed += context => CurrentEntity.TargetPrevious();
|
2026-09-19 13:02:54 +02:00
|
|
|
|
|
2026-10-15 04:42:38 +02:00
|
|
|
|
// Cut 2 (docs/fire-control-cut.md): steps the aim through the target's schematic rows -- every mount this ship
|
|
|
|
|
|
// has located, empty or not, minus the ones it saw destroyed. Decides nothing: the entity owns the cycle.
|
|
|
|
|
|
Input.Player.CycleTargetItem.performed += context => CurrentEntity.CycleAim();
|
2026-09-19 13:02:54 +02:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Action Bar
|
|
|
|
|
|
|
|
|
|
|
|
ActionBarSlot createBinding(string controlPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var action = new InputAction(binding: controlPath);
|
|
|
|
|
|
_actionBarActions.Add(action);
|
|
|
|
|
|
var slot = Instantiate(ActionBarSlot, ActionBar);
|
|
|
|
|
|
slot.Binding = null;
|
|
|
|
|
|
_actionBarSlots.Add(slot);
|
|
|
|
|
|
action.started += context => slot.Binding?.Activate();
|
|
|
|
|
|
action.canceled += context => slot.Binding?.Deactivate();
|
|
|
|
|
|
|
|
|
|
|
|
var shortName = controlPath.Substring(controlPath.LastIndexOf('/') + 1);
|
|
|
|
|
|
var sprite = Resources.Load<Sprite>($"Sprites/Input/{shortName}");
|
|
|
|
|
|
if (sprite != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
slot.InputIcon.sprite = sprite;
|
|
|
|
|
|
slot.InputLabel.gameObject.SetActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
slot.InputLabel.text = shortName;
|
|
|
|
|
|
slot.InputIcon.gameObject.SetActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
slot.PointerEnterTrigger.OnPointerEnterAsObservable().Subscribe(_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
//Debug.Log($"Pointer entered action bar slot {controlPath}");
|
|
|
|
|
|
RegisterDragTarget(dragAction =>
|
|
|
|
|
|
{
|
|
|
|
|
|
//Debug.Log("Registering binding!");
|
|
|
|
|
|
switch (dragAction)
|
|
|
|
|
|
{
|
|
|
|
|
|
case EquippedItemDragObject equippedItemDragAction:
|
|
|
|
|
|
var trigger = equippedItemDragAction.EquippedItem.GetBehavior<IActivatedBehavior>();
|
|
|
|
|
|
if (trigger == null) return false;
|
|
|
|
|
|
slot.Binding = new ActionBarGearBinding(CurrentEntity, slot, equippedItemDragAction.EquippedItem, trigger);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
case ItemInstanceDragObject itemInstanceDragAction:
|
Replace the Database Tools with the Studio and cut Unity over
Cut 8b. The Unity project compiles with 0 errors in 6000.3.24f1 batchmode.
Packages: manifest.json pins org.gamecult.cultlib at cultlib-unity-v1.0.57
and org.gamecult.caching.unity at caching-unity-v1.2.0, next to the 8a
CultMath tag. Aetheria.Shared.Unity references GameCult.CultLib and keeps
noEngineReferences.
The Database Tools (Assets/Scripts/CultCache/Editor) are deleted in this
commit, alongside their five ported drawers in
Assets/Scripts/Editor/CultCacheDrawers.cs, each an
[CultInspectorDrawer] ICultInspectorDrawer:
- InspectableType: popup of the attribute type's [Union] member names,
writing the string.
- InspectableColor: float3 and float4 through ColorField.
- InspectableAnimationCurve: float4[] and BezierCurve through CurveField.
- InspectableTemperature: Kelvin, Celsius and Fahrenheit fields over one
value.
- InspectableSchematicShape: size and cell toggles. The old drawer also
drew the item's schematic texture and tinted hull hardpoints, which need
the parent document. The tagged Studio hands a drawer only the value, so
those wait on the Studio exposing the open record.
Unity call sites:
- ActionGameManager opens the catalog through AetheriaStores; Cut 10 adds
the run and player stores.
- Refs resolve through ItemManager.GetData or Get(ref); orbit, planet and
belt maps key by CultRecordKey; identity is compared by reference or key.
- The PlayerSettings.msgpack reader and writer, which Cut 10 deletes, pass
CultDocumentMessagePackSerialization.OptionsFor, since the embedded run
reaches CultMath values and refs. Keyboard layouts need no resolver and
keep MessagePack's defaults until Cut 10.
- The input map uses the nested action to binding shape.
- PropertiesPanel reads CultInspectorRangeAttribute.
- NameTools loses its legacy name-file writer.
- Slime compares serialized settings instead of the deleted SHA1 helper.
- FieldDriver drops InspectorHeader.
- .gitignore covers the test project's build output.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6), Unity
batchmode (0 errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:15:20 +02:00
|
|
|
|
if (!(ItemManager.GetData(itemInstanceDragAction.Item) is ConsumableItemData consumable)) return false;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
slot.Binding = new ActionBarConsumableBinding(CurrentEntity, slot, consumable);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
case WeaponGroupDragObject weaponGroupDragAction:
|
|
|
|
|
|
slot.Binding = new ActionBarWeaponGroupBinding(CurrentEntity, slot, weaponGroupDragAction.Group);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(dragAction));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
slot.PointerExitTrigger.OnPointerExitAsObservable().Subscribe(_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
//Debug.Log($"Pointer exited action bar slot {controlPath}");
|
|
|
|
|
|
UnregisterDragTarget();
|
|
|
|
|
|
});
|
|
|
|
|
|
return slot;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var bindings = PlayerSettings.InputSettings.ActionBarInputs.OrderBy(i => i)
|
|
|
|
|
|
.Select(createBinding).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
StartGame();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
//if (!PlayerSettings.InputSettings.ActionBarInputs.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
var newbinds = Enumerable.Range(0, 64)//CurrentEntity.WeaponGroups
|
|
|
|
|
|
.Zip(
|
|
|
|
|
|
bindings,
|
|
|
|
|
|
(i, slot) =>
|
|
|
|
|
|
slot.Binding = new ActionBarWeaponGroupBinding(CurrentEntity, slot, i)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ConsoleController.AddCommand("revealzones",
|
|
|
|
|
|
_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var zones in CurrentGalaxy.Zones
|
|
|
|
|
|
.Where(z=>!CurrentGalaxy.DiscoveredZones.Contains(z))
|
|
|
|
|
|
.GroupBy(z=>z.Distance[CurrentGalaxy.Entrance])
|
|
|
|
|
|
.OrderBy(g=>g.Key))
|
|
|
|
|
|
{
|
|
|
|
|
|
SectorMap.QueueZoneReveal(zones);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
ConsoleController.AddCommand("give",
|
|
|
|
|
|
args =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var itemName = string.Join(" ", args);
|
|
|
|
|
|
var item = ItemManager.ItemData.GetAll<EquippableItemData>()
|
|
|
|
|
|
.FirstOrDefault(itemData => string.Equals(itemData.Name, itemName, StringComparison.InvariantCultureIgnoreCase));
|
2026-09-30 01:04:11 +02:00
|
|
|
|
if (item == null)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
2026-09-30 01:04:11 +02:00
|
|
|
|
ConsoleController.Instance.AppendLogLine($"No equippable item named \"{itemName}\"");
|
|
|
|
|
|
return;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
2026-09-30 01:04:11 +02:00
|
|
|
|
if (item is HullData hull)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (hull.HullType != HullType.Ship)
|
|
|
|
|
|
{
|
|
|
|
|
|
ConsoleController.Instance.AppendLogLine($"Refused: {item.Name} is a {hull.HullType} hull, not a ship");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (DockedEntity == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
ConsoleController.Instance.AppendLogLine($"Refused: dock to take delivery of a {item.Name}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
CommissionShip(ItemManager.CreateInstance(ItemManager.CreateLot(item, default, .95f)) as EquippableItem);
|
|
|
|
|
|
ConsoleController.Instance.AppendLogLine($"{item.Name} moored at {DockedEntity.Name}");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!_currentEntity.CargoBays.First().TryStore(ItemManager.CreateInstance(ItemManager.CreateLot(item, default, .95f))))
|
|
|
|
|
|
ConsoleController.Instance.AppendLogLine($"Refused: no cargo space for {item.Name}");
|
2026-08-10 14:57:50 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
ConsoleController.AddCommand("trackmissile",
|
|
|
|
|
|
_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var missileManager in FindObjectsByType<GuidedProjectileManager>(FindObjectsSortMode.None))
|
|
|
|
|
|
{
|
|
|
|
|
|
missileManager.OnFireGuided.Where(x => x.source == _currentEntity).Take(1).Subscribe(x =>
|
|
|
|
|
|
{
|
|
|
|
|
|
FollowCamera.Follow = x.missile.transform;
|
2026-09-29 17:05:15 +02:00
|
|
|
|
FollowCamera.LookAt = x.target ? x.target : x.missile.transform;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
x.missile.OnKill += () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
FollowCamera.LookAt = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint;
|
|
|
|
|
|
FollowCamera.Follow = ZoneRenderer.EntityInstances[CurrentEntity].transform;
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
ConsoleController.AddCommand("spawnturret",
|
|
|
|
|
|
_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var nearestFaction = CurrentGalaxy.Factions.MinBy(f => CurrentGalaxy.HomeZones[f].Distance[Zone.GalaxyZone]);
|
|
|
|
|
|
|
2026-10-03 20:26:30 +02:00
|
|
|
|
var loadoutGenerator = new LoadoutGenerator(ref ItemManager.Random, ItemManager, CurrentGalaxy, Zone.GalaxyZone, nearestFaction, .5f);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2026-09-17 03:42:12 +02:00
|
|
|
|
var turret = EntitySerializer.Unpack(ItemManager, Zone, loadoutGenerator.GenerateTurretLoadout());
|
2026-08-10 14:57:50 +01:00
|
|
|
|
turret.Position.xz = _currentEntity.Position.xz +
|
|
|
|
|
|
ItemManager.Random.NextFloat2Direction() * ItemManager.Random.NextFloat(50, 500);
|
2026-09-30 12:33:04 +02:00
|
|
|
|
Zone.Admit(turret, piloted: false);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
});
|
|
|
|
|
|
//Temporary, or not
|
|
|
|
|
|
ConsoleController.AddCommand("tow", _ => TowShip());
|
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
|
|
|
|
|
2026-09-17 14:01:15 +02:00
|
|
|
|
// Manual IFF override for the player's current target, for hand-testing combat.
|
|
|
|
|
|
// "iff hostile"/"iff neutral" force a stance; "iff clear" restores the derived faction rule.
|
|
|
|
|
|
ConsoleController.AddCommand("iff", args =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var console = ConsoleController.Instance;
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
var target = _currentEntity?.Target.Value.Entity;
|
2026-09-17 14:01:15 +02:00
|
|
|
|
if (target == null) { console.AppendLogLine("iff: no target selected"); return; }
|
|
|
|
|
|
var mode = args.Length > 0 ? args[0] : "";
|
|
|
|
|
|
if (mode != "hostile" && mode != "neutral" && mode != "clear")
|
|
|
|
|
|
{
|
|
|
|
|
|
console.AppendLogLine("usage: iff hostile|neutral|clear");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
switch (mode)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "hostile":
|
|
|
|
|
|
_currentEntity.SetIff(target, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "neutral":
|
|
|
|
|
|
_currentEntity.SetIff(target, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "clear":
|
|
|
|
|
|
_currentEntity.SetIff(target, null);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.AppendLogLine($"{target.Name}: {(_currentEntity.IsHostileTo(target) ? "hostile" : "neutral")}");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Capture presets through their own cache; keep the catalog read-only
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:32:52 +02:00
|
|
|
|
// Editor only: capturepreset "<name>" [replace] writes the piloted ship as a catalog preset. A multi-word name must
|
|
|
|
|
|
// be quoted; any other second argument than replace is refused rather than dropped.
|
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
|
|
|
|
if (Application.isEditor)
|
|
|
|
|
|
ConsoleController.AddCommand("capturepreset", args =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var console = ConsoleController.Instance;
|
|
|
|
|
|
var name = args.Length > 0 ? args[0] : "";
|
Capture presets through their own cache; keep the catalog read-only
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:32:52 +02:00
|
|
|
|
var replace = args.Length == 2 && args[1] == "replace";
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(name) || args.Length > 2 || args.Length == 2 && !replace)
|
|
|
|
|
|
{
|
|
|
|
|
|
console.AppendLogLine("usage: capturepreset \"<name>\" [replace] (quote a name with spaces)");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
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
|
|
|
|
if (!(_currentEntity is Ship ship)) { console.AppendLogLine("capturepreset: pilot a ship first"); return; }
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
Capture presets through their own cache; keep the catalog read-only
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:32:52 +02:00
|
|
|
|
var written = Loadouts.Commit(CatalogPath, Loadouts.Capture(ItemManager, ship, name), replace);
|
|
|
|
|
|
console.AppendLogLine(written
|
|
|
|
|
|
? $"Preset '{name}' written to {CatalogPath}. This session sees it after the catalog reloads (next launch). " +
|
|
|
|
|
|
"Reopen CultCache Studio before saving there: a Studio session opened earlier overwrites captured presets."
|
|
|
|
|
|
: $"Preset '{name}' changed on disk since load; nothing written");
|
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
|
|
|
|
}
|
|
|
|
|
|
catch (InvalidOperationException e)
|
|
|
|
|
|
{
|
|
|
|
|
|
console.AppendLogLine(e.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void BeginDrag(DragObject dragObject)
|
|
|
|
|
|
{
|
|
|
|
|
|
this.DragObject = dragObject;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void RegisterDragTarget(Func<DragObject, bool> onEndDrag)
|
|
|
|
|
|
{
|
|
|
|
|
|
_endDragCallback = onEndDrag;
|
2026-06-17 01:50:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public void UnregisterDragTarget()
|
2026-06-17 01:50:15 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
_endDragCallback = null;
|
2026-06-17 01:50:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public bool EndDrag()
|
2026-06-17 01:50:15 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var success = _endDragCallback?.Invoke(DragObject);
|
|
|
|
|
|
DragObject = null;
|
|
|
|
|
|
return success ?? false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void EnablePlayerInput()
|
|
|
|
|
|
{
|
|
|
|
|
|
Cursor.lockState = CursorLockMode.Locked;
|
|
|
|
|
|
Input.Player.Enable();
|
|
|
|
|
|
foreach (var a in _actionBarActions) a.Enable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void DisablePlayerInput()
|
|
|
|
|
|
{
|
|
|
|
|
|
Cursor.lockState = CursorLockMode.None;
|
|
|
|
|
|
Input.Player.Disable();
|
|
|
|
|
|
foreach (var a in _actionBarActions) a.Disable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void EnterWormhole(Wormhole wormhole)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!(CurrentEntity is Ship ship) || ship.WormholeAnimationInProgress) return;
|
|
|
|
|
|
// var wormholeCameraFollow = new GameObject("Wormhole Camera Follow").transform;
|
|
|
|
|
|
// wormholeCameraFollow.position = new Vector3(wormhole.Position.x, -50, wormhole.Position.y);
|
2026-10-07 12:33:22 +02:00
|
|
|
|
// wormholeCameraFollow.rotation = Quaternion.LookRotation(Vector3.down, ship.Aim);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
// WormholeCamera.enabled = true;
|
|
|
|
|
|
// WormholeCamera.Follow = wormholeCameraFollow;
|
|
|
|
|
|
// FollowCamera.enabled = false;
|
|
|
|
|
|
ship.EnterWormhole(wormhole.Position);
|
|
|
|
|
|
ship.OnEnteredWormhole += () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var oldZone = Zone;
|
|
|
|
|
|
PopulateLevel(wormhole.Target);
|
|
|
|
|
|
foreach (var zone in wormhole.Target.AdjacentZones)
|
|
|
|
|
|
CurrentGalaxy.DiscoveredZones.Add(zone);
|
|
|
|
|
|
SectorMap.QueueZoneReveal(wormhole.Target.AdjacentZones);
|
|
|
|
|
|
ship.ExitWormhole(ZoneRenderer.WormholeInstances.Keys.First(w => w.Target == oldZone.GalaxyZone).Position,
|
|
|
|
|
|
Settings.GameplaySettings.WormholeExitVelocity * ItemManager.Random.NextFloat2Direction());
|
|
|
|
|
|
CurrentEntity.Zone = Zone;
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
SaveRun();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void PopulateLevel(GalaxyZone galaxyZone)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (galaxyZone == null) throw new ArgumentNullException(nameof(galaxyZone));
|
|
|
|
|
|
|
|
|
|
|
|
if (galaxyZone.Contents == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
galaxyZone.PackedContents ??= ZoneGenerator.GenerateZone(
|
|
|
|
|
|
ItemManager,
|
|
|
|
|
|
Settings.ZoneSettings,
|
2026-10-17 04:55:57 +02:00
|
|
|
|
Settings.PlanetSettings,
|
2026-08-10 14:57:50 +01:00
|
|
|
|
CurrentGalaxy,
|
|
|
|
|
|
galaxyZone,
|
2026-10-03 20:26:30 +02:00
|
|
|
|
CurrentGalaxy.IsPrelude
|
2026-08-10 14:57:50 +01:00
|
|
|
|
);
|
|
|
|
|
|
galaxyZone.Contents = new Zone(ItemManager, Settings.PlanetSettings, galaxyZone.PackedContents, galaxyZone, CurrentGalaxy);
|
|
|
|
|
|
}
|
|
|
|
|
|
Zone = galaxyZone.Contents;
|
2026-10-08 23:39:30 +02:00
|
|
|
|
_zoneEntered = true; // the clock forgets its lead between frames, never inside a step
|
2026-08-10 14:57:50 +01:00
|
|
|
|
PlayMusic(MusicType.Overworld);
|
2026-10-13 07:13:55 +02:00
|
|
|
|
_music = MusicType.Overworld;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
Zone.Log = s => Debug.Log($"Zone: {s}");
|
|
|
|
|
|
|
|
|
|
|
|
if (CurrentEntity != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentEntity.Deactivate();
|
2026-10-13 02:13:43 +02:00
|
|
|
|
CurrentEntity.Zone.Remove(CurrentEntity);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
CurrentEntity.Zone = Zone;
|
2026-09-30 12:33:04 +02:00
|
|
|
|
Zone.Admit(CurrentEntity, piloted: false);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ZoneRenderer.LoadZone(Zone);
|
|
|
|
|
|
|
|
|
|
|
|
if (CurrentEntity != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
UnbindEntity();
|
|
|
|
|
|
BindToEntity(CurrentEntity);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ToggleFullscreenMenu(GameObject menu)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (EventSystem.current != null && EventSystem.current.currentSelectedGameObject != null && EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != null) return;
|
|
|
|
|
|
if (CurrentEntity == null) return;
|
|
|
|
|
|
if (menu.activeSelf)
|
|
|
|
|
|
{
|
|
|
|
|
|
_paused = false;
|
|
|
|
|
|
menu.SetActive(false);
|
|
|
|
|
|
UiRoot.SetActive(true);
|
|
|
|
|
|
if (!_menuShown)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnablePlayerInput();
|
|
|
|
|
|
UpdatePlayerPanel();
|
|
|
|
|
|
UpdateTargetPanel(CurrentEntity.Target.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_paused = true;
|
|
|
|
|
|
menu.SetActive(true);
|
|
|
|
|
|
UiRoot.SetActive(false);
|
|
|
|
|
|
_menuShown = Menu.gameObject.activeSelf;
|
|
|
|
|
|
if (!_menuShown) DisablePlayerInput();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ToggleMenuTab(MenuTab tab)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (EventSystem.current.currentSelectedGameObject != null && EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != null) return;
|
|
|
|
|
|
if (MainMenu.gameObject.activeSelf) return;
|
|
|
|
|
|
if (Menu.gameObject.activeSelf && Menu.CurrentTab == tab)
|
|
|
|
|
|
{
|
|
|
|
|
|
Menu.gameObject.SetActive(false);
|
|
|
|
|
|
if (CurrentEntity != null && CurrentEntity.Parent == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnablePlayerInput();
|
|
|
|
|
|
UpdatePlayerPanel();
|
|
|
|
|
|
UpdateTargetPanel(CurrentEntity.Target.Value);
|
|
|
|
|
|
GameplayUI.gameObject.SetActive(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DisablePlayerInput();
|
|
|
|
|
|
Menu.ShowTab(tab);
|
|
|
|
|
|
GameplayUI.gameObject.SetActive(false);
|
2026-06-17 01:50:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
private void StartGame()
|
2026-06-17 01:50:15 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (CurrentGalaxy != null)
|
|
|
|
|
|
{
|
A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:03:50 +02:00
|
|
|
|
if (PendingScenario != null) StartScenario();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
else
|
|
|
|
|
|
{
|
A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:03:50 +02:00
|
|
|
|
var saved = CultCache.GetGlobal<SavedGame>();
|
2026-10-06 19:30:34 +02:00
|
|
|
|
Credits = saved.Credits;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
foreach(var group in CurrentGalaxy.DiscoveredZones
|
|
|
|
|
|
.GroupBy(dz=>dz.Distance[CurrentGalaxy.Entrance]))
|
|
|
|
|
|
SectorMap.QueueZoneReveal(group);
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
PopulateLevel(CurrentGalaxy.Zones[saved.CurrentZone]);
|
|
|
|
|
|
var targetEntity = Zone.Entities[saved.CurrentZoneEntity];
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (targetEntity is OrbitalEntity orbitalEntity)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentEntity = targetEntity.Children.First(c => c is Ship {IsPlayerShip: true});
|
|
|
|
|
|
DoDock(orbitalEntity, orbitalEntity.DockingBays.First());
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
BindToEntity(targetEntity);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < _actionBarSlots.Count; i++)
|
|
|
|
|
|
{
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
_actionBarSlots[i].Restore(saved.ActionBarBindings[i], CurrentEntity);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-17 01:50:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:03:50 +02:00
|
|
|
|
// RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this enters and binds. The saved run is
|
|
|
|
|
|
// replaced only once the scenario has staged. A start that fails leaves the saved run as it was and says why on
|
|
|
|
|
|
// the main menu, where Continue still resumes the saved run.
|
|
|
|
|
|
private void StartScenario()
|
|
|
|
|
|
{
|
|
|
|
|
|
var scenario = PendingScenario;
|
|
|
|
|
|
PendingScenario = null;
|
|
|
|
|
|
var failures = new List<string>();
|
|
|
|
|
|
RunStart.Staged staged = null;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
staged = RunSave.Replace(CultCache, () =>
|
|
|
|
|
|
{
|
2026-10-17 04:55:57 +02:00
|
|
|
|
RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, Settings.PlanetSettings, CurrentGalaxy, scenario);
|
A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:03:50 +02:00
|
|
|
|
PopulateLevel(CurrentGalaxy.Entrance);
|
|
|
|
|
|
return RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception exception)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogException(exception);
|
|
|
|
|
|
failures.Add(exception.Message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (staged == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentGalaxy = null;
|
|
|
|
|
|
MainMenu.gameObject.SetActive(true);
|
|
|
|
|
|
MainMenu.Refuse($"{scenario.Name} could not start", string.Join("\n", failures));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-10-06 19:30:34 +02:00
|
|
|
|
Credits = staged.Credits;
|
A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:03:50 +02:00
|
|
|
|
SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance));
|
|
|
|
|
|
BindToEntity(staged.Player);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public void Dock()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CurrentEntity.Parent != null) return;
|
|
|
|
|
|
if (CurrentEntity is Ship ship)
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
foreach (var entity in Zone.Entities.ToArray())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (entity != CurrentEntity && lengthsq(entity.Position.xz - CurrentEntity.Position.xz) <
|
|
|
|
|
|
Settings.GameplaySettings.DockingDistance * Settings.GameplaySettings.DockingDistance)
|
|
|
|
|
|
{
|
|
|
|
|
|
var bay = entity.TryDock(ship);
|
|
|
|
|
|
if (bay != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
UnbindEntity();
|
|
|
|
|
|
DoDock(entity, bay);
|
|
|
|
|
|
// TODO: SFX: Docking
|
|
|
|
|
|
//AkSoundEngine.PostEvent("Dock", gameObject);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
2026-06-29 11:48:00 +01:00
|
|
|
|
|
2026-09-30 01:04:11 +02:00
|
|
|
|
// A ship hull acquired while docked becomes a bare player ship moored at the docked entity. It takes no docking
|
|
|
|
|
|
// bay: mothballed ships do not need one. Buying from the trade menu and the give command both land here.
|
|
|
|
|
|
public Ship CommissionShip(EquippableItem hull)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ship = new Ship(ItemManager, Zone, hull, ItemManager.GameplaySettings.DefaultEntitySettings) { IsPlayerShip = true };
|
|
|
|
|
|
ship.SetParent(DockedEntity);
|
|
|
|
|
|
return ship;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private void DoDock(Entity entity, EquippedDockingBay dockingBay)
|
|
|
|
|
|
{
|
|
|
|
|
|
TradeMenu.Inventory = entity.CargoBays.First();
|
|
|
|
|
|
DockedEntity = entity;
|
|
|
|
|
|
ZoneRenderer.PerspectiveEntity = DockedEntity;
|
|
|
|
|
|
DockingBay = dockingBay;
|
|
|
|
|
|
DockCamera.enabled = true;
|
|
|
|
|
|
FollowCamera.enabled = false;
|
|
|
|
|
|
var orbital = (OrbitalEntity) entity;
|
|
|
|
|
|
DockCamera.Follow = ZoneRenderer.EntityInstances[orbital].transform;
|
Replace the Database Tools with the Studio and cut Unity over
Cut 8b. The Unity project compiles with 0 errors in 6000.3.24f1 batchmode.
Packages: manifest.json pins org.gamecult.cultlib at cultlib-unity-v1.0.57
and org.gamecult.caching.unity at caching-unity-v1.2.0, next to the 8a
CultMath tag. Aetheria.Shared.Unity references GameCult.CultLib and keeps
noEngineReferences.
The Database Tools (Assets/Scripts/CultCache/Editor) are deleted in this
commit, alongside their five ported drawers in
Assets/Scripts/Editor/CultCacheDrawers.cs, each an
[CultInspectorDrawer] ICultInspectorDrawer:
- InspectableType: popup of the attribute type's [Union] member names,
writing the string.
- InspectableColor: float3 and float4 through ColorField.
- InspectableAnimationCurve: float4[] and BezierCurve through CurveField.
- InspectableTemperature: Kelvin, Celsius and Fahrenheit fields over one
value.
- InspectableSchematicShape: size and cell toggles. The old drawer also
drew the item's schematic texture and tinted hull hardpoints, which need
the parent document. The tagged Studio hands a drawer only the value, so
those wait on the Studio exposing the open record.
Unity call sites:
- ActionGameManager opens the catalog through AetheriaStores; Cut 10 adds
the run and player stores.
- Refs resolve through ItemManager.GetData or Get(ref); orbit, planet and
belt maps key by CultRecordKey; identity is compared by reference or key.
- The PlayerSettings.msgpack reader and writer, which Cut 10 deletes, pass
CultDocumentMessagePackSerialization.OptionsFor, since the embedded run
reaches CultMath values and refs. Keyboard layouts need no resolver and
keep MessagePack's defaults until Cut 10.
- The input map uses the nested action to binding shape.
- PropertiesPanel reads CultInspectorRangeAttribute.
- NameTools loses its legacy name-file writer.
- Slime compares serialized settings instead of the deleted SHA1 helper.
- FieldDriver drops InspectorHeader.
- .gitignore covers the test project's build output.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6), Unity
batchmode (0 errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:15:20 +02:00
|
|
|
|
var parentOrbit = Zone.Orbits[orbital.OrbitData].Data.Parent.Key;
|
|
|
|
|
|
var parentOrbitPlanet = Zone.Planets.FirstOrDefault(p => p.Value.Orbit.Key.Equals(parentOrbit)).Key;
|
|
|
|
|
|
if (parentOrbitPlanet.IsSet() && ZoneRenderer.Planets.ContainsKey(parentOrbitPlanet))
|
2026-08-10 14:57:50 +01:00
|
|
|
|
DockCamera.LookAt = ZoneRenderer.Planets[parentOrbitPlanet].Body.transform;
|
|
|
|
|
|
else DockCamera.LookAt = ZoneRenderer.ZoneRoot;
|
|
|
|
|
|
if (entity is OrbitalEntity {CanTow: true})
|
|
|
|
|
|
TowingStation = entity;
|
|
|
|
|
|
Menu.ShowTab(MenuTab.Inventory);
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public void Undock()
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (CurrentEntity.Parent == null) return;
|
|
|
|
|
|
if (CurrentEntity is Ship ship)
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (CurrentEntity.GetBehavior<Cockpit>() == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Dialog.Clear();
|
|
|
|
|
|
Dialog.Title.text = "Can't undock. Missing cockpit component!";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
|
|
|
|
|
// TODO: SFX: Fail
|
|
|
|
|
|
}
|
Delete the aether drive: behaviour, union 12, hardpoint type, aggregates, settings, presenters, prefab
AetherDriveData and AetherDrive, Union(12) (now a retired-do-not-reuse comment, as 26 is),
HardpointType.AetherDrive (last member, nothing shifts), Ship's drive sets, aggregates and
Axis write, the five request-field rows, GameplaySettings Aether multipliers, ShipInstance's
drive particles, SchematicDisplay's RPM display and AetherDriveUi, the undock refusal's drive
clause, the drive prefab and material, and AetherDb's drive roles and brownout row. The
design is kept at parked/aether-drive.
Adds EveryShippedShipHullHasATopSpeed. ARPG.unity still holds stale serialized references to
the removed SchematicDisplay fields and the RPM Display object; the next commit removes them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 14:04:28 +02:00
|
|
|
|
else if (CurrentEntity.GetBehavior<Thruster>() == null)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
|
|
|
|
|
Dialog.Clear();
|
|
|
|
|
|
Dialog.Title.text = "Can't undock. Missing thruster component!";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
|
|
|
|
|
// TODO: SFX: Fail
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (CurrentEntity.GetBehavior<Reactor>() == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Dialog.Clear();
|
|
|
|
|
|
Dialog.Title.text = "Can't undock. Missing reactor component!";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
|
|
|
|
|
// TODO: SFX: Fail
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (CurrentEntity.Parent.TryUndock(ship))
|
|
|
|
|
|
{
|
|
|
|
|
|
BindToEntity(ship);
|
|
|
|
|
|
// TODO: SFX: Undock
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Dialog.Title.text = "Can't undock. Must empty docking bay!";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
|
|
|
|
|
// TODO: SFX: Fail
|
|
|
|
|
|
}
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
2026-06-29 11:48:00 +01:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public void TowShip()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CurrentEntity.Parent != null) return;
|
|
|
|
|
|
if (CurrentEntity is Ship ship)
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (Zone.Equals(TowingStation.Zone))
|
|
|
|
|
|
{
|
|
|
|
|
|
var distance = (int)length(TowingStation.Position.xz - CurrentEntity.Position.xz);
|
|
|
|
|
|
CurrentEntity.Position.xz = TowingStation.Position.xz;
|
|
|
|
|
|
Dock();
|
|
|
|
|
|
//Debug.Log($"${distance}");
|
|
|
|
|
|
//payment = distance * TowingZoneRate;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
var distance = Zone.GalaxyZone.Distance[TowingStation.Zone.GalaxyZone];
|
|
|
|
|
|
PopulateLevel(TowingStation.Zone.GalaxyZone);
|
|
|
|
|
|
CurrentEntity.Zone = Zone;
|
|
|
|
|
|
CurrentEntity.Position.xz = TowingStation.Position.xz;
|
|
|
|
|
|
Dock();
|
|
|
|
|
|
//Debug.Log($"${distance * 1000}");
|
|
|
|
|
|
//payment = distance * TowingGalaxyRate;
|
|
|
|
|
|
}
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
2026-06-29 11:48:00 +01:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private void UnbindEntity()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var indicator in _visibleHostileIndicators) Destroy(indicator.Value.gameObject);
|
|
|
|
|
|
_visibleHostileIndicators.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var indicator in _visibleFriendlyIndicators) Destroy(indicator.Value.gameObject);
|
|
|
|
|
|
_visibleFriendlyIndicators.Clear();
|
2026-10-14 13:54:43 +02:00
|
|
|
|
|
|
|
|
|
|
foreach (var marker in _bearingMarkers.Values) RetireMarker(marker);
|
|
|
|
|
|
_bearingMarkers.Clear();
|
|
|
|
|
|
foreach (var flash in _sweepFlashes) RetireFlash(flash.indicator, flash.image);
|
|
|
|
|
|
_sweepFlashes.Clear();
|
|
|
|
|
|
_pendingSweeps.Clear();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2026-10-14 21:11:37 +02:00
|
|
|
|
if(_progressIndicators!=null) foreach(var (_, _, indicator, _) in _progressIndicators)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
indicator.GetComponent<Prototype>().ReturnToPool();
|
2026-10-10 19:25:22 +02:00
|
|
|
|
if(_incomingLockIndicator.indicator != null) _incomingLockIndicator.indicator.GetComponent<Prototype>().ReturnToPool();
|
|
|
|
|
|
_incomingLockIndicator = default;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
DisablePlayerInput();
|
|
|
|
|
|
Cursor.lockState = CursorLockMode.None;
|
|
|
|
|
|
GameplayUI.gameObject.SetActive(false);
|
|
|
|
|
|
|
|
|
|
|
|
foreach(var subscription in _shipSubscriptions) subscription.Dispose();
|
|
|
|
|
|
_shipSubscriptions.Clear();
|
2026-09-20 21:58:44 +02:00
|
|
|
|
_debugLastShots.Clear();
|
|
|
|
|
|
if (DebugInfoText != null) DebugInfoText.text = "";
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private void BindToEntity(Entity entity)
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (!ZoneRenderer.EntityInstances.ContainsKey(entity))
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"Attempted to bind to entity {entity.Name}, but SectorRenderer has no such instance!");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CurrentEntity = entity;
|
|
|
|
|
|
DeathPost.weight = 0;
|
|
|
|
|
|
ZoneRenderer.PerspectiveEntity = CurrentEntity;
|
|
|
|
|
|
|
|
|
|
|
|
Menu.gameObject.SetActive(false);
|
|
|
|
|
|
DockedEntity = null;
|
|
|
|
|
|
DockingBay = null;
|
|
|
|
|
|
DockCamera.enabled = false;
|
|
|
|
|
|
FollowCamera.enabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (length(CurrentEntity.Direction) > .1f)
|
|
|
|
|
|
_viewDirection = float3(CurrentEntity.Direction.x,0,CurrentEntity.Direction.y);
|
|
|
|
|
|
|
|
|
|
|
|
Cursor.lockState = CursorLockMode.Locked;
|
|
|
|
|
|
EnablePlayerInput();
|
|
|
|
|
|
GameplayUI.gameObject.SetActive(true);
|
|
|
|
|
|
ShipPanel.Display(CurrentEntity, true);
|
|
|
|
|
|
SchematicDisplay.ShowShip(CurrentEntity);
|
2026-09-20 21:58:44 +02:00
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.Zone.ShotResolved
|
|
|
|
|
|
.Where(outcome => outcome.Source == CurrentEntity)
|
|
|
|
|
|
.Subscribe(outcome => _debugLastShots[outcome.Weapon] = outcome));
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
FollowCamera.LookAt = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint;
|
|
|
|
|
|
FollowCamera.Follow = ZoneRenderer.EntityInstances[CurrentEntity].transform;
|
|
|
|
|
|
_articulationGroups = CurrentEntity.Equipment
|
|
|
|
|
|
.Where(item => item.Behaviors.Any(x => x.Data is WeaponData && !(x.Data is LauncherData)))
|
|
|
|
|
|
.GroupBy(item => ZoneRenderer.EntityInstances[CurrentEntity]
|
2026-10-03 14:57:27 +02:00
|
|
|
|
.GetBarrel(item.Hardpoint)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
.GetComponentInParent<ArticulationPoint>()?.Group ?? -1)
|
|
|
|
|
|
.Select((group, index) => {
|
|
|
|
|
|
return (
|
2026-10-03 14:57:27 +02:00
|
|
|
|
group.Select(item => item.Hardpoint).ToArray(),
|
|
|
|
|
|
group.Select(item => ZoneRenderer.EntityInstances[CurrentEntity].GetBarrel(item.Hardpoint)).ToArray(),
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Crosshairs[index]
|
|
|
|
|
|
);
|
|
|
|
|
|
}).ToArray();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var crosshair in Crosshairs)
|
|
|
|
|
|
crosshair.gameObject.SetActive(false);
|
|
|
|
|
|
foreach (var group in _articulationGroups)
|
|
|
|
|
|
group.crosshair.gameObject.SetActive(true);
|
|
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.Target.Subscribe(target =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// Clear previous subscriptions related to currently targeted enemy
|
|
|
|
|
|
foreach(var subscription in _targetSubscriptions)
|
|
|
|
|
|
subscription.Dispose();
|
|
|
|
|
|
_targetSubscriptions.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
UpdateTargetPanel(target);
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
var targetEntity = target.Entity;
|
|
|
|
|
|
if (targetEntity != null)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
if (targetEntity.Shield != null)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
|
|
|
|
|
TargetShieldsBackground.color = new Color(ShieldColor.r, ShieldColor.g, ShieldColor.b, .4f);
|
|
|
|
|
|
TargetShieldsIcon.color = ShieldColor;
|
|
|
|
|
|
TargetShieldsIcon.sprite = ShieldIcon;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
TargetShieldsBackground.color = new Color(NoShieldColor.r, NoShieldColor.g, NoShieldColor.b, .4f);
|
|
|
|
|
|
TargetShieldsIcon.color = NoShieldColor;
|
|
|
|
|
|
TargetShieldsIcon.sprite = NoShieldIcon;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Subscribe to incoming hits from the player ship to display the hit marker
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
_targetSubscriptions.Add(targetEntity.IncomingHit.Where(e => e == CurrentEntity).Subscribe(_ =>
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
|
|
|
|
|
HitMarker.SetActive(true);
|
|
|
|
|
|
_hitMarkerTime = HitMarkerDuration;
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var hostile in CurrentEntity.VisibleEnemies)
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var indicator = HostileTargetIndicator.Instantiate<VisibleTargetIndicator>();
|
|
|
|
|
|
_visibleHostileIndicators.Add(hostile, indicator);
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.VisibleEnemies.ObserveAdd().Subscribe(addEvent =>
|
2026-06-29 11:48:00 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var indicator = HostileTargetIndicator.Instantiate<VisibleTargetIndicator>();
|
|
|
|
|
|
_visibleHostileIndicators.Add(addEvent.Value, indicator);
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.VisibleEnemies.ObserveRemove().Subscribe(removeEvent =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_visibleHostileIndicators[removeEvent.Value].GetComponent<Prototype>().ReturnToPool();
|
|
|
|
|
|
_visibleHostileIndicators.Remove(removeEvent.Value);
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var friendly in CurrentEntity.VisibleFriendlies)
|
|
|
|
|
|
{
|
|
|
|
|
|
var indicator = FriendlyTargetIndicator.Instantiate<VisibleTargetIndicator>();
|
|
|
|
|
|
_visibleFriendlyIndicators.Add(friendly, indicator);
|
2026-06-29 11:48:00 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.VisibleFriendlies.ObserveAdd().Subscribe(addEvent =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var indicator = FriendlyTargetIndicator.Instantiate<VisibleTargetIndicator>();
|
|
|
|
|
|
_visibleFriendlyIndicators.Add(addEvent.Value, indicator);
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.VisibleFriendlies.ObserveRemove().Subscribe(removeEvent =>
|
|
|
|
|
|
{
|
|
|
|
|
|
_visibleFriendlyIndicators[removeEvent.Value].GetComponent<Prototype>().ReturnToPool();
|
|
|
|
|
|
_visibleFriendlyIndicators.Remove(removeEvent.Value);
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
_shipSubscriptions.Add(CurrentEntity.Death.Subscribe(Die));
|
|
|
|
|
|
|
2026-10-14 21:11:37 +02:00
|
|
|
|
var progressSources = CurrentEntity.GetBehaviors<LockWeapon>().Select(x => ((Func<float>) (() => x.Lock), true))
|
|
|
|
|
|
.Concat(CurrentEntity.GetBehaviors<Scanner>().Select(x => ((Func<float>) (() => x.Progress), false)));
|
|
|
|
|
|
_progressIndicators = progressSources
|
2026-08-10 14:57:50 +01:00
|
|
|
|
.Select(x =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var i = LockIndicator.Instantiate<PlaceUIElementWorldspace>();
|
2026-10-14 21:11:37 +02:00
|
|
|
|
return (x.Item1, x.Item2, i, i.GetComponent<Rotate>());
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}).ToArray();
|
2026-10-10 19:25:22 +02:00
|
|
|
|
|
|
|
|
|
|
var incoming = LockIndicator.Instantiate<PlaceUIElementWorldspace>();
|
|
|
|
|
|
incoming.gameObject.SetActive(false);
|
|
|
|
|
|
_incomingLockIndicator = (incoming, incoming.GetComponent<Rotate>());
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdatePlayerPanel()
|
|
|
|
|
|
{
|
|
|
|
|
|
ShipPanel.Display(CurrentEntity, true);
|
|
|
|
|
|
SchematicDisplay.ShowShip(CurrentEntity);
|
2026-06-16 20:42:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
// Mining Cut 3: any target gets the indicator; only a ship target gets the ship panel.
|
|
|
|
|
|
private void UpdateTargetPanel(TargetRef target)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
TargetIndicator.gameObject.SetActive(!target.IsNone);
|
|
|
|
|
|
TargetShipPanel.gameObject.SetActive(target.Entity != null);
|
|
|
|
|
|
if (target.Entity != null)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
TargetShipPanel.Display(target.Entity, true);
|
|
|
|
|
|
TargetSchematicDisplay.ShowShip(target.Entity, CurrentEntity);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
// A chunk's pose is planar (Zone.ChunkPose), read at the drawn time so the reticle sits on the drawn rock; the renderer
|
|
|
|
|
|
// draws it at the asteroid layer's height.
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
private float3 ChunkWorldPosition(ChunkId chunk)
|
|
|
|
|
|
{
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
var zone = CurrentEntity.Zone;
|
|
|
|
|
|
var pose = zone.ChunkPose(chunk.Field, chunk.Index, DrawAhead.Time(zone, Clock.Lead));
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
return float3(pose.x, Settings.PlanetSettings.AsteroidVerticalOffset, pose.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 01:13:09 +02:00
|
|
|
|
// The one way a run ends, won or lost: the run is cleared and the main menu returns.
|
|
|
|
|
|
private void EndRun()
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
|
|
|
|
|
UnbindEntity();
|
|
|
|
|
|
CurrentEntity = null;
|
|
|
|
|
|
MainMenu.gameObject.SetActive(true);
|
|
|
|
|
|
Menu.gameObject.SetActive(false);
|
|
|
|
|
|
CurrentGalaxy = null;
|
Land RunSave and the one-cache run and player lifecycle (Cut 10)
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:00:27 +02:00
|
|
|
|
RunSave.Clear(CultCache);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
SavePlayerSettings();
|
2026-10-07 01:13:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-16 21:18:52 +02:00
|
|
|
|
// The one way a run is won: RunGoal decides whether the exit is open (read before EndRun clears the galaxy), the
|
|
|
|
|
|
// caller shows its own refusal.
|
|
|
|
|
|
private bool TryWin()
|
2026-10-07 01:13:09 +02:00
|
|
|
|
{
|
2026-10-16 21:18:52 +02:00
|
|
|
|
if (!RunGoal.ExitOpen(Zone)) return false;
|
2026-10-07 01:13:09 +02:00
|
|
|
|
EndRun();
|
|
|
|
|
|
Dialog.Clear();
|
|
|
|
|
|
Dialog.Title.text = "You have escaped the Terminus. This was the demo; the whole of it is yet to come.";
|
|
|
|
|
|
Dialog.Show();
|
|
|
|
|
|
Dialog.MoveToCursor();
|
2026-10-16 21:18:52 +02:00
|
|
|
|
return true;
|
2026-10-07 01:13:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Die(CauseOfDeath cause)
|
|
|
|
|
|
{
|
|
|
|
|
|
var deathTime = Time.time;
|
|
|
|
|
|
EndRun();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Observable.EveryUpdate()
|
|
|
|
|
|
.Where(_ => Time.time - deathTime < DeathPostTransitionTime)
|
|
|
|
|
|
.Subscribe(_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = (Time.time - deathTime) / DeathPostTransitionTime;
|
|
|
|
|
|
if(cause==CauseOfDeath.Heatstroke)
|
|
|
|
|
|
{
|
|
|
|
|
|
HeatstrokePost.weight = 1 - t;
|
|
|
|
|
|
SevereHeatstrokePost.weight = 1 - t;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (cause == CauseOfDeath.Hypothermia)
|
|
|
|
|
|
{
|
|
|
|
|
|
HypothermiaPost.weight = 1 - t;
|
|
|
|
|
|
SevereHypothermiaPost.weight = 1 - t;
|
|
|
|
|
|
}
|
|
|
|
|
|
DeathPost.weight = t;
|
|
|
|
|
|
},
|
|
|
|
|
|
() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
HeatstrokePost.weight = 0;
|
|
|
|
|
|
SevereHeatstrokePost.weight = 0;
|
|
|
|
|
|
HypothermiaPost.weight = 0;
|
|
|
|
|
|
SevereHypothermiaPost.weight = 0;
|
|
|
|
|
|
DeathPost.weight = 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
// public void ToggleEditMode()
|
|
|
|
|
|
// {
|
|
|
|
|
|
// _editMode = !_editMode;
|
|
|
|
|
|
// FollowCamera.gameObject.SetActive(!_editMode);
|
|
|
|
|
|
// TopDownCamera.gameObject.SetActive(_editMode);
|
|
|
|
|
|
// }
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
public IEnumerable<EquippedCargoBay> AvailableCargoBays()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CurrentEntity.Parent != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var bay in CurrentEntity.Parent.DockingBays)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (bay.DockedShip.IsPlayerShip) yield return bay;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<Entity> AvailableEntities()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(DockedEntity != null)
|
|
|
|
|
|
foreach (var entity in DockedEntity.Children)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (entity is Ship { IsPlayerShip: true }) yield return entity;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (CurrentEntity != null)
|
|
|
|
|
|
yield return CurrentEntity;
|
|
|
|
|
|
}
|
2026-06-16 20:42:42 +01:00
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
public void PlayMusic(MusicType type)
|
2026-06-16 20:42:42 +01:00
|
|
|
|
{
|
2026-06-25 18:09:33 +01:00
|
|
|
|
// TODO: SFX: Music
|
2026-06-21 08:56:07 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 07:13:55 +02:00
|
|
|
|
// What the player's own instruments say about combat, nothing a hostile knows: a hostile tracked, a paint on the
|
|
|
|
|
|
// player, or a remembered hostile the player still has a bearing on (a reveal younger than BearingMemory).
|
|
|
|
|
|
private void UpdateMusic()
|
|
|
|
|
|
{
|
|
|
|
|
|
var combat = CurrentEntity.VisibleEnemies.Count > 0 || CurrentEntity.IncomingPaints.Count > 0;
|
2026-10-14 13:54:43 +02:00
|
|
|
|
combat |= CurrentEntity.RememberedHostiles.Any();
|
2026-10-13 07:13:55 +02:00
|
|
|
|
var type = combat ? MusicType.Combat : MusicType.Overworld;
|
|
|
|
|
|
if (_music == type) return;
|
|
|
|
|
|
_music = type;
|
|
|
|
|
|
PlayMusic(type);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// An indicator's fill is how far the player's own sensors have resolved the ship from a track toward identifying
|
|
|
|
|
|
// it (ContactOf.Info over the detection-to-gear span): nothing of what the ship sees of the player.
|
|
|
|
|
|
private void PlaceFill(VisibleTargetIndicator indicator, Entity subject)
|
|
|
|
|
|
{
|
|
|
|
|
|
indicator.Fill.enabled = subject.Active;
|
|
|
|
|
|
if (!subject.Active) return;
|
|
|
|
|
|
var settings = Settings.GameplaySettings;
|
|
|
|
|
|
indicator.Fill.fillAmount = saturate(unlerp(settings.TargetDetectionInfoThreshold, settings.TargetGearInfoThreshold,
|
|
|
|
|
|
CurrentEntity.ContactOf(subject).Info));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Whether an effect drawn at `subject`'s true position may be drawn: the player's own ship, or a ship its
|
|
|
|
|
|
// instruments track. The one seam for impact and hit effects (follow-up presentation-gated-on-tracked); a bearing
|
|
|
|
|
|
// alone (Detected without Tracked) places nothing there. No player ship, nothing to gate on.
|
|
|
|
|
|
public static bool DrawsAt(Entity subject) =>
|
|
|
|
|
|
Instance == null || Instance.CurrentEntity == null || subject == null || subject == Instance.CurrentEntity ||
|
|
|
|
|
|
Instance.CurrentEntity.ContactOf(subject).Tracked;
|
|
|
|
|
|
|
2026-06-25 18:09:33 +01:00
|
|
|
|
void Update()
|
2026-06-21 08:56:07 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if(!_paused)
|
|
|
|
|
|
{
|
|
|
|
|
|
_time += Time.deltaTime;
|
|
|
|
|
|
_hitMarkerTime -= Time.deltaTime;
|
|
|
|
|
|
if(HitMarker.activeSelf && _hitMarkerTime < 0) HitMarker.SetActive(false);
|
|
|
|
|
|
// ItemManager.Time = _time;
|
|
|
|
|
|
if(CurrentEntity !=null && CurrentEntity.Parent==null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var indicator in _visibleHostileIndicators)
|
|
|
|
|
|
{
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
indicator.Value.gameObject.SetActive(indicator.Key!=CurrentEntity.Target.Value.Entity);
|
2026-10-16 00:50:29 +02:00
|
|
|
|
indicator.Value.Place.Target = DrawAhead.Position(indicator.Key, Clock.Lead).ToUnity();
|
2026-10-13 07:13:55 +02:00
|
|
|
|
PlaceFill(indicator.Value, indicator.Key);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
foreach (var indicator in _visibleFriendlyIndicators)
|
|
|
|
|
|
{
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
indicator.Value.gameObject.SetActive(indicator.Key!=CurrentEntity.Target.Value.Entity);
|
2026-10-16 00:50:29 +02:00
|
|
|
|
indicator.Value.Place.Target = DrawAhead.Position(indicator.Key, Clock.Lead).ToUnity();
|
2026-10-13 07:13:55 +02:00
|
|
|
|
PlaceFill(indicator.Value, indicator.Key);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
var look = Input.Player.Look.ReadValue<Vector2>();
|
2026-09-17 17:22:09 +02:00
|
|
|
|
var sensitivity = PlayerSettings.InputSettings.Sensitivity;
|
|
|
|
|
|
_entityYawPitch = float2(_entityYawPitch.x + look.x * sensitivity.x, clamp(_entityYawPitch.y + look.y * sensitivity.y, -.45f * PI, .45f * PI));
|
2026-09-14 17:39:12 +02:00
|
|
|
|
_viewDirection = mul(float3(0, 0, 1), CultMath.float3x3.Euler(float3(_entityYawPitch.yx, 0), RotationOrder.YXZ));
|
2026-10-07 12:33:22 +02:00
|
|
|
|
CurrentEntity.Aim = _viewDirection;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
HeatstrokePost.weight = saturate(unlerp(0, Settings.GameplaySettings.SevereHeatstrokeRiskThreshold, CurrentEntity.Heatstroke));
|
|
|
|
|
|
var severeHeatstrokeLerp = saturate(unlerp(Settings.GameplaySettings.SevereHeatstrokeRiskThreshold, 1, CurrentEntity.Heatstroke));
|
|
|
|
|
|
SevereHeatstrokePost.weight =
|
|
|
|
|
|
severeHeatstrokeLerp + severeHeatstrokeLerp * (1 - severeHeatstrokeLerp) *
|
|
|
|
|
|
max(Settings.HeatstrokePhasingFloor, sin(Time.time * Settings.HeatstrokePhasingFrequency));
|
|
|
|
|
|
|
2026-10-07 12:33:22 +02:00
|
|
|
|
_faceAimMark.enabled = _helm.Latched;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if(CurrentEntity is Ship ship)
|
|
|
|
|
|
{
|
Convert engine types through CultMath.UnityBridge
CultMath has no implicit conversions to or from Vector*, Quaternion or
Color, so Assembly-CSharp converts explicitly with ToCultMath/ToUnity at
each engine boundary: FieldDriver, GuidedProjectile(Manager),
EntityInstance, ActionGameManager, HullCollider, GridObject, ShipInstance,
ShieldManager, ItemPickup, FieldTester, PlaceUIElementWorldspace,
MapMenuInput, MapRenderer, SectorRenderer and ZoneRenderer. CultMath math
calls that were handed Unity vectors now take converted values;
Vector3.Scale in FieldDriver becomes a float3 product, and ItemPickup's
Vector3.Dot on float3s becomes dot. packages-lock.json records the
cultmath git package at f0605367 and demotes com.unity.mathematics to a
transitive dependency.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:47:33 +02:00
|
|
|
|
ship.MovementDirection = Input.Player.Move.ReadValue<Vector2>().ToCultMath();
|
2026-10-07 12:33:22 +02:00
|
|
|
|
_helm.Held = Input.Player.FaceAim.IsPressed();
|
|
|
|
|
|
ship.Turn = _helm.Demand(ship, _viewDirection.xz, Input.Player.Turn.ReadValue<float>());
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 07:13:55 +02:00
|
|
|
|
UpdateMusic();
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
UpdateFireControlDebug(CurrentEntity.Target.Value);
|
|
|
|
|
|
var target = CurrentEntity.Target.Value.Entity;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (target != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var threshold = Settings.GameplaySettings.TargetDetectionInfoThreshold;
|
2026-10-13 07:13:55 +02:00
|
|
|
|
TargetVisibilityFill.fillAmount = lerp(.25f, .75f, (CurrentEntity.ContactOf(target).Info - threshold)/(1-threshold));
|
|
|
|
|
|
VisibilityToTargetFill.fillAmount = lerp(.25f, .75f, saturate(CurrentEntity.Visibility / OwnSignatureFullScale));
|
2026-08-10 14:57:50 +01:00
|
|
|
|
TargetHitpointsFill.fillAmount = lerp(.25f, .75f, target.Hull.Durability / target.EquippedHull.Data.Durability);
|
|
|
|
|
|
TargetShieldsFill.fillAmount = target.Shield == null ? 0 : lerp(.25f, .75f, target.Shield.Progress);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 05:00:22 +02:00
|
|
|
|
CurrentEntity.TractorCommand = Input.Player.TractorBeam.ReadValue<float>();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-08 18:26:02 +02:00
|
|
|
|
// One clock steps the zone, paused or not: a paused frame feeds it no real time.
|
2026-10-08 23:39:30 +02:00
|
|
|
|
// A zone entered inside a step (a wormhole) ends this frame's stepping: the rest of its steps are the old zone's.
|
|
|
|
|
|
void EnterNewZone() { if (_zoneEntered) { Clock.Reset(); _zoneEntered = false; } }
|
|
|
|
|
|
EnterNewZone();
|
|
|
|
|
|
var steppedZone = Zone;
|
2026-10-14 13:54:43 +02:00
|
|
|
|
Clock.Advance(_paused ? 0f : Time.deltaTime, delta =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Zone != steppedZone) return;
|
|
|
|
|
|
steppedZone.Update(delta);
|
2026-10-14 17:53:58 +02:00
|
|
|
|
// A warning lives one tick, so sweeps are collected per tick, never read per frame.
|
|
|
|
|
|
if (CurrentEntity != null && CurrentEntity.Zone == steppedZone)
|
2026-10-14 13:54:43 +02:00
|
|
|
|
foreach (var warning in CurrentEntity.IncomingPaints)
|
|
|
|
|
|
if (warning.Mode == PaintMode.Search) _pendingSweeps.Add((warning.Bearing, steppedZone.Time));
|
|
|
|
|
|
});
|
2026-10-08 23:39:30 +02:00
|
|
|
|
EnterNewZone();
|
2026-06-16 20:42:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
No-lock fused rounds: aim, invalid targets, contact stops, outlived shooters
Operator rulings 2026-09-30 after Soul's pass on 8c002637:
- A no-lock round flies along Entity.LookDirection restricted by the mount's
arc (FireControl.AimDirection: inside InArc's cone as is, outside it turned
to the cone's edge; no aim falls back to the mount). The simulation had no
aim clamp before; the old code used the hull-fixed mount direction.
- An invalid target (out of range, out of arc, not visible, not locked) counts
as no lock for a fused round. PFire's gate is the one decider and now also
reports `bears`; Fire reads it. The round carries no target afterwards.
- A no-lock Contact round bursts at the first hull its aim line crosses
before max range, read through Lane's own cell admission (CellsOnLine,
split out of Lane). Proximity and Delayed keep the max-range burst. Such a
round commits at Fire, since it has no target facing to read late, and
arrives when it has flown to the burst point. The seam for mining chunks is
FireControl.ContactDistance.
- A round outlives its shooter: Step no longer cancels a shot whose source
left the zone. Resolution reads the zone and the target; the shooter is an
identity a shot carries. PendingShot.FireOrigin freezes where it left from.
- ShotOutcome.Result (Miss, Hit, Burst) replaces the stored Hit bool, which
is now derived. A burst without a target is no longer reported as a miss,
and the debug HUD labels it BURST.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:27:43 +02:00
|
|
|
|
private static string ResultLabel(ShotResult result) => result switch
|
|
|
|
|
|
{
|
|
|
|
|
|
ShotResult.Hit => "HIT",
|
|
|
|
|
|
ShotResult.Burst => "BURST",
|
|
|
|
|
|
_ => "MISS"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
private void UpdateFireControlDebug(TargetRef target)
|
2026-09-20 21:58:44 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (DebugInfoText == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
EquippedItem selectedItem = null;
|
|
|
|
|
|
Weapon selectedWeapon = null;
|
|
|
|
|
|
foreach (var item in CurrentEntity.Equipment)
|
|
|
|
|
|
{
|
|
|
|
|
|
var weapon = item.Behaviors.OfType<Weapon>().FirstOrDefault(x => !(x is LockWeapon));
|
|
|
|
|
|
if (weapon == null) continue;
|
|
|
|
|
|
selectedItem = item;
|
|
|
|
|
|
selectedWeapon = weapon;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedWeapon == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
DebugInfoText.text = "FIRE CONTROL\nNo non-lock weapon";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
// Mining Cut 3: fire control prices ships; a chunk target reads as none there until Cut 4.
|
|
|
|
|
|
var d = FireControl.Inspect(selectedWeapon, CurrentEntity, target.Entity);
|
2026-09-20 21:58:44 +02:00
|
|
|
|
var pendingLine = "pending: none";
|
|
|
|
|
|
for (var i = CurrentEntity.Zone.PendingShots.Count - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
var shot = CurrentEntity.Zone.PendingShots[i];
|
|
|
|
|
|
if (shot.Source != CurrentEntity || shot.Weapon != selectedItem) continue;
|
|
|
|
|
|
if (shot.Committed)
|
No-lock fused rounds: aim, invalid targets, contact stops, outlived shooters
Operator rulings 2026-09-30 after Soul's pass on 8c002637:
- A no-lock round flies along Entity.LookDirection restricted by the mount's
arc (FireControl.AimDirection: inside InArc's cone as is, outside it turned
to the cone's edge; no aim falls back to the mount). The simulation had no
aim clamp before; the old code used the hull-fixed mount direction.
- An invalid target (out of range, out of arc, not visible, not locked) counts
as no lock for a fused round. PFire's gate is the one decider and now also
reports `bears`; Fire reads it. The round carries no target afterwards.
- A no-lock Contact round bursts at the first hull its aim line crosses
before max range, read through Lane's own cell admission (CellsOnLine,
split out of Lane). Proximity and Delayed keep the max-range burst. Such a
round commits at Fire, since it has no target facing to read late, and
arrives when it has flown to the burst point. The seam for mining chunks is
FireControl.ContactDistance.
- A round outlives its shooter: Step no longer cancels a shot whose source
left the zone. Resolution reads the zone and the target; the shooter is an
identity a shot carries. PendingShot.FireOrigin freezes where it left from.
- ShotOutcome.Result (Miss, Hit, Burst) replaces the stored Hit bool, which
is now derived. A burst without a target is no longer reported as a miss,
and the debug HUD labels it BURST.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:27:43 +02:00
|
|
|
|
pendingLine = $"shot {shot.ShotId}: committed {ResultLabel(shot.Outcome.Result)}";
|
2026-09-20 21:58:44 +02:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
var pDeviation = FireControl.DeviationProbability(shot, CurrentEntity.Zone.Time, out var deviation);
|
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
|
|
|
|
// Cut 12.2 (docs/fire-control-cut.md): retires the inline estimate (shot.PBase * pDeviation),
|
|
|
|
|
|
// a second, wrong copy of the model now that the target's facing enters at Commit -- this
|
|
|
|
|
|
// calls the same function Commit itself rolls against (TheHudEstimateIsTheCommitPrice).
|
|
|
|
|
|
var estimate = FireControl.CommitProbability(shot, CurrentEntity.Zone.Time, out _);
|
|
|
|
|
|
pendingLine = $"shot {shot.ShotId}: dev {deviation:F1}/{shot.Tracking:F1} x{pDeviation:F3} estimate {estimate:P1}";
|
2026-09-20 21:58:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var lastLine = _debugLastShots.TryGetValue(selectedItem, out var lastShot)
|
No-lock fused rounds: aim, invalid targets, contact stops, outlived shooters
Operator rulings 2026-09-30 after Soul's pass on 8c002637:
- A no-lock round flies along Entity.LookDirection restricted by the mount's
arc (FireControl.AimDirection: inside InArc's cone as is, outside it turned
to the cone's edge; no aim falls back to the mount). The simulation had no
aim clamp before; the old code used the hull-fixed mount direction.
- An invalid target (out of range, out of arc, not visible, not locked) counts
as no lock for a fused round. PFire's gate is the one decider and now also
reports `bears`; Fire reads it. The round carries no target afterwards.
- A no-lock Contact round bursts at the first hull its aim line crosses
before max range, read through Lane's own cell admission (CellsOnLine,
split out of Lane). Proximity and Delayed keep the max-range burst. Such a
round commits at Fire, since it has no target facing to read late, and
arrives when it has flown to the burst point. The seam for mining chunks is
FireControl.ContactDistance.
- A round outlives its shooter: Step no longer cancels a shot whose source
left the zone. Resolution reads the zone and the target; the shooter is an
identity a shot carries. PendingShot.FireOrigin freezes where it left from.
- ShotOutcome.Result (Miss, Hit, Burst) replaces the stored Hit bool, which
is now derived. A burst without a target is no longer reported as a miss,
and the debug HUD labels it BURST.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:27:43 +02:00
|
|
|
|
? $"last {lastShot.ShotId}: {ResultLabel(lastShot.Result)} cell {lastShot.Cell.x},{lastShot.Cell.y}"
|
2026-09-20 21:58:44 +02:00
|
|
|
|
: "last: none";
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
var gates = target.Chunk.HasValue ? "target: chunk"
|
|
|
|
|
|
: target.Entity == null
|
2026-09-20 21:58:44 +02:00
|
|
|
|
? "target: none"
|
Fuse fixes: arming holds for every burst, no-lock rounds stop on the first hull, settle before user code
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:31:59 +02:00
|
|
|
|
: $"gates designated {d.Designated} arc {d.InArc}";
|
2026-09-20 21:58:44 +02:00
|
|
|
|
|
Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
|
|
|
|
// A fused weapon's forecast is its outcome, not a hit chance: the burst point or the refusal.
|
|
|
|
|
|
var outcomeLine = d.Outcome == FireOutcome.Burst ? $"burst at {d.BurstReach:F0}"
|
|
|
|
|
|
: d.Outcome == FireOutcome.Refused ? "refused: arming distance exceeds range"
|
|
|
|
|
|
: $"base {d.PBase:P1}";
|
|
|
|
|
|
|
2026-09-20 21:58:44 +02:00
|
|
|
|
DebugInfoText.text =
|
2026-10-08 15:30:48 +02:00
|
|
|
|
$"FIRE CONTROL - {ItemManager.Title(selectedItem.EquippableItem)}\n" +
|
2026-09-20 21:58:44 +02:00
|
|
|
|
$"{gates}\n" +
|
|
|
|
|
|
$"range {d.Range:F0} [{d.MinRange:F0}..{d.MaxRange:F0}]\n" +
|
|
|
|
|
|
$"info {d.Info:F3}/{d.InfoDemandCeiling:F3} sensor {d.PSensor:F3}\n" +
|
|
|
|
|
|
$"accuracy {d.Accuracy:F3} spread {d.PSpread:F3} hull {d.POnHull:F3}\n" +
|
|
|
|
|
|
$"precision {d.Precision:F3} tracking {d.Tracking:F1}\n" +
|
2026-10-07 16:40:00 +02:00
|
|
|
|
$"mount {d.AngularVelocity:F1}/{d.TrackingRate:F1} x{d.PMount:F3}\n" +
|
Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
|
|
|
|
$"{outcomeLine}\n" +
|
2026-09-20 21:58:44 +02:00
|
|
|
|
$"{pendingLine}\n" +
|
|
|
|
|
|
lastLine;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
private void LateUpdate()
|
2026-06-16 20:42:42 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
UpdateTargetIndicators();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateTargetIndicators()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CurrentEntity == null || CurrentEntity.Parent != null) return;
|
|
|
|
|
|
|
Presenters read the drawn pose: HUD markers, wells, rounds, ping, shield hit, compass, loot view, guided aim
HUD target, lock, incoming-lock, bearing and sweep markers anchor on DrawAhead.Position / AtBearing at Clock.Lead.
GravityLowering's record fill is DrawAhead.Well (pure, pinned at non-zero lead). Rounds take DrawAhead.Time
instead of float Zone.Time + lead. Sensor ping, shield hit, compass icons, wormhole bearing, loot ViewOrigin and
the guided-projectile aim read the drawn pose. Not built in Unity yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:41:49 +02:00
|
|
|
|
var lead = Clock.Lead;
|
|
|
|
|
|
var ownDrawn = DrawAhead.Position(CurrentEntity, lead);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
ViewDot.Target = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint.position;
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
|
var target = CurrentEntity.Target.Value;
|
|
|
|
|
|
if (target.Chunk is ChunkId chunk)
|
|
|
|
|
|
TargetIndicator.Target = ChunkWorldPosition(chunk).ToUnity();
|
|
|
|
|
|
else if (target.Entity != null)
|
Presenters read the drawn pose: HUD markers, wells, rounds, ping, shield hit, compass, loot view, guided aim
HUD target, lock, incoming-lock, bearing and sweep markers anchor on DrawAhead.Position / AtBearing at Clock.Lead.
GravityLowering's record fill is DrawAhead.Well (pure, pinned at non-zero lead). Rounds take DrawAhead.Time
instead of float Zone.Time + lead. Sensor ping, shield hit, compass icons, wormhole bearing, loot ViewOrigin and
the guided-projectile aim read the drawn pose. Not built in Unity yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:41:49 +02:00
|
|
|
|
TargetIndicator.Target = DrawAhead.Position(target.Entity, lead).ToUnity();
|
|
|
|
|
|
var distance = length(ViewDot.Target.ToCultMath() - ownDrawn);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
foreach (var (_, barrels, crosshair) in _articulationGroups)
|
2026-06-16 20:42:42 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var averagePosition = Vector3.zero;
|
|
|
|
|
|
foreach (var barrel in barrels)
|
|
|
|
|
|
averagePosition += barrel.position + barrel.forward * distance;
|
|
|
|
|
|
averagePosition /= barrels.Length;
|
|
|
|
|
|
crosshair.Target = averagePosition;
|
2026-06-16 20:42:42 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
2026-10-14 21:11:37 +02:00
|
|
|
|
foreach (var (progress, hostileOnly, indicator, spin) in _progressIndicators)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
2026-10-14 21:11:37 +02:00
|
|
|
|
var value = progress();
|
|
|
|
|
|
var targetEntity = CurrentEntity.Target.Value.Entity;
|
|
|
|
|
|
var showIndicator = value > .01f && targetEntity != null && (!hostileOnly || CurrentEntity.IsHostileTo(targetEntity));
|
|
|
|
|
|
indicator.gameObject.SetActive(showIndicator);
|
|
|
|
|
|
if(showIndicator)
|
2026-08-10 14:57:50 +01:00
|
|
|
|
{
|
Presenters read the drawn pose: HUD markers, wells, rounds, ping, shield hit, compass, loot view, guided aim
HUD target, lock, incoming-lock, bearing and sweep markers anchor on DrawAhead.Position / AtBearing at Clock.Lead.
GravityLowering's record fill is DrawAhead.Well (pure, pinned at non-zero lead). Rounds take DrawAhead.Time
instead of float Zone.Time + lead. Sensor ping, shield hit, compass icons, wormhole bearing, loot ViewOrigin and
the guided-projectile aim read the drawn pose. Not built in Unity yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:41:49 +02:00
|
|
|
|
indicator.Target = DrawAhead.Position(targetEntity, lead).ToUnity();
|
2026-10-14 21:11:37 +02:00
|
|
|
|
ApplyLockProgress(indicator, spin, value);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-10 19:25:22 +02:00
|
|
|
|
|
2026-10-14 21:11:37 +02:00
|
|
|
|
// The strongest lock or scan on the player's ship, read only from IncomingPaints (never another ship's LockWeapon or
|
|
|
|
|
|
// Scanner). A Scan paint's Strength is the scan's progress (ruling scan-is-an-act). Search paints (ping sweeps) are
|
|
|
|
|
|
// this class's sweep flashes (UpdateSweepFlashes). Same curves as the outgoing indicator, driven by the warning's Strength.
|
2026-10-11 14:05:16 +02:00
|
|
|
|
var warnings = CurrentEntity.IncomingPaints;
|
2026-10-10 19:25:22 +02:00
|
|
|
|
var strongest = -1;
|
|
|
|
|
|
for (var w = 0; w < warnings.Count; w++)
|
2026-10-14 21:11:37 +02:00
|
|
|
|
if ((warnings[w].Mode is PaintMode.Track or PaintMode.Scan) && (strongest < 0 || warnings[w].Strength > warnings[strongest].Strength)) strongest = w;
|
2026-10-10 19:25:22 +02:00
|
|
|
|
var (incomingIndicator, incomingSpin) = _incomingLockIndicator;
|
|
|
|
|
|
incomingIndicator.gameObject.SetActive(strongest >= 0);
|
|
|
|
|
|
if (strongest >= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var warning = warnings[strongest];
|
Presenters read the drawn pose: HUD markers, wells, rounds, ping, shield hit, compass, loot view, guided aim
HUD target, lock, incoming-lock, bearing and sweep markers anchor on DrawAhead.Position / AtBearing at Clock.Lead.
GravityLowering's record fill is DrawAhead.Well (pure, pinned at non-zero lead). Rounds take DrawAhead.Time
instead of float Zone.Time + lead. Sensor ping, shield hit, compass icons, wormhole bearing, loot ViewOrigin and
the guided-projectile aim read the drawn pose. Not built in Unity yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:41:49 +02:00
|
|
|
|
incomingIndicator.Target = DrawAhead.AtBearing(CurrentEntity, warning.Bearing, IncomingLockIndicatorDistance, lead).ToUnity();
|
2026-10-10 23:29:27 +02:00
|
|
|
|
ApplyLockProgress(incomingIndicator, incomingSpin, warning.Strength);
|
2026-10-10 19:25:22 +02:00
|
|
|
|
}
|
2026-10-14 13:54:43 +02:00
|
|
|
|
|
|
|
|
|
|
UpdateBearingMarkers();
|
|
|
|
|
|
UpdateSweepFlashes();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// One marker per remembered hostile (a contact held by a bearing only): at the bearing the book keeps, labelled with the
|
|
|
|
|
|
// classes it claimed. Never at the subject's position, never read from its gear.
|
|
|
|
|
|
private void UpdateBearingMarkers()
|
|
|
|
|
|
{
|
|
|
|
|
|
var remembered = CurrentEntity.RememberedHostiles.ToList();
|
|
|
|
|
|
_markerScratch.Clear();
|
|
|
|
|
|
foreach (var subject in _bearingMarkers.Keys)
|
|
|
|
|
|
if (!remembered.Contains(subject)) _markerScratch.Add(subject);
|
|
|
|
|
|
foreach (var subject in _markerScratch)
|
|
|
|
|
|
{
|
|
|
|
|
|
RetireMarker(_bearingMarkers[subject]);
|
|
|
|
|
|
_bearingMarkers.Remove(subject);
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach (var subject in remembered)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_bearingMarkers.TryGetValue(subject, out var marker))
|
|
|
|
|
|
{
|
|
|
|
|
|
marker = HostileTargetIndicator.Instantiate<VisibleTargetIndicator>();
|
|
|
|
|
|
marker.Fill.enabled = false;
|
|
|
|
|
|
_bearingMarkers.Add(subject, marker);
|
|
|
|
|
|
}
|
|
|
|
|
|
marker.gameObject.SetActive(true);
|
|
|
|
|
|
var bearing = CurrentEntity.ContactOf(subject).Bearing;
|
Presenters read the drawn pose: HUD markers, wells, rounds, ping, shield hit, compass, loot view, guided aim
HUD target, lock, incoming-lock, bearing and sweep markers anchor on DrawAhead.Position / AtBearing at Clock.Lead.
GravityLowering's record fill is DrawAhead.Well (pure, pinned at non-zero lead). Rounds take DrawAhead.Time
instead of float Zone.Time + lead. Sensor ping, shield hit, compass icons, wormhole bearing, loot ViewOrigin and
the guided-projectile aim read the drawn pose. Not built in Unity yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:41:49 +02:00
|
|
|
|
marker.Place.Target = DrawAhead.AtBearing(CurrentEntity, bearing, BearingMarkerDistance, Clock.Lead).ToUnity();
|
2026-10-14 13:54:43 +02:00
|
|
|
|
marker.ShowLabel(string.Join(", ", CurrentEntity.ClaimedClasses(subject).Select(c => c.Name)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// A pooled indicator goes back as the next hostile indicator will want it: label hidden, fill restored.
|
|
|
|
|
|
private static void RetireMarker(VisibleTargetIndicator marker)
|
|
|
|
|
|
{
|
|
|
|
|
|
marker.ShowLabel(null);
|
|
|
|
|
|
marker.Fill.enabled = true;
|
|
|
|
|
|
marker.GetComponent<Prototype>().ReturnToPool();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void RetireFlash(PlaceUIElementWorldspace indicator, Image image)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (image != null) image.color = new Color(image.color.r, image.color.g, image.color.b, 1);
|
|
|
|
|
|
indicator.GetComponent<Prototype>().ReturnToPool();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// A ping crossing the player draws one flash toward the pinger, fading over SearchFlashSeconds of zone time.
|
|
|
|
|
|
private void UpdateSweepFlashes()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var (bearing, time) in _pendingSweeps)
|
|
|
|
|
|
{
|
|
|
|
|
|
var indicator = LockIndicator.Instantiate<PlaceUIElementWorldspace>();
|
2026-10-14 16:12:44 +02:00
|
|
|
|
ApplyLockProgress(indicator, indicator.GetComponent<Rotate>(), 0); // a pooled lock indicator keeps its last tightening
|
2026-10-14 13:54:43 +02:00
|
|
|
|
_sweepFlashes.Add((indicator, indicator.GetComponentInChildren<Image>(), bearing, time));
|
|
|
|
|
|
}
|
|
|
|
|
|
_pendingSweeps.Clear();
|
|
|
|
|
|
for (var i = _sweepFlashes.Count - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (indicator, image, bearing, time) = _sweepFlashes[i];
|
|
|
|
|
|
var age = Zone.Time - time;
|
|
|
|
|
|
if (age < 0 || age >= SearchFlashSeconds)
|
|
|
|
|
|
{
|
|
|
|
|
|
RetireFlash(indicator, image);
|
|
|
|
|
|
_sweepFlashes.RemoveAt(i);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
Presenters read the drawn pose: HUD markers, wells, rounds, ping, shield hit, compass, loot view, guided aim
HUD target, lock, incoming-lock, bearing and sweep markers anchor on DrawAhead.Position / AtBearing at Clock.Lead.
GravityLowering's record fill is DrawAhead.Well (pure, pinned at non-zero lead). Rounds take DrawAhead.Time
instead of float Zone.Time + lead. Sensor ping, shield hit, compass icons, wormhole bearing, loot ViewOrigin and
the guided-projectile aim read the drawn pose. Not built in Unity yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:41:49 +02:00
|
|
|
|
indicator.Target = DrawAhead.AtBearing(CurrentEntity, bearing, IncomingLockIndicatorDistance, Clock.Lead).ToUnity();
|
2026-10-14 13:54:43 +02:00
|
|
|
|
if (image != null) image.color = new Color(image.color.r, image.color.g, image.color.b, 1 - age / SearchFlashSeconds);
|
|
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
2026-10-10 23:29:27 +02:00
|
|
|
|
|
|
|
|
|
|
// The one lock-indicator curve: outgoing (the player's own lock) and incoming (the warning's strength) both tighten through it.
|
|
|
|
|
|
private void ApplyLockProgress(PlaceUIElementWorldspace indicator, Rotate spin, float progress)
|
|
|
|
|
|
{
|
|
|
|
|
|
indicator.NoiseAmplitude = Settings.GameplaySettings.LockIndicatorNoiseAmplitude * (1 - progress);
|
|
|
|
|
|
indicator.NoiseFrequency = Settings.GameplaySettings.LockIndicatorFrequency.Evaluate(progress);
|
|
|
|
|
|
spin.Speed = Settings.GameplaySettings.LockSpinSpeed.Evaluate(progress);
|
|
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public abstract class DragObject{}
|
2026-06-21 16:53:33 +01:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public class WeaponGroupDragObject : DragObject
|
|
|
|
|
|
{
|
|
|
|
|
|
public WeaponGroupDragObject(int group)
|
|
|
|
|
|
{
|
|
|
|
|
|
Group = group;
|
2026-06-21 16:53:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public int Group { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public abstract class ItemDragObject : DragObject
|
|
|
|
|
|
{
|
|
|
|
|
|
protected ItemDragObject(int2 originCellOffset, ItemInstance item)
|
|
|
|
|
|
{
|
|
|
|
|
|
OriginCellOffset = originCellOffset;
|
|
|
|
|
|
Item = item;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ItemInstance Item { get; }
|
|
|
|
|
|
public int2 OriginCellOffset { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class ItemInstanceDragObject : ItemDragObject
|
|
|
|
|
|
{
|
|
|
|
|
|
public ItemInstanceDragObject(ItemInstance item, EquippedCargoBay originInventory, int2 originCellOffset) : base(originCellOffset, item)
|
2026-06-21 17:10:27 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
OriginInventory = originInventory;
|
2026-06-21 17:10:27 +01:00
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
|
|
public EquippedCargoBay OriginInventory { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class EquippedItemDragObject : ItemDragObject
|
|
|
|
|
|
{
|
|
|
|
|
|
public EquippedItemDragObject(EquippedItem item, Entity originEntity, int2 originCellOffset) : base(originCellOffset, item.EquippableItem)
|
|
|
|
|
|
{
|
|
|
|
|
|
EquippedItem = item;
|
|
|
|
|
|
OriginEntity = originEntity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public EquippedItem EquippedItem { get; }
|
|
|
|
|
|
public Entity OriginEntity { get; }
|
2025-01-30 01:58:05 +00:00
|
|
|
|
}
|