Compare commits
253 changed files with 7189 additions and 9150 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -95,6 +95,7 @@ Packages/packages-lock.json
|
|||
GameData/run.cc
|
||||
GameData/player.cc
|
||||
GameData/RemovedMods/
|
||||
Vault/Aetheria/.obsidian/plugins/copilot/data.json
|
||||
|
||||
# Agent scratch, sandboxed toolchain caches, and throwaway checkouts. These had
|
||||
# grown to 47 untracked entries; the worktree must stay legible, so they never
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
[lfs]
|
||||
url = https://eureka.gamecult.org/forge/GameCult/Aetheria.git/info/lfs
|
||||
|
|
@ -8,12 +8,12 @@ using UnityEditor.AddressableAssets.Settings;
|
|||
using UnityEditor.Build.Reporting;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
// The one way to make a player directory, for Windows or Linux. Owns what the directory contains: the player and its data,
|
||||
// The one way to make a Win64 player directory. Owns what the directory contains: Aetheria.exe and its data,
|
||||
// GameData/Aetheria.cc, GameData/Narrative/**, GameData/Mods/** and ModTools/AetherDb. Manual builds, smokes and
|
||||
// release cuts all call this and stage nothing themselves.
|
||||
//
|
||||
// Unity -batchmode -quit -projectPath <repo> -executeMethod AetheriaBuild.Build
|
||||
// -aetheriaVersion <semver> -aetheriaOut <dir> [-aetheriaTarget win64|linux64] -logFile <log>
|
||||
// -aetheriaVersion <semver> -aetheriaOut <dir> -logFile <log>
|
||||
//
|
||||
// AetherDb is published from tools/AetherDb, so the headless build's CULTLIB_ROOT / CULTMATH_ROOT (Directory.Build.props)
|
||||
// must be set for dotnet when the defaults do not hold the pinned revisions.
|
||||
|
|
@ -22,14 +22,6 @@ public static class AetheriaBuild
|
|||
{
|
||||
private static readonly Regex SemVer = new Regex(@"^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$");
|
||||
|
||||
// A target: the player Unity builds, its executable, and the runtime AetherDb is published for.
|
||||
private static (BuildTarget target, string player, string runtime, string aetherDb) Target(string name) => name switch
|
||||
{
|
||||
"win64" => (BuildTarget.StandaloneWindows64, "Aetheria.exe", "win-x64", "AetherDb.exe"),
|
||||
"linux64" => (BuildTarget.StandaloneLinux64, "Aetheria.x86_64", "linux-x64", "AetherDb"),
|
||||
_ => throw new InvalidOperationException($"-aetheriaTarget {name}: expected win64 or linux64"),
|
||||
};
|
||||
|
||||
public static void Build()
|
||||
{
|
||||
var step = "arguments";
|
||||
|
|
@ -38,7 +30,6 @@ public static class AetheriaBuild
|
|||
var version = Argument("-aetheriaVersion");
|
||||
if (!SemVer.IsMatch(version)) throw new InvalidOperationException("-aetheriaVersion is not a semver");
|
||||
var output = Path.GetFullPath(Argument("-aetheriaOut"));
|
||||
var target = Target(Argument("-aetheriaTarget", "win64"));
|
||||
var repoRoot = Directory.GetParent(UnityEngine.Application.dataPath).FullName;
|
||||
|
||||
step = "version";
|
||||
|
|
@ -55,8 +46,8 @@ public static class AetheriaBuild
|
|||
var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions
|
||||
{
|
||||
scenes = scenes,
|
||||
locationPathName = Path.Combine(output, target.player),
|
||||
target = target.target,
|
||||
locationPathName = Path.Combine(output, "Aetheria.exe"),
|
||||
target = BuildTarget.StandaloneWindows64,
|
||||
options = BuildOptions.None,
|
||||
});
|
||||
if (report.summary.result != BuildResult.Succeeded || report.summary.totalErrors != 0)
|
||||
|
|
@ -66,7 +57,7 @@ public static class AetheriaBuild
|
|||
PlayerStaging.Stage(repoRoot, output);
|
||||
|
||||
step = "aetherdb";
|
||||
Publish(repoRoot, Path.Combine(output, "ModTools"), target.runtime, target.aetherDb);
|
||||
Publish(repoRoot, Path.Combine(output, "ModTools"));
|
||||
|
||||
step = "burst";
|
||||
var burst = Path.Combine(output, "Aetheria_BurstDebugInformation_DoNotShip");
|
||||
|
|
@ -83,11 +74,11 @@ public static class AetheriaBuild
|
|||
EditorApplication.Exit(0);
|
||||
}
|
||||
|
||||
private static void Publish(string repoRoot, string modTools, string runtime, string aetherDb)
|
||||
private static void Publish(string repoRoot, string modTools)
|
||||
{
|
||||
var info = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
Arguments = $"publish tools/AetherDb -c Release -r {runtime} --self-contained -p:PublishSingleFile=true -o \"{modTools}\"",
|
||||
Arguments = $"publish tools/AetherDb -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o \"{modTools}\"",
|
||||
WorkingDirectory = repoRoot,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
|
|
@ -101,16 +92,15 @@ public static class AetheriaBuild
|
|||
if (process.ExitCode != 0)
|
||||
throw new InvalidOperationException($"dotnet publish exited {process.ExitCode}:\n{text}\n{errors.Result}");
|
||||
}
|
||||
if (!File.Exists(Path.Combine(modTools, aetherDb)))
|
||||
throw new InvalidOperationException($"dotnet publish produced no ModTools/{aetherDb}");
|
||||
if (!File.Exists(Path.Combine(modTools, "AetherDb.exe")))
|
||||
throw new InvalidOperationException("dotnet publish produced no ModTools/AetherDb.exe");
|
||||
}
|
||||
|
||||
private static string Argument(string name, string fallback = null)
|
||||
private static string Argument(string name)
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
var at = Array.IndexOf(args, name);
|
||||
if (at < 0 || at + 1 >= args.Length)
|
||||
return fallback ?? throw new InvalidOperationException(name + " is required");
|
||||
if (at < 0 || at + 1 >= args.Length) throw new InvalidOperationException(name + " is required");
|
||||
return args[at + 1];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ namespace Aetheria.EditorTools
|
|||
MaxRange = 1e6f, FireRange = 1e6f, CommitTime = 1e5f, ArrivalTime = 2e5f
|
||||
};
|
||||
var projectile = new GameObject("Wiring Round").AddComponent<Projectile>();
|
||||
projectile.Launch(null, zone, shot, new Vector3(0, 0, 0));
|
||||
projectile.Launch(zone, shot, new Vector3(0, 0, 0));
|
||||
yield return null;
|
||||
|
||||
zone.Update(.5f);
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// A render of the New Game screen as the game builds it: the real Main Menu scene's panel prototype, docked and filled
|
||||
// by ScenarioPicker.Dock and ScenarioPicker.Show with every scenario group, on a plain dark background (the cloud
|
||||
// backdrop is a play-mode renderer). Writes <project>/Temp/scenario-picker.png at 1920x1080, or the path after -pngOut (Unity empties Temp on exit).
|
||||
//
|
||||
// Unity -batchmode -projectPath <project> -executeMethod ScenarioPickerCapture.Run [-pngOut <file>] -logFile <log>
|
||||
//
|
||||
// Needs a GPU device; do not pass -nographics or -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
|
||||
public static class ScenarioPickerCapture
|
||||
{
|
||||
private const int Width = 1920, Height = 1080;
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
Capture();
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// The menu's two panes built the way MainMenu.Start builds them, against the Main Menu scene.
|
||||
internal static (MainMenu menu, PropertiesPanel list, PropertiesPanel detail) Build()
|
||||
{
|
||||
EditorSceneManager.OpenScene("Assets/Scenes/Main Menu.unity", OpenSceneMode.Single);
|
||||
var menu = UnityEngine.Object.FindObjectsByType<MainMenu>(FindObjectsInactive.Include, FindObjectsSortMode.None)[0];
|
||||
menu.PanelPrototype.gameObject.SetActive(false);
|
||||
var list = menu.PanelPrototype.Instantiate<PropertiesPanel>();
|
||||
var detail = menu.PanelPrototype.Instantiate<PropertiesPanel>();
|
||||
// Edit mode runs no Awake, which is what activates each row a panel adds.
|
||||
list.Awake();
|
||||
detail.Awake();
|
||||
ScenarioPicker.Dock(detail, list);
|
||||
return (menu, list, detail);
|
||||
}
|
||||
|
||||
internal static List<(string label, Scenario[] scenarios)> AllGroups() => new List<(string label, Scenario[] scenarios)>
|
||||
{
|
||||
("modes", Scenarios.Modes), ("development", Scenarios.Development), ("test arenas", Scenarios.Tests)
|
||||
};
|
||||
|
||||
private static void Capture()
|
||||
{
|
||||
var (menu, list, detail) = Build();
|
||||
menu.Dialog.gameObject.SetActive(false);
|
||||
list.Title.text = MainMenu.TitleSubtitle("new game", "scenarios");
|
||||
ScenarioPicker.Show(list, detail, AllGroups(), _ => { }, () => { }, menu.Dialog.Confirm);
|
||||
list.RefreshValues();
|
||||
detail.RefreshValues();
|
||||
|
||||
var canvas = menu.GetComponentInParent<Canvas>(true).rootCanvas;
|
||||
var target = new RenderTexture(Width, Height, 24, RenderTextureFormat.ARGB32);
|
||||
var cameraObject = new GameObject("ScenarioPickerCapture camera");
|
||||
var camera = cameraObject.AddComponent<Camera>();
|
||||
camera.orthographic = true;
|
||||
camera.orthographicSize = 5;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = new Color(.04f, .05f, .08f);
|
||||
camera.cullingMask = 1 << LayerMask.NameToLayer("UI");
|
||||
camera.targetTexture = target;
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = camera;
|
||||
canvas.planeDistance = 10;
|
||||
canvas.gameObject.SetActive(true);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform) list.transform);
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform) detail.transform);
|
||||
foreach (var text in list.transform.GetComponentsInChildren<TMPro.TMP_Text>())
|
||||
text.ForceMeshUpdate();
|
||||
Canvas.ForceUpdateCanvases();
|
||||
list.Content.GetComponentInParent<ScrollRect>().onValueChanged.Invoke(Vector2.zero);
|
||||
camera.Render();
|
||||
|
||||
var previous = RenderTexture.active;
|
||||
RenderTexture.active = target;
|
||||
var read = new Texture2D(Width, Height, TextureFormat.RGBA32, false);
|
||||
read.ReadPixels(new Rect(0, 0, Width, Height), 0, 0);
|
||||
RenderTexture.active = previous;
|
||||
|
||||
var background = read.GetPixel(0, 0);
|
||||
var differing = 0;
|
||||
foreach (var pixel in read.GetPixels32())
|
||||
if (Mathf.Abs(pixel.r / 255f - background.r) + Mathf.Abs(pixel.g / 255f - background.g) + Mathf.Abs(pixel.b / 255f - background.b) > .05f)
|
||||
differing++;
|
||||
if (differing < Width * Height / 100)
|
||||
throw new InvalidOperationException($"The capture is uniform: only {differing} pixels differ from the background.");
|
||||
|
||||
var path = Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Temp", "scenario-picker.png"));
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
var outIndex = Array.IndexOf(args, "-pngOut");
|
||||
if (outIndex >= 0) path = Path.GetFullPath(args[outIndex + 1]);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
File.WriteAllBytes(path, read.EncodeToPNG());
|
||||
Debug.Log($"[ScenarioPickerCapture] {path} ({differing} of {Width * Height} pixels differ from the background)");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2e79f280179150e4080250c45e07cd69
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// The picker's selection and keyboard rules, driven through the events the game delivers: Move and Cancel reach the
|
||||
// focused Launch button through ExecuteEvents, as the Input System UI module sends them, and a click is Button.onClick.
|
||||
//
|
||||
// Unity -batchmode -projectPath <project> -executeMethod ScenarioPickerVerify.Run -logFile <log>
|
||||
//
|
||||
// Exits the Editor itself, 0 when every rule holds and 1 on the first that does not.
|
||||
public static class ScenarioPickerVerify
|
||||
{
|
||||
private sealed class Fake : Scenario
|
||||
{
|
||||
private readonly uint _seed;
|
||||
private readonly bool _ambient;
|
||||
public Fake(string name, uint seed = 0, bool ambient = true) { Name = name; _seed = seed; _ambient = ambient; }
|
||||
public override string Name { get; }
|
||||
public override string Id => Name.ToLowerInvariant();
|
||||
public override string Brief => $"brief of {Name}";
|
||||
public override uint Seed => _seed;
|
||||
public override bool Ambient => _ambient;
|
||||
public override Galaxy Generate(GalaxyStage stage) => throw new NotSupportedException();
|
||||
public override void Stage(ScenarioStage stage) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private sealed class Screen
|
||||
{
|
||||
public ScenarioPicker Picker;
|
||||
public PropertiesPanel List, Detail;
|
||||
public Button[] Rows;
|
||||
public Button Back, Launch;
|
||||
public readonly List<Scenario> Launched = new List<Scenario>();
|
||||
public int Backs;
|
||||
public Scenario[] A, B, C;
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
SelectionFollowsClicksAndArrows();
|
||||
ArrowsStopAtTheEnds();
|
||||
LeftAndRightAreIgnored();
|
||||
LaunchIsOnePathAndFocusClears();
|
||||
CancelAndBackAreOnePath();
|
||||
DetailPaneFollowsSelection();
|
||||
OneGroupHasNoSectionLabel();
|
||||
LaunchesOnceHoweverItIsReached();
|
||||
ArrowingThroughTheRealListKeepsTheSelectionVisible();
|
||||
Debug.Log("[ScenarioPickerVerify] ok");
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Require(bool condition, string what)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(what);
|
||||
}
|
||||
|
||||
private static Screen Build(bool oneGroup = false, bool real = false)
|
||||
{
|
||||
var (menu, list, detail) = ScenarioPickerCapture.Build();
|
||||
// Build opens the scene afresh, which destroys the previous EventSystem.
|
||||
var events = new GameObject("verify EventSystem").AddComponent<EventSystem>();
|
||||
// Edit mode never calls OnEnable, which is what registers an EventSystem as current.
|
||||
typeof(EventSystem).GetMethod("OnEnable", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(events, null);
|
||||
var screen = new Screen
|
||||
{
|
||||
List = list, Detail = detail,
|
||||
A = new Scenario[] { new Fake("Alpha", 7), new Fake("Beta") },
|
||||
B = new Scenario[] { new Fake("Gamma", ambient: false) },
|
||||
C = new Scenario[] { new Fake("Delta"), new Fake("Echo") }
|
||||
};
|
||||
var groups = real ? ScenarioPickerCapture.AllGroups() : oneGroup
|
||||
? new List<(string label, Scenario[] scenarios)> { ("modes", screen.A) }
|
||||
: new List<(string label, Scenario[] scenarios)> { ("modes", screen.A), ("development", screen.B), ("test arenas", screen.C) };
|
||||
// The menu builds a panel while it is inactive and shows it afterwards; the picker must find its scroll all the same.
|
||||
if (real) list.gameObject.SetActive(false);
|
||||
screen.Picker = ScenarioPicker.Show(list, detail, groups, screen.Launched.Add, () => screen.Backs++, menu.Dialog.Confirm);
|
||||
var buttons = list.GetComponentsInChildren<PropertyButton>(false).Where(button => !button.transform.IsChildOf(detail.transform)).Select(button => button.Button).ToArray();
|
||||
screen.Rows = buttons.Take(buttons.Length - 1).ToArray();
|
||||
screen.Back = buttons.Last();
|
||||
screen.Launch = detail.GetComponentsInChildren<Button>(false).Single();
|
||||
if (real) { list.gameObject.SetActive(true); Layout(menu, list, detail); }
|
||||
return screen;
|
||||
}
|
||||
|
||||
// A live 1920x1080 layout, as the game has one a frame after Show; edit mode has no frames, so it is forced here.
|
||||
private static void Layout(MainMenu menu, PropertiesPanel list, PropertiesPanel detail)
|
||||
{
|
||||
menu.Dialog.gameObject.SetActive(false);
|
||||
var canvas = menu.GetComponentInParent<Canvas>(true).rootCanvas;
|
||||
var camera = new GameObject("verify camera").AddComponent<Camera>();
|
||||
camera.targetTexture = new RenderTexture(1920, 1080, 24);
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = camera;
|
||||
canvas.planeDistance = 10;
|
||||
canvas.gameObject.SetActive(true);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform) canvas.transform);
|
||||
foreach (var text in list.GetComponentsInChildren<TMP_Text>()) text.ForceMeshUpdate();
|
||||
Canvas.ForceUpdateCanvases();
|
||||
Scroll(list).onValueChanged.Invoke(Vector2.zero);
|
||||
}
|
||||
|
||||
private static ScrollRect Scroll(PropertiesPanel list) => list.Content.GetComponentInParent<ScrollRect>();
|
||||
|
||||
// Wholly inside the viewport, in world space, and not hidden by FitViewport's CanvasGroup.
|
||||
private static bool Shown(Button row, ScrollRect scroll)
|
||||
{
|
||||
var view = new Vector3[4];
|
||||
((RectTransform) scroll.viewport).GetWorldCorners(view);
|
||||
var rect = new Vector3[4];
|
||||
((RectTransform) row.transform).GetWorldCorners(rect);
|
||||
var alpha = 1f;
|
||||
for (var t = row.transform; t != null && t != scroll.content; t = t.parent)
|
||||
if (t.TryGetComponent<CanvasGroup>(out var group)) alpha = Mathf.Min(alpha, group.alpha);
|
||||
return rect[0].y >= view[0].y - .01f && rect[1].y <= view[1].y + .01f && alpha == 1;
|
||||
}
|
||||
|
||||
private static void Move(Screen screen, MoveDirection direction) =>
|
||||
ExecuteEvents.Execute(screen.Launch.gameObject, new AxisEventData(EventSystem.current) { moveDir = direction }, ExecuteEvents.moveHandler);
|
||||
|
||||
private static void Cancel(Screen screen) =>
|
||||
ExecuteEvents.Execute(screen.Launch.gameObject, new BaseEventData(EventSystem.current), ExecuteEvents.cancelHandler);
|
||||
|
||||
private static string Selected(Screen screen) => screen.Picker.Selected.Name;
|
||||
|
||||
private static void SelectionFollowsClicksAndArrows()
|
||||
{
|
||||
var screen = Build();
|
||||
Require(screen.Rows.Length == 5, $"Expected 5 scenario rows, found {screen.Rows.Length}.");
|
||||
Require(Selected(screen) == "Alpha", "The first scenario is not selected on Show.");
|
||||
Require(EventSystem.current.currentSelectedGameObject == screen.Launch.gameObject, "Launch is not focused on Show.");
|
||||
Move(screen, MoveDirection.Down);
|
||||
Require(Selected(screen) == "Beta", "Down did not select the next row.");
|
||||
Move(screen, MoveDirection.Down);
|
||||
Require(Selected(screen) == "Gamma", "Down did not cross into the next group.");
|
||||
Move(screen, MoveDirection.Up);
|
||||
Require(Selected(screen) == "Beta", "Up did not select the previous row.");
|
||||
screen.Rows[3].onClick.Invoke();
|
||||
Require(Selected(screen) == "Delta", "Clicking a name did not select it.");
|
||||
Require(screen.Launched.Count == 0, "Clicking a name launched it.");
|
||||
Require(EventSystem.current.currentSelectedGameObject == screen.Launch.gameObject, "A click on a name took focus off Launch.");
|
||||
Require(screen.Rows.All(row => row.navigation.mode == Navigation.Mode.None) && screen.Launch.navigation.mode == Navigation.Mode.None
|
||||
&& screen.Back.navigation.mode == Navigation.Mode.None, "A picker button still takes Unity navigation.");
|
||||
var accent = screen.Rows[3].targetGraphic.color;
|
||||
Require(screen.Rows.Where((row, i) => i != 3).All(row => row.targetGraphic.color != accent), "Only the selected row is drawn in the accent.");
|
||||
Move(screen, MoveDirection.Up);
|
||||
Require(screen.Rows[2].targetGraphic.color == accent && screen.Rows[3].targetGraphic.color != accent, "The accent did not follow the selection.");
|
||||
}
|
||||
|
||||
private static void ArrowsStopAtTheEnds()
|
||||
{
|
||||
var screen = Build();
|
||||
Move(screen, MoveDirection.Up);
|
||||
Require(Selected(screen) == "Alpha", "Up wrapped off the first row.");
|
||||
for (var i = 0; i < 10; i++) Move(screen, MoveDirection.Down);
|
||||
Require(Selected(screen) == "Echo", "Down wrapped or stopped short of the last row.");
|
||||
}
|
||||
|
||||
private static void LeftAndRightAreIgnored()
|
||||
{
|
||||
var screen = Build();
|
||||
Move(screen, MoveDirection.Down);
|
||||
Move(screen, MoveDirection.Left);
|
||||
Move(screen, MoveDirection.Right);
|
||||
Require(Selected(screen) == "Beta", "Left or Right moved the selection.");
|
||||
}
|
||||
|
||||
private static void LaunchIsOnePathAndFocusClears()
|
||||
{
|
||||
var screen = Build();
|
||||
Move(screen, MoveDirection.Down);
|
||||
screen.Launch.onClick.Invoke();
|
||||
Require(screen.Launched.Count == 1 && screen.Launched[0] == screen.A[1], "Launch did not launch the selected scenario once.");
|
||||
Require(EventSystem.current.currentSelectedGameObject == null, "Focus survived Launch, so a second Enter could launch again.");
|
||||
Require(screen.Backs == 0, "Launch went back.");
|
||||
}
|
||||
|
||||
private static void CancelAndBackAreOnePath()
|
||||
{
|
||||
var screen = Build();
|
||||
Cancel(screen);
|
||||
Require(screen.Backs == 1 && screen.Launched.Count == 0, "Cancel did not go back exactly once.");
|
||||
Require(EventSystem.current.currentSelectedGameObject == null, "Focus survived Cancel.");
|
||||
screen = Build();
|
||||
screen.Back.onClick.Invoke();
|
||||
Require(screen.Backs == 1 && screen.Launched.Count == 0, "The Back button did not go back exactly once.");
|
||||
Require(EventSystem.current.currentSelectedGameObject == null, "Focus survived Back.");
|
||||
}
|
||||
|
||||
private static string DetailText(Screen screen)
|
||||
{
|
||||
screen.Detail.RefreshValues();
|
||||
return string.Join("|", screen.Detail.GetComponentsInChildren<TextMeshProUGUI>(false).Select(text => text.text));
|
||||
}
|
||||
|
||||
private static void DetailPaneFollowsSelection()
|
||||
{
|
||||
var screen = Build();
|
||||
var first = DetailText(screen);
|
||||
Require(first.Contains("brief of Alpha") && first.Contains("7") && first.Contains("on"), $"Detail does not describe Alpha: {first}");
|
||||
Require(first.Contains("modes") && first.Contains("Alpha"), "Detail heading is not the group and name.");
|
||||
Move(screen, MoveDirection.Down);
|
||||
Move(screen, MoveDirection.Down);
|
||||
var third = DetailText(screen);
|
||||
Require(third.Contains("brief of Gamma") && third.Contains("new each launch") && third.Contains("off"), $"Detail does not describe Gamma: {third}");
|
||||
Require(third.Contains("development") && !third.Contains("Alpha"), "Detail heading did not follow the selection.");
|
||||
Require(screen.Detail.GetComponentsInChildren<Button>(false).Single() == screen.Launch, "Selecting rebuilt the Launch button.");
|
||||
}
|
||||
|
||||
private static void OneGroupHasNoSectionLabel()
|
||||
{
|
||||
var many = Build();
|
||||
var one = Build(oneGroup: true);
|
||||
Require(many.List.Children == 5 + 3 + 2 + 1, $"Three groups should list 5 rows, 3 section labels, 2 spacers and Back, not {many.List.Children} children.");
|
||||
Require(one.List.Children == 2 + 1, $"One group should list its 2 rows and Back, not {one.List.Children} children.");
|
||||
}
|
||||
|
||||
private static void LaunchesOnceHoweverItIsReached()
|
||||
{
|
||||
var screen = Build();
|
||||
screen.Launch.onClick.Invoke();
|
||||
screen.Launch.onClick.Invoke();
|
||||
ExecuteEvents.Execute(screen.Launch.gameObject, new BaseEventData(EventSystem.current), ExecuteEvents.submitHandler);
|
||||
Require(screen.Launched.Count == 1, $"Launch ran {screen.Launched.Count} times after a click, a second click and Submit.");
|
||||
Require(screen.Launch.navigation.mode == Navigation.Mode.None && Scroll(screen.List).verticalScrollbar.navigation.mode == Navigation.Mode.None,
|
||||
"The list scrollbar or Launch takes Unity navigation.");
|
||||
}
|
||||
|
||||
// The real scenario groups overflow the list's viewport; every row the arrows reach must end wholly visible.
|
||||
private static void ArrowingThroughTheRealListKeepsTheSelectionVisible()
|
||||
{
|
||||
var screen = Build(real: true);
|
||||
var scroll = Scroll(screen.List);
|
||||
var content = (RectTransform) scroll.content;
|
||||
var viewport = (RectTransform) scroll.viewport;
|
||||
Require(content.rect.height > viewport.rect.height + 1, $"The real list ({content.rect.height}) does not overflow its viewport ({viewport.rect.height}); the scroll rules are unmeasured.");
|
||||
Require(Shown(screen.Rows[0], scroll), "The first row is not shown on Show.");
|
||||
for (var i = 1; i < screen.Rows.Length; i++)
|
||||
{
|
||||
Move(screen, MoveDirection.Down);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
Require(Shown(screen.Rows[i], scroll), $"Down to row {i} ({Selected(screen)}) left it hidden or cut off.");
|
||||
}
|
||||
for (var i = screen.Rows.Length - 2; i >= 0; i--)
|
||||
{
|
||||
Move(screen, MoveDirection.Up);
|
||||
Canvas.ForceUpdateCanvases();
|
||||
Require(Shown(screen.Rows[i], scroll), $"Up to row {i} ({Selected(screen)}) left it hidden or cut off.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7ca0b5db32f745a43a8fdc67dfac2996
|
||||
|
|
@ -201,10 +201,6 @@ public class ActionGameManager : MonoBehaviour
|
|||
private float3 _viewDirection;
|
||||
// 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();
|
||||
// Seconds a held turn key takes to reach full turn rate.
|
||||
public float KeyboardTurnRampTime = .4f;
|
||||
// Whether the turn axis was last driven by keys; kept while released so the ramp down still applies.
|
||||
private bool _turnDigital;
|
||||
private TextMeshProUGUI _faceAimMark;
|
||||
private (HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)[] _articulationGroups;
|
||||
// The outgoing progress indicators on the player's target: one per lock weapon (its Lock, shown only on a hostile target)
|
||||
|
|
@ -228,10 +224,6 @@ public class ActionGameManager : MonoBehaviour
|
|||
private bool _menuShown;
|
||||
private List<ActionBarSlot> _actionBarSlots = new List<ActionBarSlot>();
|
||||
private List<InputAction> _actionBarActions = new List<InputAction>();
|
||||
// Each action bar slot's control path, index for index with _actionBarSlots.
|
||||
private List<string> _actionBarPaths = new List<string>();
|
||||
// Ships whose action bar has been set up: the default weapon-group binding runs once per ship, then the bar is the player's.
|
||||
private HashSet<Entity> _actionBarSetUp = new HashSet<Entity>();
|
||||
private float _hitMarkerTime;
|
||||
|
||||
public AetheriaInput Input { get; private set; }
|
||||
|
|
@ -256,7 +248,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
|
||||
public ItemManager ItemManager { get; private set; }
|
||||
public Zone Zone { get; private set; }
|
||||
public static SimClock Clock { get; } = SimClock.ForGame();
|
||||
public static SimClock Clock { get; } = new SimClock(1f / 60f);
|
||||
private bool _zoneEntered;
|
||||
|
||||
private readonly (float2 direction, string name)[] _directions = {
|
||||
|
|
@ -445,7 +437,6 @@ public class ActionGameManager : MonoBehaviour
|
|||
var slot = Instantiate(ActionBarSlot, ActionBar);
|
||||
slot.Binding = null;
|
||||
_actionBarSlots.Add(slot);
|
||||
_actionBarPaths.Add(controlPath);
|
||||
action.started += context => slot.Binding?.Activate();
|
||||
action.canceled += context => slot.Binding?.Deactivate();
|
||||
|
||||
|
|
@ -504,6 +495,16 @@ public class ActionGameManager : MonoBehaviour
|
|||
|
||||
StartGame();
|
||||
|
||||
//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",
|
||||
_ =>
|
||||
{
|
||||
|
|
@ -577,33 +578,6 @@ public class ActionGameManager : MonoBehaviour
|
|||
ItemManager.Random.NextFloat2Direction() * ItemManager.Random.NextFloat(50, 500);
|
||||
Zone.Admit(turret, piloted: false);
|
||||
});
|
||||
// "fly Djinni": a freshly fitted ship of the named hull beside the current one, and you in it. The ship you leave
|
||||
// stays where it is, unpiloted.
|
||||
ConsoleController.AddCommand("fly",
|
||||
args =>
|
||||
{
|
||||
var hullName = string.Join(" ", args);
|
||||
if (_currentEntity == null || _currentEntity.Parent != null)
|
||||
{
|
||||
ConsoleController.Instance.AppendLogLine("Refused: undock first");
|
||||
return;
|
||||
}
|
||||
var generator = new LoadoutGenerator(ref ItemManager.Random, ItemManager, CurrentGalaxy, Zone.GalaxyZone, null, 2);
|
||||
var pack = generator.GenerateShipLoadout(data => string.Equals(data.Name, hullName, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (pack == null)
|
||||
{
|
||||
ConsoleController.Instance.AppendLogLine($"No ship hull named \"{hullName}\"");
|
||||
return;
|
||||
}
|
||||
var ship = (Ship) EntitySerializer.Unpack(ItemManager, Zone, pack);
|
||||
ship.IsPlayerShip = true;
|
||||
ship.Direction = _currentEntity.Direction;
|
||||
ship.Position.xz = _currentEntity.Position.xz + _currentEntity.Direction.Rotate(ItemRotation.Clockwise) * 30;
|
||||
ship.Velocity = _currentEntity.Velocity;
|
||||
Zone.Admit(ship, piloted: false);
|
||||
UnbindEntity(); // or the ship you leave still owns your death
|
||||
BindToEntity(ship);
|
||||
});
|
||||
//Temporary, or not
|
||||
ConsoleController.AddCommand("tow", _ => TowShip());
|
||||
|
||||
|
|
@ -1021,35 +995,6 @@ public class ActionGameManager : MonoBehaviour
|
|||
if (DebugInfoText != null) DebugInfoText.text = "";
|
||||
}
|
||||
|
||||
// The weapon group a slot's input fires by default: mouse left, right and middle fire groups 1-3, and number key N fires group N.
|
||||
private static int DefaultWeaponGroup(string controlPath) => controlPath switch
|
||||
{
|
||||
"<Mouse>/leftButton" => 0,
|
||||
"<Mouse>/rightButton" => 1,
|
||||
"<Mouse>/middleButton" => 2,
|
||||
_ when controlPath.StartsWith("<Keyboard>/") && int.TryParse(controlPath.Substring("<Keyboard>/".Length), out var n) && n >= 1 => n - 1,
|
||||
_ => -1
|
||||
};
|
||||
|
||||
// A new ship's first action bar: each slot's default weapon group when the group has weapons. Gear and consumables the
|
||||
// player placed are kept; a slot holding another ship's weapon group is rebound to this ship, or cleared when its default
|
||||
// group here is empty.
|
||||
private void AutoBindWeaponGroups()
|
||||
{
|
||||
if (CurrentEntity?.WeaponGroups == null) return;
|
||||
for (var i = 0; i < _actionBarSlots.Count; i++)
|
||||
{
|
||||
var slot = _actionBarSlots[i];
|
||||
if (slot.Binding != null && slot.Binding is not ActionBarWeaponGroupBinding) continue;
|
||||
var group = DefaultWeaponGroup(_actionBarPaths[i]);
|
||||
if (group < 0) continue;
|
||||
if (group < CurrentEntity.WeaponGroups.Length && CurrentEntity.WeaponGroups[group].weapons.Any())
|
||||
slot.Binding = new ActionBarWeaponGroupBinding(CurrentEntity, slot, group);
|
||||
else if (slot.Binding is ActionBarWeaponGroupBinding)
|
||||
slot.Binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void BindToEntity(Entity entity)
|
||||
{
|
||||
if (!ZoneRenderer.EntityInstances.ContainsKey(entity))
|
||||
|
|
@ -1100,10 +1045,6 @@ public class ActionGameManager : MonoBehaviour
|
|||
crosshair.gameObject.SetActive(false);
|
||||
foreach (var group in _articulationGroups)
|
||||
group.crosshair.gameObject.SetActive(true);
|
||||
|
||||
// A loaded run's bar is restored after this and replaces it, so a saved setup is never overwritten.
|
||||
if (_actionBarSetUp.Add(CurrentEntity))
|
||||
AutoBindWeaponGroups();
|
||||
|
||||
_shipSubscriptions.Add(CurrentEntity.Target.Subscribe(target =>
|
||||
{
|
||||
|
|
@ -1366,10 +1307,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
{
|
||||
ship.Command.Movement = Input.Player.Move.ReadValue<Vector2>().ToCultMath();
|
||||
_helm.Held = Input.Player.FaceAim.IsPressed();
|
||||
if (Input.Player.Turn.activeControl is { } turnControl)
|
||||
_turnDigital = turnControl.device is UnityEngine.InputSystem.Keyboard;
|
||||
var turnAxis = _helm.TurnAxis(Input.Player.Turn.ReadValue<float>(), _turnDigital, Time.deltaTime, KeyboardTurnRampTime);
|
||||
ship.Command.Turn = _helm.Demand(_viewDirection.xz, ship.BandedTurn(turnAxis));
|
||||
ship.Command.Turn = _helm.Demand(_viewDirection.xz, Input.Player.Turn.ReadValue<float>());
|
||||
}
|
||||
|
||||
UpdateMusic();
|
||||
|
|
@ -1497,18 +1435,11 @@ public class ActionGameManager : MonoBehaviour
|
|||
else if (target.Entity != null)
|
||||
TargetIndicator.Target = DrawAhead.Position(target.Entity, lead).ToUnity();
|
||||
var distance = length(ViewDot.Target.ToCultMath() - ownDrawn);
|
||||
// The aim's pitch, applied to each barrel's planar bearing: the crosshair sits where the guns point at the view's
|
||||
// elevation, not on the ship's level plane (which is the horizon).
|
||||
var aim = CurrentEntity.Aim;
|
||||
var aimLevel = length(aim.xz);
|
||||
foreach (var (_, barrels, crosshair) in _articulationGroups)
|
||||
{
|
||||
var averagePosition = Vector3.zero;
|
||||
foreach (var barrel in barrels)
|
||||
{
|
||||
var bearing = new Vector2(barrel.forward.x, barrel.forward.z).normalized;
|
||||
averagePosition += barrel.position + new Vector3(bearing.x * aimLevel, aim.y, bearing.y * aimLevel) * distance;
|
||||
}
|
||||
averagePosition += barrel.position + barrel.forward * distance;
|
||||
averagePosition /= barrels.Length;
|
||||
crosshair.Target = averagePosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,9 +408,6 @@ public class EntityInstance : MonoBehaviour
|
|||
var aimDistance = !Entity.Target.Value.IsNone
|
||||
? max(Entity.TargetRange, Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance)
|
||||
: 10000;
|
||||
// The aim's pitch: a planar firing bearing is drawn at the same elevation as the view, so a group aims where the
|
||||
// player looks, not at the horizon of the ship's plane.
|
||||
var aimLevel = length(Entity.Aim.xz);
|
||||
LookAtPoint.position = transform.position + Entity.Aim.ToUnity() * aimDistance;
|
||||
foreach (var group in _groupAims)
|
||||
{
|
||||
|
|
@ -418,7 +415,7 @@ public class EntityInstance : MonoBehaviour
|
|||
if (weapon == null) { group.Value.position = LookAtPoint.position; continue; }
|
||||
var barrel = Barrels.TryGetValue(weapon.Item.Hardpoint, out var barrels) ? barrels[0].position : transform.position;
|
||||
var direction = FireControl.Solution(weapon, Entity, Entity.Target.Value).Direction;
|
||||
group.Value.position = barrel + new Vector3(direction.x * aimLevel, Entity.Aim.y, direction.y * aimLevel) * aimDistance;
|
||||
group.Value.position = barrel + new Vector3(direction.x, 0, direction.y) * aimDistance;
|
||||
}
|
||||
if (_influenceInstance)
|
||||
_influenceInstance.position = new Vector3(drawn.x, 0, drawn.z);
|
||||
|
|
|
|||
|
|
@ -24,16 +24,19 @@ public class Projectile : MonoBehaviour
|
|||
private CultMath.float3 _barrel;
|
||||
private ShotOutcome _known;
|
||||
private bool _alive;
|
||||
private ProjectileManager _owner;
|
||||
private CompositeDisposable _binding;
|
||||
|
||||
// Takes the shot as the sim holds it, the zone whose sim time it is drawn at, and where the barrel is as this round is first seen.
|
||||
public void Launch(ProjectileManager owner, Zone zone, PendingShot shot, Vector3 barrel)
|
||||
public void Launch(Zone zone, PendingShot shot, Vector3 barrel)
|
||||
{
|
||||
_owner = owner;
|
||||
_zone = zone;
|
||||
_shot = shot;
|
||||
_barrel = new CultMath.float3(barrel.x, barrel.y, barrel.z);
|
||||
_known = null;
|
||||
_binding?.Dispose();
|
||||
_binding = new CompositeDisposable(
|
||||
zone.ShotCommitted.Where(o => o.ShotId == shot.ShotId).Subscribe(Committed),
|
||||
zone.ShotResolved.Where(o => o.ShotId == shot.ShotId).Subscribe(Resolve));
|
||||
_alive = true;
|
||||
transform.forward = new Vector3(shot.TravelDirection.x, 0, shot.TravelDirection.y);
|
||||
Draw();
|
||||
|
|
@ -53,14 +56,14 @@ public class Projectile : MonoBehaviour
|
|||
}
|
||||
|
||||
// The sim's commit: the record it holds now (a contact burst's arrival is shortened here) and what it decided.
|
||||
internal void Committed(ShotOutcome outcome)
|
||||
private void Committed(ShotOutcome outcome)
|
||||
{
|
||||
if (_zone.TryGetShot(outcome.ShotId, out var record)) _shot = record;
|
||||
_known = outcome;
|
||||
}
|
||||
|
||||
// The simulation's verdict. A round that hit or burst is drawn at its end and stops; a miss keeps flying.
|
||||
internal void Resolve(ShotOutcome outcome)
|
||||
private void Resolve(ShotOutcome outcome)
|
||||
{
|
||||
if (!_alive) return;
|
||||
_known = outcome;
|
||||
|
|
@ -75,14 +78,16 @@ public class Projectile : MonoBehaviour
|
|||
private void Finish()
|
||||
{
|
||||
_alive = false;
|
||||
_owner?.Forget(_shot.ShotId);
|
||||
_binding?.Dispose();
|
||||
_binding = null;
|
||||
StartCoroutine(Fade());
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
_alive = false;
|
||||
_owner?.Forget(_shot.ShotId);
|
||||
_binding?.Dispose();
|
||||
_binding = null;
|
||||
}
|
||||
|
||||
IEnumerator Fade()
|
||||
|
|
|
|||
|
|
@ -1,46 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
using UnityEngine;
|
||||
|
||||
public class ProjectileManager : InstantWeaponEffectManager
|
||||
{
|
||||
public Prototype ProjectilePrototype;
|
||||
|
||||
// The rounds this manager has in the air by shot id, told of the sim's commit and verdict by one subscription per zone: a
|
||||
// gun that fires hundreds of rounds in a step costs the dictionary an entry per round and nothing else (no subscription,
|
||||
// closure or disposable per round).
|
||||
private readonly Dictionary<int, Projectile> _live = new Dictionary<int, Projectile>();
|
||||
private readonly CompositeDisposable _listening = new CompositeDisposable();
|
||||
private Zone _zone;
|
||||
|
||||
// The round is drawn by DrawAhead from what the simulation publishes, so there is nothing to aim, jitter
|
||||
// or inherit here: spread is priced by FireControl, and the sim's round inherits nothing from its shooter.
|
||||
// A round the sim fired inside the step (rounds per step) is first seen where its shooter's barrel was at that moment:
|
||||
// the barrel as it is now, carried back along the shooter's motion for the time since.
|
||||
public override void Fire(InstantWeapon weapon, EquippedItem item, EntityInstance source, EntityInstance target, int shotId)
|
||||
{
|
||||
var zone = source.Entity.Zone;
|
||||
if (!zone.TryGetShot(shotId, out var shot)) return;
|
||||
Listen(zone);
|
||||
var age = (float) (zone.ExactTime - shot.FireTime);
|
||||
var velocity = source.Entity.Velocity;
|
||||
var p = ProjectilePrototype.Instantiate<Projectile>();
|
||||
_live[shotId] = p;
|
||||
p.Launch(this, zone, shot, source.GetBarrel(item.Hardpoint).position - new Vector3(velocity.x, 0, velocity.y) * age);
|
||||
p.Launch(zone, shot, source.GetBarrel(item.Hardpoint).position);
|
||||
p.Trail.Clear();
|
||||
}
|
||||
|
||||
internal void Forget(int shotId) => _live.Remove(shotId);
|
||||
|
||||
private void Listen(Zone zone)
|
||||
{
|
||||
if (_zone == zone) return;
|
||||
_listening.Clear();
|
||||
_live.Clear();
|
||||
_zone = zone;
|
||||
zone.ShotCommitted.Subscribe(outcome => { if (_live.TryGetValue(outcome.ShotId, out var p)) p.Committed(outcome); }).AddTo(_listening);
|
||||
zone.ShotResolved.Subscribe(outcome => { if (_live.TryGetValue(outcome.ShotId, out var p)) p.Resolve(outcome); }).AddTo(_listening);
|
||||
}
|
||||
|
||||
void OnDestroy() => _listening.Dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,19 +7,17 @@ using GameCult.Caching.MessagePack;
|
|||
// Aetheria's stores: one home store per type, composed here and nowhere else.
|
||||
public static class AetheriaStores
|
||||
{
|
||||
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(InputLayout), typeof(Loadout), typeof(FieldKindData), typeof(ProvinceData), typeof(VerseGrammar), typeof(VerseVerb), typeof(StationKindData), typeof(RecipeData), typeof(OutsideMarketData) };
|
||||
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(InputLayout), typeof(Loadout), typeof(FieldKindData), typeof(VerseGrammar), typeof(VerseVerb) };
|
||||
public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) };
|
||||
public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) };
|
||||
|
||||
// Attaches (hydrates) the catalog read-only unless catalogWritable, then the run and player stores when given.
|
||||
// Throws when a [CultGlobal] type routed to the catalog has no record. The one exemption is a writable catalog with
|
||||
// no records at all, a seed store being authored from nothing; a populated catalog is held to it however it opens.
|
||||
// A catalog's station hull with no Kind is refused, unless the caller gives kindlessStation: the tool that repairs a catalog
|
||||
// is told the hull's name and goes on.
|
||||
// The registry is the process-wide one unless the caller scopes it (the test host does, so its own document types never
|
||||
// become catalog globals); the global check above is judged against whichever registry opened the cache.
|
||||
public static CultCache Open(string catalogPath, string runPath = null, string playerPath = null, bool catalogWritable = false,
|
||||
CultDocumentRegistry registry = null, Action<string> kindlessStation = null)
|
||||
CultDocumentRegistry registry = null)
|
||||
{
|
||||
// A read-only open of an absent catalog would hydrate an empty game; a shipped player missing its catalog stops here.
|
||||
if (!catalogWritable && !File.Exists(catalogPath))
|
||||
|
|
@ -44,7 +42,6 @@ public static class AetheriaStores
|
|||
foreach (var data in cache.GetAll<ConsumableItemData>()) CultRecordRefs.Validate(data);
|
||||
VerseGrammarValidation.ValidateAll(cache.GetAll<VerseVerb>());
|
||||
ValidateBins(cache);
|
||||
StationKinds.Validate(cache, kindlessStation);
|
||||
return cache;
|
||||
}
|
||||
catch
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ public class CombatState : BaseState
|
|||
// only a contact a member tracks has a track to fly on, so an untracked target is no target here.
|
||||
var view = _agent.View;
|
||||
var target = view.TargetOf(_agent.Ship);
|
||||
if (target == null) { ReleaseUnwanted(); return; }
|
||||
if (target == null) return;
|
||||
var track = view.TrackOf(_agent.Ship, target.Value);
|
||||
if (track == null) { ReleaseUnwanted(); return; }
|
||||
if (track == null) return;
|
||||
|
||||
_availableGroups.Clear();
|
||||
_availableLockingWeapons.Clear();
|
||||
|
|
@ -88,9 +88,7 @@ public class CombatState : BaseState
|
|||
view.Designated(_agent.Ship, weapon, target.Value) &&
|
||||
!view.Refuses(_agent.Ship, weapon, target.Value) &&
|
||||
(weapon is ConstantWeapon ||
|
||||
// A held AutoWeapon stays a candidate between its rounds (it is cooling because it is firing), so the
|
||||
// group it belongs to is judged by the same Fires verdict below, not dropped for cooling down.
|
||||
weapon is InstantWeapon instantWeapon && (instantWeapon.CanFire || weapon is AutoWeapon && weapon.Firing)))
|
||||
weapon is InstantWeapon instantWeapon && instantWeapon.CanFire))
|
||||
{
|
||||
if(weapon is LockWeapon lockWeapon)
|
||||
_availableLockingWeapons.Add(lockWeapon);
|
||||
|
|
@ -153,7 +151,6 @@ public class CombatState : BaseState
|
|||
_agent.Ship.Command.Release(weapon);
|
||||
}
|
||||
}
|
||||
ReleaseUnwanted();
|
||||
_agent.Ship.Command.Aim = toTarget;
|
||||
_agent.Ship.Command.Turn = TurnCommand.Toward(toTarget.xz);
|
||||
_agent.Accelerate(movementDirection * _agent.TopSpeed, true);//selectedGroup >= 0);
|
||||
|
|
@ -161,19 +158,6 @@ public class CombatState : BaseState
|
|||
// Fire charged guns!
|
||||
}
|
||||
|
||||
public override void OnExitState() => ReleaseUnwanted(ending: true);
|
||||
|
||||
// The AI's trigger level is what it commands this step: a weapon holds a down trigger until it is released (a gun, a lock,
|
||||
// a beam), so every held one this step has no command for is let go, whether the target is gone, untracked, out of reach,
|
||||
// its group is not the pick, or Combat has ended. Ending lets go of every held one, a weapon pulled this very step
|
||||
// (held once the pull is applied) included, since nothing after Combat will command it.
|
||||
private void ReleaseUnwanted(bool ending = false)
|
||||
{
|
||||
var command = _agent.Ship.Command;
|
||||
foreach (var weapon in _agent.Ship.Weapons)
|
||||
if ((weapon.Firing || command.Pulls(weapon)) && (ending || !command.Commands(weapon))) command.Release(weapon);
|
||||
}
|
||||
|
||||
// Overdrive to close a distant target ahead, through the same consumable gate as the vent (Agent.VentOnTrack).
|
||||
private void UseConsumables(float3 toTarget, float targetDistance)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,17 +9,23 @@ using static CultMath.math;
|
|||
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
|
||||
public class AutoWeaponData : InstantWeaponData
|
||||
{
|
||||
[IgnoreMember]
|
||||
public override bool Automatic => true;
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
return new AutoWeapon(this, item);
|
||||
}
|
||||
}
|
||||
|
||||
// A fully automatic gun: it fires every round its cooldown allows while its trigger is held, from the press (Activate)
|
||||
// to the let-go (Deactivate). A plain InstantWeapon is semi-automatic.
|
||||
public class AutoWeapon : InstantWeapon
|
||||
{
|
||||
protected override bool Repeats => _firing;
|
||||
|
||||
|
||||
public AutoWeapon(InstantWeaponData data, EquippedItem item) : base(data, item) { }
|
||||
public AutoWeapon(InstantWeaponData data, ConsumableItemEffect item) : base(data, item) { }
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
if(_firing && _burstRemaining == 0 && _cooldown < 0)
|
||||
Trigger();
|
||||
return base.Execute(dt);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -157,7 +157,8 @@ public class ChargedWeapon : InstantWeapon
|
|||
if (_data.FailureCharge > 1 && _charge > _data.FailureCharge)
|
||||
{
|
||||
_charging = false;
|
||||
StartCountdown();
|
||||
_cooldown = 1;
|
||||
_coolingDown = true;
|
||||
_charge = 0;
|
||||
OnFailed?.Invoke();
|
||||
CauseDamage(_data.FailureDamage);
|
||||
|
|
|
|||
|
|
@ -146,10 +146,9 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
|
|||
}
|
||||
|
||||
_ammoInterval -= dt / _data.AmmoInterval;
|
||||
// An interval shorter than the step spends every round it spans.
|
||||
while (_ammoInterval < 0)
|
||||
if (_ammoInterval < 0)
|
||||
{
|
||||
_ammoInterval += 1;
|
||||
_ammoInterval = 1;
|
||||
if (_data.MagazineSize > 1 && _ammo > 0) _ammo--;
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class Cooldown : Behavior, IAlwaysUpdatedBehavior, IProgressBehavior
|
|||
{
|
||||
if (_cooldown < 0)
|
||||
{
|
||||
_cooldown += 1;
|
||||
_cooldown = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -57,8 +57,6 @@ public class Cooldown : Behavior, IAlwaysUpdatedBehavior, IProgressBehavior
|
|||
|
||||
public void Update(float delta)
|
||||
{
|
||||
// A finished timer keeps what the last step overshot, at most one step, so the restart in Execute is exact.
|
||||
var step = delta / Evaluate(_data.Cooldown);
|
||||
_cooldown = max(_cooldown - step, -step);
|
||||
_cooldown -= delta / Evaluate(_data.Cooldown);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,18 +42,6 @@ public class InputCapacitor
|
|||
|
||||
public void AddCharge(float amount) => Charge = clamp(Charge + amount, 0f, Capacity);
|
||||
|
||||
// A weapon held through a step may fire several rounds in it (ruling rounds-per-step): the charge it holds and what flows in
|
||||
// during the step pay for them, so for that step its buffer is a full activation plus the step's inflow; Settle trims what
|
||||
// the step did not spend back to Capacity. The fill rate is unchanged, so the energy a gun draws is still exactly its
|
||||
// rounds' cost. A sensor or a shield, and a weapon not held, keep the one-activation buffer above.
|
||||
private float StepReach(float dt) => Capacity + Rate * dt;
|
||||
|
||||
public float RequestedStepFill(float dt) => min(Rate * dt, max(0f, StepReach(dt) - Charge));
|
||||
|
||||
public void AddStepCharge(float amount, float dt) => Charge = clamp(Charge + amount, 0f, StepReach(dt));
|
||||
|
||||
public void Settle() => Charge = min(Charge, Capacity);
|
||||
|
||||
public bool CanSpend(float cost) => Charge >= cost;
|
||||
|
||||
// Atomic and whole-or-nothing, mirroring Entity.TrySpendCapacitorCharge's old contract exactly (down to the
|
||||
|
|
|
|||
|
|
@ -21,39 +21,14 @@ public class InstantWeaponData : WeaponData
|
|||
[Inspectable, JsonProperty("ammoInterval"), Key(20)]
|
||||
public bool SingleAmmoBurst;
|
||||
|
||||
// The fire mode, the one fact creation and the semi-auto catalog check both read: an automatic gun fires every round its
|
||||
// cooldown allows while its trigger is held, every other kind is semi-automatic (a press fires one burst).
|
||||
[IgnoreMember]
|
||||
public virtual bool Automatic => false;
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
return Automatic ? new AutoWeapon(this, item) : new InstantWeapon(this, item);
|
||||
return new InstantWeapon(this, item);
|
||||
}
|
||||
|
||||
public override Behavior CreateInstance(ConsumableItemEffect item)
|
||||
{
|
||||
return Automatic ? new AutoWeapon(this, item) : new InstantWeapon(this, item);
|
||||
}
|
||||
|
||||
// The shortest cooldown a plain (semi-automatic) gun may be authored with, 0.1 s: about ten presses a second. A faster one
|
||||
// is a fire-as-fast-as-you-can-press mechanic, unfriendly to accessibility and a special case for the AI (ruling
|
||||
// fire-mode-is-the-design); a gun that cycles faster is an AutoWeapon.
|
||||
public const float ShortestSemiAutoCooldown = .1f;
|
||||
|
||||
// The semi-automatic weapons of a catalog (gear and consumables, every kind that is not Automatic: plain, guided, mine,
|
||||
// launcher, charged) that cycle faster than a finger should have to: one fault line per weapon.
|
||||
public static IEnumerable<string> SemiAutoFaults(IEnumerable<ItemData> catalog)
|
||||
{
|
||||
foreach (var item in catalog)
|
||||
{
|
||||
var behaviors = (item as GearData)?.Behaviors ?? (item as ConsumableItemData)?.Behaviors;
|
||||
if (behaviors == null) continue;
|
||||
foreach (var behavior in behaviors)
|
||||
if (behavior is InstantWeaponData weapon && !weapon.Automatic &&
|
||||
Math.Min(weapon.Cooldown.Min, weapon.Cooldown.Max) < ShortestSemiAutoCooldown)
|
||||
yield return $"{item.Name}: a {weapon.GetType().Name} with a {Math.Min(weapon.Cooldown.Min, weapon.Cooldown.Max):0.###} s cooldown, below {ShortestSemiAutoCooldown} s; make it an AutoWeapon or slow it";
|
||||
}
|
||||
return new InstantWeapon(this, item);
|
||||
}
|
||||
|
||||
// The one formula of an instant weapon's damage per second at a range; the live weapon calls it with its evaluated
|
||||
|
|
@ -83,11 +58,6 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
protected float _cooldown; // Normalized
|
||||
private int _ammo = 0;
|
||||
protected bool _coolingDown;
|
||||
// Seconds the last countdown ran past zero (less than one step). The next countdown starts that far along, so a held
|
||||
// trigger fires Cooldown apart on average at any step, never floor(Cooldown/dt)+1 steps apart.
|
||||
private float _overshoot;
|
||||
// The shortest cycle a countdown divides by: a cooldown of zero would otherwise fire without end in one step.
|
||||
private const float ShortestCycle = .0005f;
|
||||
|
||||
public float BurstCount { get; protected set; }
|
||||
public float BurstTime { get; protected set; }
|
||||
|
|
@ -117,11 +87,6 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
// manager can bind its presentation to the one shot it belongs to instead of applying damage itself.
|
||||
public event Action<int> OnFire;
|
||||
|
||||
// Whether a down trigger keeps firing: a plain gun is semi-automatic, one burst per press (a held key does not repeat, and
|
||||
// an agent presses it again each time it is ready); an AutoWeapon repeats while its trigger is held, every round its
|
||||
// cooldown allows at any step length. Fire mode is authored by the kind of the weapon (ruling fire-mode-is-the-design).
|
||||
protected virtual bool Repeats => false;
|
||||
|
||||
public virtual void ResetEvents()
|
||||
{
|
||||
OnReloadBegin = null;
|
||||
|
|
@ -200,15 +165,11 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
_capacitor.UpdateStats(perShotEnergy, cooldown, rateOverride: RateOverride);
|
||||
}
|
||||
|
||||
// What the capacitor asks the bus for this step: a weapon held through it may fire several rounds in it, so it asks for
|
||||
// the step's whole inflow; any other tops up to one activation.
|
||||
private float Fill(float dt) => Repeats ? _capacitor.RequestedStepFill(dt) : _capacitor.RequestedFill(dt);
|
||||
|
||||
// Cut 4: the request PowerBus needs before Execute runs, mirroring EnergyDraw's IPowerConsumer pattern.
|
||||
public float PowerRequest(float dt)
|
||||
{
|
||||
RefreshInputCapacitor();
|
||||
return Fill(dt);
|
||||
return _capacitor.RequestedFill(dt);
|
||||
}
|
||||
|
||||
// Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): Low -- offense. The ruling's own example of what
|
||||
|
|
@ -222,40 +183,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
// would ever fill this capacitor -- bypass it entirely rather than starving such an instance forever.
|
||||
protected bool TrySpendActivationEnergy() => Item == null || _capacitor.TrySpend(_capacitor.Capacity);
|
||||
|
||||
// Ends the countdown once it is below zero, keeping what it ran past zero (at most one step) for the next one.
|
||||
// True when it ended the countdown now.
|
||||
private bool FinishCountdown(float dt)
|
||||
{
|
||||
if (!_coolingDown || !(_cooldown < 0)) return false;
|
||||
_overshoot = min(-_cooldown * CountdownSpan, dt);
|
||||
_coolingDown = false;
|
||||
if (_data.MagazineSize > 0 && _ammo == 0)
|
||||
{
|
||||
_ammo = _data.MagazineSize;
|
||||
OnReloadComplete?.Invoke();
|
||||
}
|
||||
else
|
||||
OnCooldownComplete?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
private float CountdownSpan => max(ShortestCycle, _data.MagazineSize > 0 && _ammo == 0 ? _data.ReloadTime : Cooldown);
|
||||
|
||||
// A countdown that starts outside a round (a charge failing) carries the last one's overshoot like every restart.
|
||||
protected void StartCountdown()
|
||||
{
|
||||
_cooldown = 1 - TakeOvershoot() / CountdownSpan;
|
||||
_coolingDown = true;
|
||||
}
|
||||
|
||||
private float TakeOvershoot()
|
||||
{
|
||||
var overshoot = _overshoot;
|
||||
_overshoot = 0;
|
||||
return overshoot;
|
||||
}
|
||||
|
||||
private bool UseAmmo(float dt)
|
||||
private bool UseAmmo()
|
||||
{
|
||||
if (_data.MagazineSize <= 1) return true;
|
||||
|
||||
|
|
@ -280,7 +208,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
if(hasAmmo)
|
||||
{
|
||||
OnReloadBegin?.Invoke();
|
||||
_cooldown = 1 - (_burstTimer + TakeOvershoot()) / _data.ReloadTime;
|
||||
_cooldown = 1;
|
||||
_coolingDown = true;
|
||||
_firing = false;
|
||||
}
|
||||
|
|
@ -290,32 +218,29 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
}
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
try { return Step(dt); }
|
||||
finally { _capacitor.Settle(); }
|
||||
}
|
||||
|
||||
// One step of rounds. A round is due at a moment inside the step (_burstTimer is how long ago, at the step's end), and a
|
||||
// cooldown that ends inside the step while the trigger is held starts the next burst at that moment, so a gun fires
|
||||
// every round its cooldown allows in a step, each with its own roll, however short the cooldown or long the step.
|
||||
private bool Step(float dt)
|
||||
{
|
||||
base.Execute(dt);
|
||||
// Cut 4: this tick's grant, read back via Item.PowerSupply -- Item is non-null here because a null-Item
|
||||
// instance never reaches PowerBus.Step (see TrySpendActivationEnergy) and so never accrues charge.
|
||||
if (Item != null)
|
||||
_capacitor.AddStepCharge(Fill(dt) * Item.PowerSupply, dt);
|
||||
var ended = false;
|
||||
var retriggered = false;
|
||||
_capacitor.AddCharge(_capacitor.RequestedFill(dt) * Item.PowerSupply);
|
||||
if (_coolingDown)
|
||||
{
|
||||
_cooldown -= dt / CountdownSpan;
|
||||
ended = FinishCountdown(dt);
|
||||
_cooldown -= dt / (_data.MagazineSize > 0 && _ammo == 0 ? _data.ReloadTime : Cooldown);
|
||||
if (_cooldown < 0)
|
||||
{
|
||||
_coolingDown = false;
|
||||
if (_data.MagazineSize > 0 && _ammo == 0)
|
||||
{
|
||||
_ammo = _data.MagazineSize;
|
||||
OnReloadComplete?.Invoke();
|
||||
}
|
||||
else
|
||||
OnCooldownComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
_burstTimer += dt;
|
||||
while (true)
|
||||
{
|
||||
while (_burstRemaining > 0 && _burstTimer > 0)
|
||||
{
|
||||
// A fused round whose arming distance Range cannot reach is refused, per round, before anything is
|
||||
|
|
@ -324,7 +249,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
var refused = FireControl.Refuses(this, Entity);
|
||||
// Ammo and energy are consumed here, per round, or once per burst at its first unrefused round
|
||||
// (SingleAmmoBurst). UseAmmo returns false when triggering reload; cancel firing if that is the case
|
||||
if (!refused && !_burstPaid && (!TrySpendActivationEnergy() || !UseAmmo(dt)))
|
||||
if (!refused && !_burstPaid && (!TrySpendActivationEnergy() || !UseAmmo()))
|
||||
{
|
||||
_burstRemaining = 0;
|
||||
return false;
|
||||
|
|
@ -335,9 +260,8 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
if (!_burstStarted)
|
||||
{
|
||||
_burstStarted = true;
|
||||
_cooldown = 1 - (_burstTimer + TakeOvershoot()) / CountdownSpan;
|
||||
// The round starts the cooldown after this tick's decrement above: charge the tick and the last
|
||||
// countdown's overshoot to it, so the cadence stays Cooldown
|
||||
_cooldown = 1 - dt / Cooldown;
|
||||
// The round starts the cooldown after this tick's decrement above: charge the tick to it, so the cadence stays Cooldown
|
||||
_coolingDown = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -353,27 +277,12 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
// whole expression, argument included, whenever OnFire has no subscriber (a headless run with no
|
||||
// Unity EntityInstance wiring, docs/headless-playground-cut.md), so a shot silently never fired.
|
||||
// Evaluate it into a local first.
|
||||
var shotId = FireControl.Fire(this, Item, Entity, age: _burstTimer + _burstInterval);
|
||||
var shotId = FireControl.Fire(this, Item, Entity);
|
||||
OnFire?.Invoke(shotId);
|
||||
CauseWearDamage(1);
|
||||
AddHeat(Heat);
|
||||
Entity.SetSignature(this, SignatureChannel.LaunchFlash, Visibility, Item);
|
||||
}
|
||||
// A cooldown shorter than the step it started in is over in that step.
|
||||
ended |= FinishCountdown(dt);
|
||||
// The trigger is held and the weapon is ready: the next burst starts now, at the moment the cooldown ended (what it
|
||||
// ran past zero is how long ago, at the step's end), or at the step's start when the weapon was already ready.
|
||||
if (!Repeats || _burstRemaining > 0 || !CanFire || (retriggered && !_burstStarted)) break;
|
||||
retriggered = true;
|
||||
Trigger();
|
||||
if (_burstRemaining == 0) break;
|
||||
if (ended)
|
||||
{
|
||||
_burstTimer = TakeOvershoot();
|
||||
ended = false;
|
||||
}
|
||||
else _burstTimer = dt;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
{
|
||||
_pinging = true;
|
||||
_pingZone = Entity.Zone;
|
||||
_pingCooldown += 1;
|
||||
_pingCooldown = 1;
|
||||
_pingLerp = 0;
|
||||
_pingRadius = 0;
|
||||
_reached.Clear();
|
||||
|
|
@ -180,14 +180,8 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
if (_pingLerp > 1) EndPing();
|
||||
}
|
||||
|
||||
// A finished timer keeps what the last step overshot, at most one step, so the restart in Ping is exact.
|
||||
var pingStep = dt / Evaluate(_data.PingCooldown);
|
||||
_pingCooldown = max(_pingCooldown - pingStep, -pingStep);
|
||||
_pingCooldown -= dt / Evaluate(_data.PingCooldown);
|
||||
|
||||
var infoDecay = ItemManager.GameplaySettings.TargetInfoDecay;
|
||||
var decay = exp(-infoDecay * dt);
|
||||
var settle = infoDecay > 0 ? (1 - decay) / infoDecay : dt;
|
||||
|
||||
var forward = Direction.xz;
|
||||
foreach (var entity in Entity.Zone.Entities)
|
||||
{
|
||||
|
|
@ -227,16 +221,14 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
credit += Gain(entity.Visibility, sensitivity, response, sqrt(r.IlluminatedRange * dist), dt, true, sqrt(r.IlluminatedObscuration * obscuration));
|
||||
}
|
||||
|
||||
// The exact decay at this step: x <- x e^(-k dt) + (G / k)(1 - e^(-k dt)) for the passive gain G per second,
|
||||
// which settles at G / k (Entity.PassiveInfo) at every dt. A ping's credit is an impulse added first.
|
||||
if (heard)
|
||||
next = saturate(previous + credit) * decay;
|
||||
next = saturate(previous + credit);
|
||||
else
|
||||
{
|
||||
var angle = acos(dot(forward, normalize(diff)));
|
||||
var passive = Gain(entity.Visibility, sensitivity, _data.SensitivityCurve.Evaluate(angle / PI), dist, 1f, false, obscuration);
|
||||
next = saturate(previous * decay + passive * settle);
|
||||
next = saturate(previous + Gain(entity.Visibility, sensitivity, _data.SensitivityCurve.Evaluate(angle / PI), dist, dt, false, obscuration));
|
||||
}
|
||||
next *= 1 - ItemManager.GameplaySettings.TargetInfoDecay * dt;
|
||||
//Context.Log($"{entity.Name} visibility {(int)(previous * 100)}% -> {(int)(next * 100)}%");
|
||||
Entity.EntityInfoGathered[entity] = next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ public class Thruster : Behavior, IPowerConsumer
|
|||
public float Thrust => Evaluate(_data.Thrust);
|
||||
// Thrust at a full grant, the same stat read without the power-supply term: what a column promises. Live, like Thrust.
|
||||
public float NominalThrust => EvaluateNominalPower(_data.Thrust);
|
||||
// GameplaySettings.ThrustScale scales the push alone: it tunes how hard ships fight gravity, and turning does not.
|
||||
public float Torque { get; }
|
||||
|
||||
// This thruster's effect per unit throttle at the given thrust, in the body frame: xy is the push (x starboard, y
|
||||
|
|
@ -57,7 +56,7 @@ public class Thruster : Behavior, IPowerConsumer
|
|||
public float3 Column(float thrust)
|
||||
{
|
||||
if (Item == null || !Item.Active.Value) return default;
|
||||
var push = -float2(0, 1).Rotate(Item.EquippableItem.Rotation) * thrust * ItemManager.GameplaySettings.ThrustScale / Entity.Mass;
|
||||
var push = -float2(0, 1).Rotate(Item.EquippableItem.Rotation) * thrust / Entity.Mass;
|
||||
return float3(push.x, push.y, Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass);
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +117,7 @@ public class Thruster : Behavior, IPowerConsumer
|
|||
if(_input > 0f && Item.PowerSupply > 1e-4f)
|
||||
{
|
||||
var thrust = Thrust;
|
||||
Entity.Velocity -= Direction.xz * _input * thrust * ItemManager.GameplaySettings.ThrustScale / Entity.Mass * dt;
|
||||
Entity.Velocity -= Direction.xz * _input * thrust / Entity.Mass * dt;
|
||||
var turnRate = _input * Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass;
|
||||
Entity.Direction = mul(Entity.Direction, CultMath.float2x2.Rotate(turnRate * dt));
|
||||
Entity.TurnRate += turnRate;
|
||||
|
|
|
|||
|
|
@ -1,469 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
|
||||
// What the economy checks read beyond the catalog: the setting's factors on recipe terms (each at least 1) and the units of a
|
||||
// commodity one worker consumes per second of work. Neutral by default; later cuts pass real values.
|
||||
public sealed class EconomyLoad
|
||||
{
|
||||
public Dictionary<string, double> LabourNeeds = new Dictionary<string, double>();
|
||||
public double InputFactor = 1;
|
||||
public double WorkforceFactor = 1;
|
||||
public double UpkeepFactor = 1;
|
||||
}
|
||||
|
||||
public sealed class EconomyFault
|
||||
{
|
||||
public readonly string Invariant;
|
||||
public readonly string Record;
|
||||
public readonly string Message;
|
||||
|
||||
// A fault only because the chain has not written the record yet (a commodity with no recipe). Reported, never a refusal.
|
||||
public readonly bool Pending;
|
||||
|
||||
// L-productive only: every good the carrying cycle holds (sorted) and the spectral radius. The fault's identity is the member
|
||||
// set; a radius that rises is a worse fault of the same identity.
|
||||
public readonly IReadOnlyList<string> Members;
|
||||
public readonly double Radius;
|
||||
|
||||
public EconomyFault(string invariant, string record, string message, bool pending = false, IReadOnlyList<string> members = null, double radius = 0)
|
||||
{
|
||||
Members = members;
|
||||
Radius = radius;
|
||||
Pending = pending;
|
||||
Invariant = invariant;
|
||||
Record = record;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{Invariant} {Record}: {Message}";
|
||||
}
|
||||
|
||||
public sealed class EconomyReport
|
||||
{
|
||||
public readonly IReadOnlyList<EconomyFault> Faults;
|
||||
// Production steps to each good, by the good's record key; null when no finite chain reaches it. Read by reviewers, never a refusal.
|
||||
public readonly IReadOnlyDictionary<string, int?> Depth;
|
||||
|
||||
public EconomyReport(IReadOnlyList<EconomyFault> faults, IReadOnlyDictionary<string, int?> depth)
|
||||
{
|
||||
Faults = faults;
|
||||
Depth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
// The one owner of the catalog's economy invariants. Pure: draws nothing, reads no clock, writes nothing. One method per rule.
|
||||
public static class EconomyChecks
|
||||
{
|
||||
public const string LRecipe = "L-recipe", LOneKindList = "L-one-kind-list", LLore = "L-lore", LSource = "L-source-sink", LProductive = "L-productive", LDemand = "L-consumer-demand";
|
||||
private const double Margin = 1e-9;
|
||||
|
||||
public static EconomyReport Check(CultCache catalog, EconomyLoad load = null)
|
||||
{
|
||||
var faults = new List<EconomyFault>();
|
||||
faults.AddRange(Recipe(catalog));
|
||||
faults.AddRange(Lore(catalog));
|
||||
faults.AddRange(SourceSink(catalog));
|
||||
faults.AddRange(Productive(catalog, load));
|
||||
faults.AddRange(ConsumerDemand(catalog));
|
||||
return new EconomyReport(faults, Deep(catalog));
|
||||
}
|
||||
|
||||
private static IEnumerable<(string Key, RecipeData Recipe)> StoredRecipes(CultCache catalog) =>
|
||||
catalog.GetAll<RecipeData>().Select(recipe => (catalog.RefOf(recipe).Key.Value, recipe)).OrderBy(pair => pair.Item1, StringComparer.Ordinal);
|
||||
|
||||
// Every rule but Recipe reads recipes through here, so a list a record holds as null (or with a null entry) reads as the list
|
||||
// without it; Recipe alone names the malformation. The stored document is never changed.
|
||||
private static IEnumerable<(string Key, RecipeData Recipe)> AllRecipes(CultCache catalog) =>
|
||||
StoredRecipes(catalog).Select(pair => (pair.Key, Sound(pair.Recipe)));
|
||||
|
||||
private static RecipeData Sound(RecipeData recipe) =>
|
||||
recipe.Inputs != null && recipe.Upkeep != null && recipe.Fills != null && recipe.RoleInputs != null
|
||||
&& !recipe.Inputs.Contains(null) && !recipe.Upkeep.Contains(null) && !recipe.Fills.Contains(null) && !recipe.RoleInputs.Contains(null)
|
||||
? recipe
|
||||
: new RecipeData
|
||||
{
|
||||
Output = recipe.Output, OutputCount = recipe.OutputCount, Cycle = recipe.Cycle, Workforce = recipe.Workforce,
|
||||
StationKind = recipe.StationKind, LoreHook = recipe.LoreHook,
|
||||
Inputs = (recipe.Inputs ?? new List<CommodityCount>()).Where(c => c != null).ToList(),
|
||||
Upkeep = (recipe.Upkeep ?? new List<CommodityCount>()).Where(c => c != null).ToList(),
|
||||
Fills = (recipe.Fills ?? new List<string>()).Where(f => f != null).ToList(),
|
||||
RoleInputs = (recipe.RoleInputs ?? new List<ItemRole>()).Where(r => r != null).ToList()
|
||||
};
|
||||
|
||||
// The designs some faction offers, but a Station or Turret hull, distinct and ordered by key: the designs that need a recipe.
|
||||
private static List<ItemData> OfferedDesigns(CultCache catalog)
|
||||
{
|
||||
var designs = new Dictionary<string, ItemData>(StringComparer.Ordinal);
|
||||
foreach (var product in catalog.GetAll<FactionProductData>().Where(p => LoadoutGenerator.IsOffered(catalog, p)))
|
||||
{
|
||||
var design = catalog.Get(product.Design);
|
||||
if (design == null || (design is HullData hull && (hull.HullType == HullType.Station || hull.HullType == HullType.Turret))) continue;
|
||||
designs.TryAdd(catalog.RefOf(design).Key.Value, design);
|
||||
}
|
||||
return designs.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => pair.Value).ToList();
|
||||
}
|
||||
|
||||
// The roles some design demands: those of every offered design and of every design a recipe makes, recipe or no.
|
||||
private static HashSet<string> DesignRoles(CultCache catalog) =>
|
||||
new HashSet<string>(OfferedDesigns(catalog).Concat(AllRecipes(catalog).Select(p => catalog.Get(p.Recipe.Output)).Where(o => o != null && !Recipes.IsCommodity(o)))
|
||||
.SelectMany(design => Recipes.RolesOf(design)).Select(role => role.Name), StringComparer.Ordinal);
|
||||
|
||||
private static IEnumerable<ItemData> Commodities(CultCache catalog) =>
|
||||
catalog.GetAll<ItemData>().Where(Recipes.IsCommodity).OrderBy(item => catalog.RefOf(item).Key.Value, StringComparer.Ordinal);
|
||||
|
||||
// Every commodity and every offered design (but a Station or Turret hull) has a recipe, and each recipe is well formed.
|
||||
public static List<EconomyFault> Recipe(CultCache catalog)
|
||||
{
|
||||
var faults = new List<EconomyFault>();
|
||||
void Add(string record, string message) => faults.Add(new EconomyFault(LRecipe, record, message));
|
||||
foreach (var item in Commodities(catalog))
|
||||
if (Recipes.RecipeOf(catalog, item) == null)
|
||||
faults.Add(new EconomyFault(LRecipe, catalog.RefOf(item).Key.Value, $"Commodity '{item.Name}' has no recipe.", pending: true));
|
||||
foreach (var design in OfferedDesigns(catalog))
|
||||
if (Recipes.RecipeOf(catalog, design) == null)
|
||||
faults.Add(new EconomyFault(LRecipe, catalog.RefOf(design).Key.Value, $"Offered design '{design.Name}' has no recipe.", pending: true));
|
||||
foreach (var market in catalog.GetAll<OutsideMarketData>())
|
||||
{
|
||||
var marketKey = catalog.RefOf(market).Key.Value;
|
||||
if (market.Rows == null) Add(marketKey, "Rows is null; an outside market holds a list, empty when it trades nothing.");
|
||||
else if (market.Rows.Contains(null)) Add(marketKey, "Rows holds a null entry.");
|
||||
}
|
||||
var kindsPlaced = new HashSet<string>(catalog.GetAll<Faction>().SelectMany(f => StationKinds.KindsOf(catalog, f)).Select(k => k.Key.Value), StringComparer.Ordinal);
|
||||
foreach (var (key, stored) in StoredRecipes(catalog))
|
||||
{
|
||||
var recipe = Sound(stored);
|
||||
foreach (var (list, name) in new[] { (stored.Inputs, "Inputs"), (stored.Upkeep, "Upkeep") })
|
||||
if (list == null) Add(key, $"{name} is null; a recipe holds a list, empty when it takes nothing.");
|
||||
else if (list.Contains(null)) Add(key, $"{name} holds a null entry.");
|
||||
if (stored.Fills == null) Add(key, "Fills is null; a recipe holds a list, empty when it fills no role.");
|
||||
else if (stored.Fills.Contains(null)) Add(key, "Fills holds a null entry.");
|
||||
if (stored.RoleInputs == null) Add(key, "RoleInputs is null; a recipe holds a list, empty when it takes no role.");
|
||||
else if (stored.RoleInputs.Contains(null)) Add(key, "RoleInputs holds a null entry.");
|
||||
var output = catalog.Get(recipe.Output);
|
||||
if (output == null) { Add(key, "Output names no item this catalog holds."); continue; }
|
||||
if (key != RecipeData.KeyOf(recipe.Output.Key).Value)
|
||||
Add(key, "The recipe's key is not recipe:<output key>; one recipe per output is its key.");
|
||||
if (recipe.OutputCount <= 0) Add(key, "OutputCount is not above zero.");
|
||||
if (!float.IsFinite(recipe.Cycle) || recipe.Cycle <= 0) Add(key, "Cycle is not a finite number above zero.");
|
||||
if (recipe.Workforce < 0) Add(key, "Workforce is below zero.");
|
||||
foreach (var (list, name) in new[] { (recipe.Inputs, "Inputs"), (recipe.Upkeep, "Upkeep") })
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i].Count <= 0) Add(key, $"{name}[{i}] count is not above zero.");
|
||||
if (!Recipes.IsCommodity(catalog.Get(list[i].Commodity))) Add(key, $"{name}[{i}] names no commodity; a recipe takes only commodities.");
|
||||
}
|
||||
var kind = catalog.Get(recipe.StationKind);
|
||||
if (kind == null) Add(key, "StationKind names no station kind this catalog holds.");
|
||||
else if (!kindsPlaced.Contains(recipe.StationKind.Key.Value))
|
||||
faults.Add(new EconomyFault(LOneKindList, key, $"No faction places station kind '{kind.Name}', so nothing can run this recipe."));
|
||||
var roles = Recipes.RolesOf(output);
|
||||
if (!Recipes.IsCommodity(output))
|
||||
{
|
||||
if (recipe.Fills.Count > 0) Add(key, "Fills is set on a design; only a commodity fills a role.");
|
||||
if (roles.Count == 0) Add(key, "A design's recipe needs a design with at least one role.");
|
||||
}
|
||||
if (roles.Count > 0 && recipe.Inputs.Count > 0) Add(key, "A role recipe has no Inputs; its inputs are its design's roles.");
|
||||
if (roles.Count > 0 && recipe.RoleInputs.Count > 0) Add(key, "A role recipe has no RoleInputs; its role inputs are its design's roles.");
|
||||
foreach (var role in Recipes.RoleInputsOf(catalog, recipe))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(role.Name)) { Add(key, "A role input has an empty name."); continue; }
|
||||
if (role.Count < 1) Add(key, $"Role '{role.Name}' count is below one.");
|
||||
if (Recipes.Fillers(catalog, role.Name).Length == 0) Add(key, $"Role '{role.Name}' has no commodity whose recipe fills it.");
|
||||
}
|
||||
}
|
||||
return faults;
|
||||
}
|
||||
|
||||
// A commodity's lore hook is present. What it says is the operator's review.
|
||||
public static List<EconomyFault> Lore(CultCache catalog) =>
|
||||
AllRecipes(catalog).Where(pair => Recipes.IsCommodity(catalog.Get(pair.Recipe.Output)) && string.IsNullOrWhiteSpace(pair.Recipe.LoreHook))
|
||||
.Select(pair => new EconomyFault(LLore, pair.Key, "A commodity's recipe needs a lore hook.")).ToList();
|
||||
|
||||
private static (HashSet<string> Sells, HashSet<string> Buys) Gate(CultCache catalog)
|
||||
{
|
||||
var rows = catalog.GetAll<OutsideMarketData>().SelectMany(market => market.Rows ?? new List<OutsideMarketRow>()).Where(r => r != null).ToList();
|
||||
return (new HashSet<string>(rows.Where(r => r.Sells).Select(r => r.Commodity.Key.Value), StringComparer.Ordinal),
|
||||
new HashSet<string>(rows.Where(r => r.Buys).Select(r => r.Commodity.Key.Value), StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
// The commodities an offered design's weapon draws as ammunition. One reader, shared by the sink and the consumer-demand rules.
|
||||
private static HashSet<string> Drawn(CultCache catalog) =>
|
||||
new HashSet<string>(catalog.GetAll<FactionProductData>().Where(p => LoadoutGenerator.IsOffered(catalog, p))
|
||||
.Select(p => catalog.Get(p.Design) as EquippableItemData).Where(design => design?.Behaviors != null)
|
||||
.SelectMany(design => design.Behaviors.OfType<WeaponData>()).Where(weapon => weapon.AmmoType.IsSet())
|
||||
.Select(weapon => weapon.AmmoType.Key.Value), StringComparer.Ordinal);
|
||||
|
||||
// Every commodity has a source (a grounded chain, or a gate that sells it) and a sink (a recipe takes it or a role it fills, a role
|
||||
// some design (offered, or made by a recipe) names, a weapon some offered design carries draws it, or a gate that buys it). A consumer good's sink is judged once needs
|
||||
// tables exist.
|
||||
public static List<EconomyFault> SourceSink(CultCache catalog)
|
||||
{
|
||||
var faults = new List<EconomyFault>();
|
||||
var (sells, buys) = Gate(catalog);
|
||||
var drawn = Drawn(catalog);
|
||||
var grounded = Depths(catalog, sells);
|
||||
var recipes = AllRecipes(catalog).ToList();
|
||||
var used = new HashSet<string>(recipes.SelectMany(p => p.Recipe.Inputs.Concat(p.Recipe.Upkeep)).Select(c => c.Commodity.Key.Value), StringComparer.Ordinal);
|
||||
var rolesNamed = DesignRoles(catalog);
|
||||
rolesNamed.UnionWith(recipes.SelectMany(p => Recipes.RoleInputsOf(catalog, p.Recipe)).Select(r => r.Name));
|
||||
foreach (var (key, recipe) in recipes)
|
||||
{
|
||||
var output = catalog.Get(recipe.Output);
|
||||
if (!Recipes.IsCommodity(output)) continue;
|
||||
var outputKey = recipe.Output.Key.Value;
|
||||
if (!grounded.TryGetValue(outputKey, out var depth) || depth == null)
|
||||
faults.Add(new EconomyFault(LSource, key, $"'{output.Name}' has no source: no chain of recipes reaches raw goods and no gate sells it."));
|
||||
if (Recipes.IsConsumerGood(catalog, recipe)) continue;
|
||||
if (!used.Contains(outputKey) && !recipe.Fills.Any(rolesNamed.Contains) && !drawn.Contains(outputKey) && !buys.Contains(outputKey))
|
||||
faults.Add(new EconomyFault(LSource, key, $"'{output.Name}' has no sink: no recipe takes it or a role it fills, no design names a role it fills, no weapon draws it, no gate buys it."));
|
||||
}
|
||||
return faults;
|
||||
}
|
||||
|
||||
// Production steps to each good: raw goods 0, a recipe's output one more than its deepest input or role, a role as deep as
|
||||
// its shallowest filler (Knuth's least fixpoint over AND and OR nodes). Null for a good no finite chain reaches. Reported for
|
||||
// review; it refuses nothing.
|
||||
public static IReadOnlyDictionary<string, int?> Deep(CultCache catalog) => Depths(catalog, new HashSet<string>());
|
||||
|
||||
private static Dictionary<string, int?> Depths(CultCache catalog, ISet<string> seeds)
|
||||
{
|
||||
var depth = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var seed in seeds) depth[seed] = 0;
|
||||
var recipes = AllRecipes(catalog).Where(p => catalog.Get(p.Recipe.Output) != null).ToList();
|
||||
bool changed;
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
foreach (var (_, recipe) in recipes)
|
||||
{
|
||||
var candidate = Candidate(catalog, recipe, depth);
|
||||
var key = recipe.Output.Key.Value;
|
||||
if (candidate == null || (depth.TryGetValue(key, out var known) && known <= candidate)) continue;
|
||||
depth[key] = candidate.Value;
|
||||
changed = true;
|
||||
}
|
||||
} while (changed);
|
||||
var result = new Dictionary<string, int?>(StringComparer.Ordinal);
|
||||
foreach (var item in Commodities(catalog)) result[catalog.RefOf(item).Key.Value] = null;
|
||||
foreach (var (_, recipe) in recipes) result[recipe.Output.Key.Value] = null;
|
||||
foreach (var pair in depth) result[pair.Key] = pair.Value;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int? Candidate(CultCache catalog, RecipeData recipe, Dictionary<string, int> depth)
|
||||
{
|
||||
if (Recipes.IsRaw(catalog, recipe)) return 0;
|
||||
var deepest = 0;
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
if (!depth.TryGetValue(input.Commodity.Key.Value, out var d)) return null;
|
||||
deepest = Math.Max(deepest, d);
|
||||
}
|
||||
foreach (var role in Recipes.RoleInputsOf(catalog, recipe))
|
||||
{
|
||||
int? shallowest = null;
|
||||
foreach (var filler in Recipes.Fillers(catalog, role.Name))
|
||||
if (depth.TryGetValue(filler.Value, out var d) && (shallowest == null || d < shallowest)) shallowest = d;
|
||||
if (shallowest == null) return null;
|
||||
deepest = Math.Max(deepest, shallowest.Value);
|
||||
}
|
||||
return deepest + 1;
|
||||
}
|
||||
|
||||
// Hawkins-Simon over commodities plus one labour good, at the worst single filler of every role slot (a recipe's role inputs): the spectral radius
|
||||
// of the requirements matrix stays below one however the roles are filled. The worst fill is the spectral simplex
|
||||
// (Nesterov and Protasov 2013) on the matrix nudged strictly positive, then the radius of the chosen matrix is taken exactly.
|
||||
public static List<EconomyFault> Productive(CultCache catalog, EconomyLoad load = null)
|
||||
{
|
||||
load = load ?? new EconomyLoad();
|
||||
if (!(load.InputFactor >= 1) || !(load.WorkforceFactor >= 1) || !(load.UpkeepFactor >= 1))
|
||||
throw new ArgumentException("Setting factors are at least 1.", nameof(load));
|
||||
var columns = AllRecipes(catalog).Where(p => Recipes.IsCommodity(catalog.Get(p.Recipe.Output)) && p.Recipe.OutputCount > 0 && float.IsFinite(p.Recipe.Cycle)).ToList();
|
||||
var names = columns.Select(p => p.Recipe.Output.Key.Value).ToList();
|
||||
var index = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < names.Count; i++) index[names[i]] = i;
|
||||
var n = names.Count + 1;
|
||||
var labour = names.Count;
|
||||
var fixedTerms = new double[n, n];
|
||||
var slots = new List<(int Column, double Amount, int[] Fillers)>();
|
||||
for (var j = 0; j < columns.Count; j++)
|
||||
{
|
||||
var recipe = columns[j].Recipe;
|
||||
foreach (var input in recipe.Inputs)
|
||||
if (index.TryGetValue(input.Commodity.Key.Value, out var row) && input.Count > 0) fixedTerms[row, j] += input.Count * load.InputFactor / recipe.OutputCount;
|
||||
foreach (var upkeep in recipe.Upkeep)
|
||||
if (index.TryGetValue(upkeep.Commodity.Key.Value, out var row) && upkeep.Count > 0) fixedTerms[row, j] += upkeep.Count * load.UpkeepFactor / recipe.OutputCount;
|
||||
fixedTerms[labour, j] = recipe.Workforce * (double) recipe.Cycle * load.WorkforceFactor / recipe.OutputCount;
|
||||
foreach (var role in Recipes.RoleInputsOf(catalog, recipe))
|
||||
{
|
||||
var fillers = Recipes.Fillers(catalog, role.Name).Where(k => index.ContainsKey(k.Value)).Select(k => index[k.Value]).ToArray();
|
||||
if (fillers.Length > 0 && role.Count > 0) slots.Add((j, role.Count * load.InputFactor / recipe.OutputCount, fillers));
|
||||
}
|
||||
}
|
||||
foreach (var need in load.LabourNeeds)
|
||||
if (index.TryGetValue(need.Key, out var row)) fixedTerms[row, labour] = need.Value;
|
||||
|
||||
double[,] Build(int[] choice, double nudge)
|
||||
{
|
||||
var a = new double[n, n];
|
||||
for (var r = 0; r < n; r++)
|
||||
for (var c = 0; c < n; c++)
|
||||
a[r, c] = fixedTerms[r, c] + nudge;
|
||||
for (var s = 0; s < slots.Count; s++) a[slots[s].Fillers[choice[s]], slots[s].Column] += slots[s].Amount;
|
||||
return a;
|
||||
}
|
||||
|
||||
var pick = new int[slots.Count];
|
||||
for (var round = 0; round < 500; round++)
|
||||
{
|
||||
var left = LeftPerron(Build(pick, 1e-12), n);
|
||||
var moved = false;
|
||||
for (var s = 0; s < slots.Count; s++)
|
||||
{
|
||||
var best = pick[s];
|
||||
for (var f = 0; f < slots[s].Fillers.Length; f++)
|
||||
if (left[slots[s].Fillers[f]] > left[slots[s].Fillers[best]] * (1 + 1e-9)) best = f;
|
||||
if (best != pick[s]) { pick[s] = best; moved = true; }
|
||||
}
|
||||
if (!moved) break;
|
||||
}
|
||||
|
||||
var final = Build(pick, 0);
|
||||
var faults = new List<EconomyFault>();
|
||||
string Name(int i) => i == labour ? "labour" : names[i];
|
||||
foreach (var (rho, component) in Radii(final, n))
|
||||
{
|
||||
if (rho < 1 - Margin) continue;
|
||||
var order = CycleOrder(final, n, component);
|
||||
faults.Add(new EconomyFault(LProductive, Name(order[0]),
|
||||
$"The goods cannot sustain themselves: spectral radius {rho:0.######} is not below 1, carried by {string.Join(" -> ", order.Select(Name))}.",
|
||||
members: order.Select(Name).OrderBy(name => name, StringComparer.Ordinal).ToArray(), radius: rho));
|
||||
}
|
||||
return faults;
|
||||
}
|
||||
|
||||
// Left Perron vector of a strictly positive matrix by power iteration on A + I (aperiodic, so a pure cycle converges too).
|
||||
private static double[] LeftPerron(double[,] a, int n)
|
||||
{
|
||||
var u = Enumerable.Repeat(1.0 / n, n).ToArray();
|
||||
for (var iteration = 0; iteration < 20000; iteration++)
|
||||
{
|
||||
var next = new double[n];
|
||||
for (var c = 0; c < n; c++)
|
||||
for (var r = 0; r < n; r++)
|
||||
next[c] += u[r] * (a[r, c] + (r == c ? 1 : 0));
|
||||
var norm = next.Sum();
|
||||
var delta = 0.0;
|
||||
for (var c = 0; c < n; c++) { next[c] /= norm; delta = Math.Max(delta, Math.Abs(next[c] - u[c])); }
|
||||
u = next;
|
||||
if (delta < 1e-15) break;
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
// The spectral radius of every strongly connected component of a nonnegative matrix, each by power iteration on A + I
|
||||
// (primitive when irreducible). An edge j -> i is a nonzero A[i, j]. A component with no cycle has radius 0.
|
||||
private static List<(double Rho, List<int> Component)> Radii(double[,] a, int n)
|
||||
{
|
||||
var reach = new bool[n, n];
|
||||
for (var i = 0; i < n; i++)
|
||||
for (var j = 0; j < n; j++)
|
||||
reach[i, j] = i == j || a[j, i] > 0;
|
||||
for (var k = 0; k < n; k++)
|
||||
for (var i = 0; i < n; i++)
|
||||
if (reach[i, k])
|
||||
for (var j = 0; j < n; j++)
|
||||
if (reach[k, j]) reach[i, j] = true;
|
||||
var done = new bool[n];
|
||||
var radii = new List<(double, List<int>)>();
|
||||
for (var start = 0; start < n; start++)
|
||||
{
|
||||
if (done[start]) continue;
|
||||
var component = Enumerable.Range(0, n).Where(i => reach[start, i] && reach[i, start]).ToList();
|
||||
foreach (var i in component) done[i] = true;
|
||||
var rho = Irreducible(a, component);
|
||||
if (double.IsNaN(rho)) rho = double.PositiveInfinity;
|
||||
radii.Add((rho, component));
|
||||
}
|
||||
return radii;
|
||||
}
|
||||
|
||||
private static double Irreducible(double[,] a, List<int> nodes)
|
||||
{
|
||||
var m = nodes.Count;
|
||||
var x = Enumerable.Repeat(1.0, m).ToArray();
|
||||
var lambda = 0.0;
|
||||
for (var iteration = 0; iteration < 200000; iteration++)
|
||||
{
|
||||
var next = new double[m];
|
||||
for (var r = 0; r < m; r++)
|
||||
{
|
||||
next[r] = x[r];
|
||||
for (var c = 0; c < m; c++) next[r] += a[nodes[r], nodes[c]] * x[c];
|
||||
}
|
||||
var norm = next.Max();
|
||||
var moved = 0.0;
|
||||
for (var r = 0; r < m; r++) { next[r] /= norm; moved = Math.Max(moved, Math.Abs(next[r] - x[r])); }
|
||||
lambda = norm;
|
||||
x = next;
|
||||
if (moved < 1e-15) break;
|
||||
}
|
||||
return lambda - 1;
|
||||
}
|
||||
|
||||
// The component's goods in cycle order: breadth-first from its first good along "needs" edges, so a simple cycle reads
|
||||
// around once and a tangled component reads outward from where it starts.
|
||||
private static List<int> CycleOrder(double[,] a, int n, List<int> component)
|
||||
{
|
||||
var inside = new HashSet<int>(component);
|
||||
var order = new List<int> { component[0] };
|
||||
var seen = new HashSet<int> { component[0] };
|
||||
for (var at = 0; at < order.Count; at++)
|
||||
for (var i = 0; i < n; i++)
|
||||
if (a[i, order[at]] > 0 && inside.Contains(i) && seen.Add(i)) order.Add(i);
|
||||
return order;
|
||||
}
|
||||
|
||||
// A processed good owes a consumer use when it is shared (rulings consumer-demand-shared-only, draw-is-one-use). Its uses are
|
||||
// the gear or hull roles, and the rounds some weapon draws, that the goods downstream of it (itself included) fill or are; a
|
||||
// good used as upkeep is shared too. Shared means two or more such uses, or upkeep. A shared good must lead, through the
|
||||
// chain (an input, upkeep or role-input edge: a role input is an edge from each of its fillers), into a consumer good or a role one names. A good whose every use is one gear role is exempt like its final assembly.
|
||||
// Derived from the recipe graph on every check; depth and distance play no part. Raw goods and the gate count for nothing.
|
||||
public static List<EconomyFault> ConsumerDemand(CultCache catalog)
|
||||
{
|
||||
var recipes = AllRecipes(catalog).Where(p => catalog.Get(p.Recipe.Output) != null).ToDictionary(p => p.Recipe.Output.Key.Value, p => p.Recipe, StringComparer.Ordinal);
|
||||
var drawn = Drawn(catalog);
|
||||
bool IsCommodityRecipe(RecipeData r) => Recipes.IsCommodity(catalog.Get(r.Output));
|
||||
var gearRoles = DesignRoles(catalog);
|
||||
var upkeep = new HashSet<string>(recipes.Values.SelectMany(r => r.Upkeep).Select(c => c.Commodity.Key.Value), StringComparer.Ordinal);
|
||||
var takenBy = recipes.Values.Where(IsCommodityRecipe)
|
||||
.SelectMany(r => r.Inputs.Concat(r.Upkeep).Select(input => input.Commodity.Key.Value)
|
||||
.Concat(Recipes.RoleInputsOf(catalog, r).SelectMany(role => Recipes.Fillers(catalog, role.Name)).Select(filler => filler.Value))
|
||||
.Select(input => (Input: input, Output: r.Output.Key.Value)))
|
||||
.ToLookup(edge => edge.Input, edge => edge.Output, StringComparer.Ordinal);
|
||||
var faults = new List<EconomyFault>();
|
||||
foreach (var key in recipes.Keys.OrderBy(k => k, StringComparer.Ordinal))
|
||||
{
|
||||
var recipe = recipes[key];
|
||||
if (!IsCommodityRecipe(recipe) || Recipes.IsRaw(catalog, recipe) || Recipes.IsConsumerGood(catalog, recipe)) continue;
|
||||
var reached = new HashSet<string>(StringComparer.Ordinal) { key };
|
||||
var open = new Stack<string>(reached);
|
||||
while (open.Count > 0)
|
||||
foreach (var next in takenBy[open.Pop()])
|
||||
if (reached.Add(next)) open.Push(next);
|
||||
var members = reached.Select(k => recipes[k]).ToList();
|
||||
if (members.Any(r => Recipes.IsConsumerGood(catalog, r))) continue;
|
||||
var uses = members.SelectMany(r => r.Fills).Where(gearRoles.Contains).Distinct(StringComparer.Ordinal).OrderBy(role => role, StringComparer.Ordinal)
|
||||
.Concat(reached.Where(drawn.Contains).OrderBy(k => k, StringComparer.Ordinal).Select(k => catalog.Get(recipes[k].Output).Name)).ToList();
|
||||
var asUpkeep = reached.Any(upkeep.Contains);
|
||||
if (asUpkeep) uses.Add("upkeep");
|
||||
if (uses.Count < 2 && !asUpkeep) continue;
|
||||
var list = uses.Count == 1 ? uses[0] : string.Join(", ", uses.Take(uses.Count - 1)) + " and " + uses[uses.Count - 1];
|
||||
faults.Add(new EconomyFault(LDemand, RecipeData.KeyOf(new CultRecordKey(key)).Value,
|
||||
$"'{catalog.Get(recipe.Output).Name}' leads into {list} but no use leads into a consumer good."));
|
||||
}
|
||||
return faults;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5e0d62a8f1c34b79b2f58ea3c6d19042
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1782,18 +1782,13 @@ public abstract class Entity
|
|||
Parent = null;
|
||||
}
|
||||
|
||||
// Indexed, not foreach: Equipment is a ReactiveCollection whose enumerator is heap-allocated, and every round a gun fires
|
||||
// asks for the targeting system several times (rounds per step: hundreds of rounds a step must not allocate each).
|
||||
public T GetBehavior<T>() where T : Behavior
|
||||
{
|
||||
for (var i = 0; i < Equipment.Count; i++)
|
||||
{
|
||||
var behaviors = Equipment[i].Behaviors;
|
||||
if (behaviors == null) continue;
|
||||
foreach (var behavior in behaviors)
|
||||
if (behavior is T b)
|
||||
return b;
|
||||
}
|
||||
foreach (var equippedItem in Equipment)
|
||||
if(equippedItem.Behaviors != null)
|
||||
foreach (var behavior in equippedItem.Behaviors)
|
||||
if (behavior is T b)
|
||||
return b;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1821,13 +1816,6 @@ public abstract class Entity
|
|||
public virtual void Update(float delta)
|
||||
{
|
||||
TurnRate = 0;
|
||||
// A parented entity (a docked ship) sits on its parent, active or not: docking deactivates the ship, so this pin
|
||||
// comes before the inactive return, or the ship keeps whatever pose its own physics last gave it.
|
||||
if (Parent != null)
|
||||
{
|
||||
Position = Parent.Position;
|
||||
Velocity = Parent.Velocity;
|
||||
}
|
||||
if (!_active) return;
|
||||
|
||||
var velocityBefore = Velocity;
|
||||
|
|
@ -1944,7 +1932,12 @@ public abstract class Entity
|
|||
foreach(var child in Children)
|
||||
child.Update(delta);
|
||||
|
||||
if (Parent == null) Position.y = RideHeight(Position.xz);
|
||||
if (Parent != null)
|
||||
{
|
||||
Position = Parent.Position;
|
||||
Velocity = Parent.Velocity;
|
||||
}
|
||||
else Position.y = RideHeight(Position.xz);
|
||||
|
||||
Acceleration = delta > 0f ? (Velocity + LimitClamp - velocityBefore) / delta : float2(0, 0);
|
||||
Manoeuvre.Observe(Acceleration, delta, ItemManager.GameplaySettings.SolutionWindow);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ public static class EntitySerializer
|
|||
{
|
||||
Orbit = new CultRecordRef<OrbitData>(orbital.OrbitData),
|
||||
SecurityLevel = orbital.SecurityLevel,
|
||||
SecurityRadius = orbital.SecurityRadius,
|
||||
Kind = orbital.Kind
|
||||
SecurityRadius = orbital.SecurityRadius
|
||||
};
|
||||
else if (entity is Ship ship)
|
||||
pack = new ShipPack
|
||||
|
|
@ -121,7 +120,6 @@ public static class EntitySerializer
|
|||
Restore(itemManager, zone, pack, entity);
|
||||
entity.SecurityLevel = pack.SecurityLevel;
|
||||
entity.SecurityRadius = pack.SecurityRadius;
|
||||
entity.Kind = pack.Kind;
|
||||
if (pack.Story >= 0) entity.Story = zone.GalaxyZone.Locations[pack.Story];
|
||||
return entity;
|
||||
}
|
||||
|
|
@ -216,7 +214,6 @@ public class OrbitalEntityPack : EntityPack
|
|||
[Key(18)] public int Story = -1;
|
||||
[Key(19)] public SecurityLevel SecurityLevel;
|
||||
[Key(20)] public float SecurityRadius;
|
||||
[Key(22)] public CultRecordRef<StationKindData> Kind;
|
||||
}
|
||||
|
||||
[MessagePackObject,
|
||||
|
|
@ -240,7 +237,7 @@ public abstract class EntityPack
|
|||
[Key(14)] public EntitySettings Settings;
|
||||
[Key(15)] public CultRecordRef<Faction> Faction;
|
||||
[Key(16)] public int[][] WeaponGroups;
|
||||
// Keys 17-20 belong to the subclasses; the base's next free key is 23 (Boss is 21, the orbital pack's Kind is 22).
|
||||
// Keys 17-20 belong to the subclasses; the base's next free key is 21.
|
||||
[Key(21)] public bool Boss;
|
||||
|
||||
private int _price;
|
||||
|
|
|
|||
|
|
@ -698,11 +698,7 @@ public static class FireControl
|
|||
// Damage * interval, through this exact same freeze-and-queue path (a flight time of zero, since a beam's
|
||||
// authored Velocity is 0, commits and resolves in the tick it fires -- R4's short-flight degradation,
|
||||
// unchanged). Every other caller keeps reading the frozen weapon.Damage it always did.
|
||||
// Rounds per step (ruling rounds-per-step): a gun fires every round its cooldown allows in a step, so a round is born
|
||||
// `age` seconds before the step's end, where the shooter and its target were then (carried back along their velocities).
|
||||
// FireTime, the origin, the target's frozen pose and the arrival and commit times all follow, so a presenter draws the round
|
||||
// from where it left and the commit measures the target's stray from the line it was fired along.
|
||||
public static int Fire(Weapon weapon, EquippedItem item, Entity source, float? damageOverride = null, float age = 0f)
|
||||
public static int Fire(Weapon weapon, EquippedItem item, Entity source, float? damageOverride = null)
|
||||
{
|
||||
var zone = source.Zone;
|
||||
if (IsMineLayer(weapon))
|
||||
|
|
@ -712,7 +708,7 @@ public static class FireControl
|
|||
}
|
||||
// Mining Cut 3: a chunk target fires as an untargeted round until Cut 4 routes shots at chunks.
|
||||
var target = source.Target.Value.Entity;
|
||||
var now = zone.Time - age;
|
||||
var now = zone.Time;
|
||||
|
||||
// R1's engage gate and the shooter-side probability, evaluated now and frozen: nothing at commit time
|
||||
// re-reads a stat, an info level or a range. Cut 12.2: PFire no longer carries the spread/hull share --
|
||||
|
|
@ -728,14 +724,14 @@ public static class FireControl
|
|||
var burstPosition = solution.BurstPosition;
|
||||
var fuse = solution.Fuse;
|
||||
var blastRadius = solution.BlastRadius;
|
||||
var origin = source.Position - float3(source.Velocity.x, 0, source.Velocity.y) * age;
|
||||
var origin = source.Position;
|
||||
|
||||
var targetVelocity = float3.zero;
|
||||
var targetPosition = origin;
|
||||
if (engaged != null)
|
||||
{
|
||||
targetVelocity = float3(engaged.Velocity.x, 0, engaged.Velocity.y);
|
||||
targetPosition = engaged.Position - targetVelocity * age;
|
||||
targetPosition = engaged.Position;
|
||||
}
|
||||
var flightTime = weapon.Velocity > .01f ? solution.FlightDistance / weapon.Velocity : 0f;
|
||||
|
||||
|
|
@ -783,12 +779,7 @@ public static class FireControl
|
|||
// point is decided at its commit (CommitBurst), which reads it there. Each shot reads it once.
|
||||
if (!shot.DeliveredAtCommit) Deliver(ref shot, solution.BurstReach);
|
||||
|
||||
// The queue is in fire-time order, ties in shot-id order (ids rise as shots are made): Step walks it oldest first. Rounds
|
||||
// of one step are born shooter by shooter, so a later shooter's earlier round goes in before an earlier shooter's later one.
|
||||
var queue = zone.PendingShots;
|
||||
var place = queue.Count;
|
||||
while (place > 0 && queue[place - 1].FireTime > shot.FireTime) place--;
|
||||
queue.Insert(place, shot);
|
||||
zone.PendingShots.Add(shot);
|
||||
return shot.ShotId;
|
||||
}
|
||||
|
||||
|
|
@ -854,10 +845,7 @@ public static class FireControl
|
|||
if (shots.Count == 0) return;
|
||||
|
||||
var now = zone.Time;
|
||||
// Commits walk the queue in fire-time order, oldest first (Fire keeps it in it). Shots queued while the walk runs are
|
||||
// left to the next step.
|
||||
List<(float Arrival, int Id)> arrived = null;
|
||||
for (int i = 0, end = shots.Count; i < end;)
|
||||
for (var i = shots.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var shot = shots[i];
|
||||
|
||||
|
|
@ -871,7 +859,6 @@ public static class FireControl
|
|||
// target is gone. Republishing the committed outcome here used to put a hit marker on a corpse.
|
||||
var miss = MakeOutcome(shot, ShotResult.Miss, false, false, int2.zero, float2.zero, 0f, now);
|
||||
shots.RemoveAt(i);
|
||||
end--;
|
||||
if (!shot.Committed) zone.ShotCommitted.OnNext(miss);
|
||||
zone.ShotResolved.OnNext(miss);
|
||||
continue;
|
||||
|
|
@ -892,41 +879,16 @@ public static class FireControl
|
|||
|
||||
if (shot.Committed && now >= shot.ArrivalTime)
|
||||
{
|
||||
(arrived ??= new List<(float, int)>()).Add((shot.ArrivalTime, shot.ShotId));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (arrived == null) return;
|
||||
|
||||
// The rounds that arrive in this step apply in arrival order, ties in shot-id order (ids rise as shots are made), so
|
||||
// which of them breaks a shield or lands a kill follows when each hit, not the order the shooters were stepped in or the
|
||||
// order their rounds left.
|
||||
arrived.Sort((a, b) => a.Arrival != b.Arrival ? a.Arrival.CompareTo(b.Arrival) : a.Id.CompareTo(b.Id));
|
||||
foreach (var (_, id) in arrived)
|
||||
{
|
||||
var i = shots.FindIndex(q => q.ShotId == id);
|
||||
if (i < 0) continue;
|
||||
var shot = shots[i];
|
||||
|
||||
var targetGone = shot.Target != null && !zone.Entities.Contains(shot.Target);
|
||||
if (targetGone)
|
||||
{
|
||||
// As in the commit walk: a shot whose target left the zone in this step resolves as a miss.
|
||||
var miss = MakeOutcome(shot, ShotResult.Miss, false, false, int2.zero, float2.zero, 0f, now);
|
||||
// Cut 12.4(b) (docs/fire-control-cut.md, "Which model a shot uses"): Apply is the one owner of
|
||||
// which model a shot uses, through its own switch on the frozen Fuse -- Step has no damage
|
||||
// branch left. A direct hit, a proximity burst and a contact or delayed blast all arrive here
|
||||
// the same way; only Apply decides what that means.
|
||||
// Removed first: Apply reaches damage observers (user code), and a shot left in the queue by one
|
||||
// that throws would resolve, and hurt, again on every later tick.
|
||||
shots.RemoveAt(i);
|
||||
zone.ShotResolved.OnNext(miss);
|
||||
continue;
|
||||
Apply(zone, shot);
|
||||
zone.ShotResolved.OnNext(shot.Outcome);
|
||||
}
|
||||
|
||||
// Cut 12.4(b) (docs/fire-control-cut.md, "Which model a shot uses"): Apply is the one owner of
|
||||
// which model a shot uses, through its own switch on the frozen Fuse -- Step has no damage
|
||||
// branch left. A direct hit, a proximity burst and a contact or delayed blast all arrive here
|
||||
// the same way; only Apply decides what that means.
|
||||
// Removed first: Apply reaches damage observers (user code), and a shot left in the queue by one
|
||||
// that throws would resolve, and hurt, again on every later tick.
|
||||
shots.RemoveAt(i);
|
||||
Apply(zone, shot);
|
||||
zone.ShotResolved.OnNext(shot.Outcome);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,6 @@ public class Galaxy
|
|||
// The run's seed, minted once by GalaxyStage.SeedFrom and carried by the save (SavedGame.Seed). It seeds
|
||||
// generation, the run's item draws (RunStart) and the zone's drop dice (Zone.DropSeed).
|
||||
public uint Seed { get; }
|
||||
|
||||
// The run's resource provinces, ordered by seed zone index; GalaxyZone.Province is each zone's. Built once, by the
|
||||
// generation constructors or the save constructor (BuildProvinces), and never by PlaceGate, faction placement or naming.
|
||||
public GalaxyProvince[] Provinces { get; private set; }
|
||||
|
||||
private GalaxyZone[] _exitPath;
|
||||
private readonly CultCache _cache;
|
||||
|
|
@ -93,9 +89,6 @@ public class Galaxy
|
|||
if(savedGame.Exit != -1)
|
||||
Exit = Zones[savedGame.Exit];
|
||||
|
||||
// A save from before ore geography has no provinces: it gets them by the rule generation uses.
|
||||
BuildProvinces(global::Provinces.DefaultZonesPerProvince, savedGame.Provinces == null ? null : savedZones.Select(zone => zone.Province).ToArray(), savedGame.Provinces);
|
||||
|
||||
CalculateDistanceMatrix();
|
||||
}
|
||||
|
||||
|
|
@ -134,8 +127,6 @@ public class Galaxy
|
|||
|
||||
PlaceFactionsMain(settings.BossCount, progressCallback);
|
||||
|
||||
BuildProvinces(settings.ProvinceZones);
|
||||
|
||||
CalculateFactionInfluence(progressCallback);
|
||||
|
||||
NamePlaces(ref random, progressCallback);
|
||||
|
|
@ -144,39 +135,6 @@ public class Galaxy
|
|||
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
||||
}
|
||||
|
||||
// Each zone's index and the links as indices: the graph Provinces reads.
|
||||
private (Dictionary<GalaxyZone, int> Index, int[][] Adjacency) ZoneGraph()
|
||||
{
|
||||
var index = Zones.Select((zone, i) => (zone, i)).ToDictionary(pair => pair.zone, pair => pair.i);
|
||||
return (index, Zones.Select(zone => zone.AdjacentZones.Select(next => index[next]).ToArray()).ToArray());
|
||||
}
|
||||
|
||||
// Regions, then each region's provinces and their types (Provinces.Regions, Partition, Deal); the one builder, called by
|
||||
// the generation constructors and the save constructor. A save passes its membership and types: membership is kept, and a
|
||||
// saved type that still resolves is kept, any other taking the card the region's Deal gives that province's place.
|
||||
private void BuildProvinces(int zonesPerProvince, int[] savedSeedOf = null, List<SavedProvince> savedTypes = null)
|
||||
{
|
||||
var (index, adjacency) = ZoneGraph();
|
||||
var regionOf = global::Provinces.Regions(adjacency, BossZones.Values.Select(zone => index[zone]).ToArray(), Exit == null ? -1 : index[Exit]);
|
||||
var seedOf = savedSeedOf ?? global::Provinces.Partition(adjacency, regionOf, Seed, zonesPerProvince);
|
||||
var types = savedTypes?.ToDictionary(saved => saved.Seed, saved => saved.Type);
|
||||
|
||||
var provinces = new List<GalaxyProvince>();
|
||||
foreach (var region in Enumerable.Range(0, Zones.Length).GroupBy(zone => regionOf[zone]).OrderBy(group => group.Key))
|
||||
{
|
||||
var seeds = region.Select(zone => seedOf[zone]).Distinct().OrderBy(seed => seed).ToArray();
|
||||
var hand = global::Provinces.Deal(region.Key, seeds.Length, Seed, _cache);
|
||||
for (var place = 0; place < seeds.Length; place++)
|
||||
{
|
||||
var type = types != null && types.TryGetValue(seeds[place], out var saved) && _cache.Get(saved) != null ? saved : hand[place];
|
||||
var province = new GalaxyProvince {Seed = Zones[seeds[place]], Type = type, Zones = region.Where(zone => seedOf[zone] == seeds[place]).Select(zone => Zones[zone]).ToArray()};
|
||||
foreach (var zone in province.Zones) zone.Province = province;
|
||||
provinces.Add(province);
|
||||
}
|
||||
}
|
||||
Provinces = provinces.OrderBy(province => index[province.Seed]).ToArray();
|
||||
}
|
||||
|
||||
public Faction ResolveFaction(string name)
|
||||
{
|
||||
return _cache.GetAll<Faction>().FirstOrDefault(f => f.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
|
@ -298,9 +256,6 @@ public class Galaxy
|
|||
|
||||
CalculateFactionInfluence(progressCallback);
|
||||
|
||||
// No boss zone and no Exit yet (PlaceGate comes after): the prelude is one region.
|
||||
BuildProvinces(settings.ProvinceZones);
|
||||
|
||||
NamePlaces(ref random, progressCallback);
|
||||
|
||||
// progressCallback?.Invoke("Weaving Narrative");
|
||||
|
|
@ -348,10 +303,6 @@ public class Galaxy
|
|||
BossZones.Values.Aggregate(1, (i, os) => i * os.Distance[z]));
|
||||
}
|
||||
|
||||
// One exit per region (ruling one-exit-per-region): prune the boss zones' extra links, then home placement below reads
|
||||
// the distances of the graph that is left.
|
||||
OneExitPerRegion(progressCallback);
|
||||
|
||||
// Place boss mega headquarters such that their sphere of influence encompasses their boss zone
|
||||
// While occupying as much territory as possible
|
||||
foreach (var mega in bossMegas)
|
||||
|
|
@ -373,21 +324,6 @@ public class Galaxy
|
|||
}
|
||||
}
|
||||
|
||||
// Each boss zone keeps one link toward the exit, the one on the exit path, so there is one exit to defend where its boss
|
||||
// parks (Provinces.ExtraExits picks the rest; ruling one-exit-per-region). Links are removed from both zones, and the
|
||||
// distance matrix follows. A galaxy built from a save never comes here: its saved links are the world.
|
||||
private void OneExitPerRegion(Action<string> progressCallback)
|
||||
{
|
||||
var (index, adjacency) = ZoneGraph();
|
||||
var exitPath = ExitPath.Select(zone => index[zone]).ToArray();
|
||||
foreach (var (boss, far) in global::Provinces.ExtraExits(adjacency, BossZones.Values.Select(zone => index[zone]).ToArray(), index[Exit], exitPath))
|
||||
{
|
||||
Zones[boss].AdjacentZones.Remove(Zones[far]);
|
||||
Zones[far].AdjacentZones.Remove(Zones[boss]);
|
||||
}
|
||||
CalculateDistanceMatrix(progressCallback);
|
||||
}
|
||||
|
||||
private void CalculateFactionInfluence(Action<string> progressCallback = null)
|
||||
{
|
||||
progressCallback?.Invoke("Calculating Faction Influence");
|
||||
|
|
@ -635,20 +571,10 @@ public class GalaxyZone
|
|||
public Faction Owner;
|
||||
public Zone Contents;
|
||||
public ZonePack PackedContents;
|
||||
public GalaxyProvince Province;
|
||||
public bool NamedZone;
|
||||
public List<LocationStory> Locations = new List<LocationStory>();
|
||||
}
|
||||
|
||||
// A resource province (ruling provinces-within-regions): a contiguous group of zones inside one region, grown from Seed,
|
||||
// whose Type (ProvinceData, unset when the catalog holds none) weights the kinds and ores of its belts.
|
||||
public class GalaxyProvince
|
||||
{
|
||||
public GalaxyZone Seed;
|
||||
public CultRecordRef<ProvinceData> Type;
|
||||
public GalaxyZone[] Zones;
|
||||
}
|
||||
|
||||
public class GalaxyQuest
|
||||
{
|
||||
public Story Story;
|
||||
|
|
|
|||
|
|
@ -13,20 +13,6 @@ public class HelmInput
|
|||
|
||||
public void Toggle() => Latched = !Latched;
|
||||
|
||||
private float _turnAxis;
|
||||
|
||||
// The turn axis as the helm reads it. A digital input (keys) ramps up while held and back down while released, both at
|
||||
// full scale per rampTime, so tapping at a rate holds a gentle turn; reversing cuts to zero and ramps the other way.
|
||||
// An analog input passes straight through.
|
||||
public float TurnAxis(float raw, bool digital, float dt, float rampTime)
|
||||
{
|
||||
if (!digital || rampTime <= 0) return _turnAxis = raw;
|
||||
var step = dt / rampTime;
|
||||
if (raw == 0) return _turnAxis = sign(_turnAxis) * max(0f, abs(_turnAxis) - step);
|
||||
if (sign(raw) != sign(_turnAxis)) _turnAxis = 0;
|
||||
return _turnAxis = sign(raw) * min(abs(raw), abs(_turnAxis) + step);
|
||||
}
|
||||
|
||||
// The turn the player commands: the aim heading, resolved against the hull at each sim step, or the turn axis.
|
||||
public TurnCommand Demand(float2 aim, float turnAxis) =>
|
||||
FaceAim ? TurnCommand.Toward(aim) : TurnCommand.OfRate(clamp(turnAxis, -1f, 1f));
|
||||
|
|
|
|||
|
|
@ -337,17 +337,14 @@ public abstract class CraftedItemData : ItemData
|
|||
public List<ItemRole> Roles = new List<ItemRole>();
|
||||
}
|
||||
|
||||
// One slot in a design: every laser has a focusing array. A role is a named slot with a count: the design takes Count units
|
||||
// of a part, and any commodity whose recipe Fills the role can be that part. What fills it is a quality, authored per role
|
||||
// by each manufacturer's product. Records written before Count existed read it as 1.
|
||||
// One slot in a design: every laser has a focusing array. A role is a name and nothing else. What fills it is
|
||||
// a quality, authored per role by each manufacturer's product; the part itself earns a record when crafting
|
||||
// needs one to exist.
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class ItemRole
|
||||
{
|
||||
[Inspectable, JsonProperty("name"), Key(0)]
|
||||
public string Name;
|
||||
|
||||
[Inspectable, JsonProperty("count"), Key(1)]
|
||||
public int Count = 1;
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.compoundcommoditydata", "1"), Inspectable, MessagePackObject]
|
||||
|
|
@ -552,12 +549,6 @@ public class HullData : EquippableItemData
|
|||
[Inspectable, JsonProperty("visual"), Key(32)]
|
||||
public CultRecordRef<ShipAuthoring> Visual;
|
||||
|
||||
// What a station hull is built to be (StationKinds): set on every Station hull and on no other. The hull names its kind
|
||||
// and the kind names no hull, so a package can add a faction's model of a kind without editing any kind record.
|
||||
// Key 33 is the next free key.
|
||||
[Inspectable, JsonProperty("kind"), Key(33)]
|
||||
public CultRecordRef<StationKindData> Kind;
|
||||
|
||||
// The farthest corner of any occupied hull cell from the centre of mass the schematic frame uses, in world
|
||||
// units: a true bound on how far the hull reaches from its position (the half-diagonal of the bounding box is not,
|
||||
// as FireControl.Detonate's comment records). A cell at (x, y) spans x +/- .5 and y +/- .5.
|
||||
|
|
|
|||
|
|
@ -74,12 +74,9 @@ public class LoadoutGenerator
|
|||
return EntitySerializer.Pack(entity) as OrbitalEntityPack;
|
||||
}
|
||||
|
||||
// A station of one kind: the hull is drawn among the station hulls the kind calls for in this faction's hands
|
||||
// (StationKinds.HullKindFor), so a faction with no model of the kind builds its stand-in's hull and keeps the kind.
|
||||
public OrbitalEntityPack GenerateStationLoadout(CultRecordRef<StationKindData> kind)
|
||||
public OrbitalEntityPack GenerateStationLoadout()
|
||||
{
|
||||
var hullKind = StationKinds.HullKindFor(ItemManager.ItemData, Faction, kind);
|
||||
var (hullProduct, hullData) = RandomHull(HullType.Station, hull => hull.Kind.Key.Equals(hullKind.Key));
|
||||
var (hullProduct, hullData) = RandomHull(HullType.Station);
|
||||
if(hullData==null)
|
||||
{
|
||||
ItemManager.Log("Unable to generate station loadout: no compatible hull found!");
|
||||
|
|
@ -88,9 +85,6 @@ public class LoadoutGenerator
|
|||
var hull = ItemManager.CreateInstance(hullProduct) as EquippableItem;
|
||||
var entity = new OrbitalEntity(ItemManager, null, hull, default, ItemManager.GameplaySettings.DefaultEntitySettings);
|
||||
entity.Faction = Faction;
|
||||
entity.Kind = kind;
|
||||
// A stand-in hull keeps its own model but not its own name: the station is what its kind says it is.
|
||||
if (!hullData.Kind.Key.Equals(kind.Key) && ItemManager.ItemData.Get(kind) is { } kindData) entity.Name = kindData.Name;
|
||||
|
||||
// Hardpoint gear goes in first so the docking bay can only take space that gear left free
|
||||
EquipHardpoints(entity);
|
||||
|
|
@ -200,8 +194,11 @@ public class LoadoutGenerator
|
|||
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
|
||||
.Select(product => (product, design: ItemManager.ItemData.Get(product.Design) as T))
|
||||
.Where(entry =>
|
||||
entry.design != null &&
|
||||
entry.design.Price > 0 &&
|
||||
entry.product.Manufacturer.IsSet() &&
|
||||
// A not-offered product is never a candidate, so the any-manufacturer fallback below cannot reach it either.
|
||||
IsDrawable(ItemManager.ItemData, entry.product, entry.design) &&
|
||||
IsOffered(ItemManager.ItemData, entry.product) &&
|
||||
(!(entry.design is EquippableItemData equippable) || HasHome(equippable, hulls)) &&
|
||||
(filter?.Invoke(entry.design) ?? true))
|
||||
.ToArray();
|
||||
|
|
@ -243,12 +240,6 @@ public class LoadoutGenerator
|
|||
Galaxy == null || Faction == null ||
|
||||
ItemManager.ItemData.Get(product.Manufacturer) == Faction || Faction.Allegiance.ContainsKey(product.Manufacturer));
|
||||
|
||||
// Whether a product can be drawn for its design at all: the design resolves, has a price, the product names a maker and is
|
||||
// offered. The one definition of 'a maker offers this design'; AvailableProducts selects by it and StationKinds decides which
|
||||
// station kinds a hull can be drawn for by it, so a kind is kept exactly when a hull of it can be drawn.
|
||||
public static bool IsDrawable(CultCache items, FactionProductData product, CraftedItemData design) =>
|
||||
design != null && design.Price > 0 && product.Manufacturer.IsSet() && IsOffered(items, product);
|
||||
|
||||
// Whether a product is on the market at all (ruling off-market-is-unoffered-product). The one reader of NotOffered. A bin is
|
||||
// offered only if both it and its line are (ruling bin-inherits-line-offered); the catalog read guarantees a bin's line exists.
|
||||
public static bool IsOffered(CultCache items, FactionProductData product) =>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ public class OrbitalEntity : Entity
|
|||
public float SecurityRadius;
|
||||
public LocationStory Story;
|
||||
public bool CanTow;
|
||||
// What this station is for (StationKinds): its own fact, which under a stand-in hull differs from the hull's kind.
|
||||
public CultRecordRef<StationKindData> Kind;
|
||||
|
||||
public OrbitalEntity(ItemManager itemManager, Zone zone, EquippableItem hull, CultRecordKey orbit, EntitySettings settings) : base(itemManager, zone, hull, settings)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -94,14 +94,6 @@ public sealed class PilotCommand
|
|||
// The pilot's own pending decisions, which it reads back instead of the ship (that is last step's applied state).
|
||||
public TargetRef? Designated => Designations.Count > 0 ? Designations[Designations.Count - 1] : (TargetRef?) null;
|
||||
|
||||
// Whether this pilot has commanded a trigger, pull or release, for `behavior` this step.
|
||||
public bool Commands(ActivatedBehavior behavior)
|
||||
{
|
||||
foreach (var trigger in Triggers)
|
||||
if (ReferenceEquals(trigger.Behavior, behavior)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the last trigger this pilot commanded for `behavior` this step is a pull.
|
||||
public bool Pulls(ActivatedBehavior behavior)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// How a good is made: one authored record per output, keyed recipe:<output key>, so a second recipe for one output replaces the
|
||||
// first. A commodity's recipe takes named commodities (Inputs) and roles (RoleInputs), each role filled by any commodity whose
|
||||
// recipe Fills it, and says which roles it can fill. A role recipe (the output is a design with roles, a ship hull, a piece of
|
||||
// gear, a consumer good) restates nothing of the design: its role inputs are the design's Roles and their counts. Station and
|
||||
// Turret hulls take no recipe.
|
||||
[CultDocument("aetheria.recipedata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class RecipeData
|
||||
{
|
||||
[JsonProperty("output"), Key(0)]
|
||||
public CultRecordRef<ItemData> Output;
|
||||
|
||||
[JsonProperty("outputCount"), Key(1)]
|
||||
public int OutputCount = 1;
|
||||
|
||||
// Commodities one cycle consumes. Empty on a raw good and on a role recipe.
|
||||
[JsonProperty("inputs"), Key(2)]
|
||||
public List<CommodityCount> Inputs = new List<CommodityCount>();
|
||||
|
||||
// Seconds of sim time for one cycle.
|
||||
[JsonProperty("cycle"), Key(3)]
|
||||
public float Cycle = 1;
|
||||
|
||||
// Crew per running facility; no kind until workforce kinds exist.
|
||||
[JsonProperty("workforce"), Key(4)]
|
||||
public int Workforce;
|
||||
|
||||
// Commodities one cycle uses up on the side (power, coolant), per cycle.
|
||||
[JsonProperty("upkeep"), Key(5)]
|
||||
public List<CommodityCount> Upkeep = new List<CommodityCount>();
|
||||
|
||||
// The kind of station that runs this recipe.
|
||||
[JsonProperty("stationKind"), Key(6)]
|
||||
public CultRecordRef<StationKindData> StationKind;
|
||||
|
||||
// Why this good exists in the world, in the operator's reading. Presence is checked, never its meaning.
|
||||
[JsonProperty("loreHook"), Key(7)]
|
||||
public string LoreHook;
|
||||
|
||||
// The roles of a design this commodity can fill. Only a commodity's recipe has any.
|
||||
[JsonProperty("fills"), Key(8)]
|
||||
public List<string> Fills = new List<string>();
|
||||
|
||||
// Roles one cycle consumes, any filler will do and the facility picks (ruling ores-fill-metal-roles). Empty on a role recipe;
|
||||
// records written before key 9 read it as empty.
|
||||
[JsonProperty("roleInputs"), Key(9)]
|
||||
public List<ItemRole> RoleInputs = new List<ItemRole>();
|
||||
|
||||
public static CultRecordKey KeyOf(CultRecordKey outputKey) => new CultRecordKey("recipe:" + outputKey.Value);
|
||||
}
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class CommodityCount
|
||||
{
|
||||
[JsonProperty("commodity"), Key(0)]
|
||||
public CultRecordRef<ItemData> Commodity;
|
||||
|
||||
[JsonProperty("count"), Key(1)]
|
||||
public int Count = 1;
|
||||
}
|
||||
|
||||
// A region gate's trade with the world beyond it: what the outside buys and sells, at what price and at most how fast.
|
||||
// The union of all records is the outside; keyed outside-market:<id>.
|
||||
[CultDocument("aetheria.outsidemarketdata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class OutsideMarketData
|
||||
{
|
||||
[CultName, JsonProperty("name"), Key(0)]
|
||||
public string Name;
|
||||
|
||||
[JsonProperty("rows"), Key(1)]
|
||||
public List<OutsideMarketRow> Rows = new List<OutsideMarketRow>();
|
||||
|
||||
public static CultRecordKey KeyOf(string id) => new CultRecordKey("outside-market:" + id);
|
||||
}
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class OutsideMarketRow
|
||||
{
|
||||
[JsonProperty("commodity"), Key(0)]
|
||||
public CultRecordRef<ItemData> Commodity;
|
||||
|
||||
[JsonProperty("price"), Key(1)]
|
||||
public int Price;
|
||||
|
||||
[JsonProperty("buys"), Key(2)]
|
||||
public bool Buys;
|
||||
|
||||
[JsonProperty("sells"), Key(3)]
|
||||
public bool Sells;
|
||||
|
||||
[JsonProperty("ratePerHour"), Key(4)]
|
||||
public float RatePerHour;
|
||||
}
|
||||
|
||||
// The one reader of recipes: which recipe makes a good, a recipe's role slots (RoleInputsOf), which commodities fill a role, and
|
||||
// what a good is by its recipe.
|
||||
public static class Recipes
|
||||
{
|
||||
public static RecipeData RecipeOf(CultCache catalog, ItemData item) =>
|
||||
item == null ? null : catalog.Get<RecipeData>(RecipeData.KeyOf(catalog.RefOf(item).Key));
|
||||
|
||||
public static bool IsCommodity(ItemData item) => item is SimpleCommodityData || item is CompoundCommodityData;
|
||||
|
||||
public static IReadOnlyList<ItemRole> RolesOf(ItemData item) =>
|
||||
(item as CraftedItemData)?.Roles ?? new List<ItemRole>();
|
||||
|
||||
// A recipe whose output is a design with roles: a part list rather than a list of inputs.
|
||||
public static bool IsRoleRecipe(CultCache catalog, RecipeData recipe)
|
||||
{
|
||||
var output = catalog.Get(recipe.Output);
|
||||
return output is CraftedItemData && RolesOf(output).Count > 0;
|
||||
}
|
||||
|
||||
// The one reader of a recipe's role slots: the design's Roles for a role recipe, else the recipe's RoleInputs.
|
||||
public static IReadOnlyList<ItemRole> RoleInputsOf(CultCache catalog, RecipeData recipe) =>
|
||||
IsRoleRecipe(catalog, recipe) ? RolesOf(catalog.Get(recipe.Output)) : (IReadOnlyList<ItemRole>) recipe.RoleInputs ?? new List<ItemRole>();
|
||||
|
||||
// Raw: takes no commodity and no role. Everything else is processed. Derived, never stored.
|
||||
public static bool IsRaw(CultCache catalog, RecipeData recipe) => recipe.Inputs.Count == 0 && RoleInputsOf(catalog, recipe).Count == 0;
|
||||
|
||||
// A commodity made by a role recipe: it is assembled from the same components gear is.
|
||||
public static bool IsConsumerGood(CultCache catalog, RecipeData recipe) =>
|
||||
IsCommodity(catalog.Get(recipe.Output)) && IsRoleRecipe(catalog, recipe);
|
||||
|
||||
// The commodities whose recipes Fill the role, ordered by key so a choice never depends on store order.
|
||||
public static CultRecordKey[] Fillers(CultCache catalog, string role) =>
|
||||
catalog.GetAll<RecipeData>()
|
||||
.Where(recipe => recipe.Fills != null && recipe.Fills.Contains(role) && IsCommodity(catalog.Get(recipe.Output)))
|
||||
.Select(recipe => recipe.Output.Key)
|
||||
.OrderBy(key => key.Value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3c9a51d7e0b24f68a1e47d92b5c08f31
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -55,22 +55,6 @@ public class SavedGame
|
|||
// by Galaxy's save constructor; 0 in a save written before run-seed, which then rolls name-only drops.
|
||||
[Key(14)]
|
||||
public uint Seed;
|
||||
|
||||
// The run's resource provinces (Galaxy.Provinces): each province's seed zone index and its type. Written only by
|
||||
// RunSave.Capture and read only by Galaxy's save constructor; null in a save written before ore geography, which then
|
||||
// gets Regions, Partition and Deal from its saved graph, boss zones, Exit and seed.
|
||||
[Key(15)]
|
||||
public List<SavedProvince> Provinces;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class SavedProvince
|
||||
{
|
||||
[Key(0)]
|
||||
public int Seed;
|
||||
|
||||
[Key(1)]
|
||||
public CultRecordRef<ProvinceData> Type;
|
||||
}
|
||||
|
||||
// The run's lifecycle over the run store. Only Commit creates SavedGame, SavedZone and ProvenanceLedger records;
|
||||
|
|
@ -112,11 +96,8 @@ public static class RunSave
|
|||
AdjacentZones = zone.AdjacentZones.Select(az => Array.IndexOf(galaxy.Zones, az)).ToArray(),
|
||||
Factions = zone.Factions.Select(f => Array.IndexOf(factions, f)).ToArray(),
|
||||
Contents = zone.Contents?.PackZone() ?? zone.PackedContents,
|
||||
Owner = zone.Owner == null ? -1 : Array.IndexOf(factions, zone.Owner),
|
||||
Province = Array.IndexOf(galaxy.Zones, zone.Province.Seed)
|
||||
Owner = zone.Owner == null ? -1 : Array.IndexOf(factions, zone.Owner)
|
||||
}).ToArray();
|
||||
game.Provinces = galaxy.Provinces.Select(province => new SavedProvince
|
||||
{Seed = Array.IndexOf(galaxy.Zones, province.Seed), Type = province.Type}).ToList();
|
||||
return (game, zones);
|
||||
}
|
||||
|
||||
|
|
@ -240,11 +221,6 @@ public class SavedZone
|
|||
|
||||
[Key(5)]
|
||||
public ZonePack Contents;
|
||||
|
||||
// The seed zone index of the zone's province. Written only by RunSave.Capture and read only by Galaxy's save
|
||||
// constructor; meaningful only when SavedGame.Provinces is present.
|
||||
[Key(6)]
|
||||
public int Province;
|
||||
}
|
||||
|
||||
[MessagePackObject,
|
||||
|
|
|
|||
|
|
@ -83,8 +83,6 @@ public class SectorGenerationSettings
|
|||
{
|
||||
public float LinkDensity = .5f;
|
||||
public int ZoneCount = 128;
|
||||
// Zones per resource province (Provinces.Partition): calibration, not a gate.
|
||||
public int ProvinceZones = Provinces.DefaultZonesPerProvince;
|
||||
public int MegaCount;
|
||||
public int BossCount;
|
||||
}
|
||||
|
|
@ -99,8 +97,6 @@ public class TutorialGenerationSettings
|
|||
public string QuestFaction;
|
||||
public float LinkDensity = .5f;
|
||||
public int ZoneCount = 32;
|
||||
// Zones per resource province (Provinces.Partition): calibration, not a gate.
|
||||
public int ProvinceZones = Provinces.DefaultZonesPerProvince;
|
||||
}
|
||||
|
||||
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
|
||||
|
|
@ -187,11 +183,6 @@ public class GameplaySettings
|
|||
public float ThermalQualityMin = 2;
|
||||
public float ThermalQualityMax = .25f;
|
||||
public float DefaultShutdownPerformance = .25f;
|
||||
// Global flight tuning over every thruster's authored push (not its turning) and every hull's authored Drag. Push is a tenth, the
|
||||
// ratio gravity fell by in the honest-wells cut (aetheria-gravity:field-strength-ruled, 5 to 0.5 u/s^2): most ships
|
||||
// cannot climb straight out of a well and use orbits instead; an overspecced hull still can.
|
||||
public float ThrustScale = .1f;
|
||||
public float DragScale = 1f;
|
||||
// Free-floating bodies (floating items, mines): drag on a launch velocity, drag on the force-driven drift, and
|
||||
// how strongly the zone force accelerates it.
|
||||
public float FloatingBodyDrag = .05f;
|
||||
|
|
|
|||
|
|
@ -112,43 +112,6 @@ public class Ship : Entity
|
|||
|
||||
#endregion
|
||||
|
||||
// The player's turn axis as a fraction of the hull's yaw extreme, banded so each thruster joins over an equal share of
|
||||
// the input. The allocator serves a turn by the thrusters with the least translation per unit of yaw first and brings in
|
||||
// the next as they saturate; here the thrusters yawing the asked way are ranked by that ratio, equal ratios form one band,
|
||||
// and band i fills over input [i/k, (i+1)/k]. A hull whose best-ratio thrusters hold a small share of its yaw then gets
|
||||
// half the travel of a two-band turn before its drives join, instead of the first tenth. Full input is still full yaw.
|
||||
public float BandedTurn(float axis)
|
||||
{
|
||||
axis = clamp(axis, -1f, 1f);
|
||||
if (axis == 0 || _allThrusters == null) return axis;
|
||||
var direction = axis > 0 ? 1f : -1f;
|
||||
var thrusters = new List<(float ratio, float yaw)>();
|
||||
var total = 0f;
|
||||
foreach (var thruster in _allThrusters)
|
||||
{
|
||||
var column = thruster.Column(thruster.NominalThrust);
|
||||
var yaw = column.z * direction;
|
||||
if (!(yaw > 0)) continue;
|
||||
thrusters.Add((length(float2(column.x, column.y)) / yaw, yaw));
|
||||
total += yaw;
|
||||
}
|
||||
if (!(total > 0)) return axis;
|
||||
thrusters.Sort((a, b) => a.ratio.CompareTo(b.ratio));
|
||||
var bands = new List<float>();
|
||||
var bandRatio = float.NaN;
|
||||
foreach (var (ratio, yaw) in thrusters)
|
||||
{
|
||||
if (bands.Count > 0 && abs(ratio - bandRatio) <= .02f * max(bandRatio, 1e-6f)) bands[bands.Count - 1] += yaw;
|
||||
else { bands.Add(yaw); bandRatio = ratio; }
|
||||
}
|
||||
var travel = abs(axis) * bands.Count;
|
||||
var band = min((int)travel, bands.Count - 1);
|
||||
var served = 0f;
|
||||
for (var i = 0; i < band; i++) served += bands[i];
|
||||
served += (travel - band) * bands[band];
|
||||
return direction * min(served / total, 1f);
|
||||
}
|
||||
|
||||
// Called by Entity.Update once the items' performance is current and before the power bus is stepped and the items
|
||||
// execute: liveness, thrust and the throttles they decide are one phase, the one Execute acts on. The lock is a
|
||||
// bound on the allocator's solve; intent reaches the allocator here and nowhere else.
|
||||
|
|
@ -164,22 +127,13 @@ public class Ship : Entity
|
|||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
// No drag below top speed: ships coast and orbits do not decay; drag bleeds off only the excess over the hull's top
|
||||
// speed (aetheria-gravity:ruling:no-drag-below-top-speed). The VelocityLimit clamp still caps it, so the excess is
|
||||
// what this step's forces added; a soft cap, where surfing the beat is a temporary boost, is the ruling's other half.
|
||||
// A docked ship has no flight of its own: its parent owns its pose (Entity.Update).
|
||||
if (Parent == null)
|
||||
{
|
||||
var velocityMagnitude = length(Velocity);
|
||||
var topSpeed = float.PositiveInfinity;
|
||||
foreach (var limit in GetBehaviors<VelocityLimit>()) topSpeed = min(topSpeed, limit.Limit);
|
||||
if (velocityMagnitude > topSpeed)
|
||||
Velocity = normalize(Velocity) * (topSpeed + decay(velocityMagnitude - topSpeed, HullData.Drag * ItemManager.GameplaySettings.DragScale, delta));
|
||||
|
||||
Position.xz += Velocity * delta;
|
||||
|
||||
Velocity += Zone.GetForce(Position.xz) * delta;
|
||||
}
|
||||
var velocityMagnitude = length(Velocity);
|
||||
if(velocityMagnitude > .01f)
|
||||
Velocity = normalize(Velocity) * decay(velocityMagnitude, HullData.Drag, delta);
|
||||
|
||||
Position.xz += Velocity * delta;
|
||||
|
||||
Velocity += Zone.GetForce(Position.xz) * delta;
|
||||
var normal = Zone.GetNormal(Position.xz);
|
||||
var shipRight = Direction.Rotate(ItemRotation.Clockwise);
|
||||
var forward = cross(float3(shipRight.x, 0, shipRight.y), normal);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ public static class ShipModCatalog
|
|||
Exclude(directory, $"hull name '{hull.Name}' collides with an existing catalog item.");
|
||||
if (shipIds.Contains(ship.Id))
|
||||
Exclude(directory, $"ship ID collides with an existing catalog ship authoring record.");
|
||||
if (StationKinds.HullKindFault(cache, hull) is { } kindFault) Exclude(directory, kindFault);
|
||||
foreach (var product in package.Products)
|
||||
{
|
||||
if (!factionKeys.Contains(product.Manufacturer.Key))
|
||||
|
|
|
|||
|
|
@ -14,19 +14,11 @@ public sealed class SimClock
|
|||
// catch-up steps.
|
||||
public const float MaxFrameSeconds = .1f;
|
||||
|
||||
// Game steps per real second at design speed 1, and the sim seconds one step carries. The one owner of the game's
|
||||
// tick: every driver of a game history steps Zone.Update by GameStep. Design speed (ruling sim-speed-surface) will
|
||||
// set Step = DesignSpeed / TickRate in ForGame and nowhere else.
|
||||
public const int TickRate = 30;
|
||||
public const float GameStep = 1f / TickRate;
|
||||
|
||||
public static SimClock ForGame() => new SimClock(GameStep, TickRate);
|
||||
|
||||
private readonly double _period;
|
||||
private double _accumulator;
|
||||
private bool _advancing;
|
||||
|
||||
public SimClock(float step, float stepsPerRealSecond)
|
||||
public SimClock(float step, float stepsPerRealSecond = 60)
|
||||
{
|
||||
if (!(step > 0)) throw new ArgumentOutOfRangeException(nameof(step));
|
||||
if (!(stepsPerRealSecond > 0)) throw new ArgumentOutOfRangeException(nameof(stepsPerRealSecond));
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// What a station is for: one authored catalog record per kind (mining, trade-hub, shipyard...). The catalog's list of
|
||||
// these records is the one list of station kinds. A kind names no hull: the hull names its kind (HullData.Kind), and
|
||||
// which hull a faction builds for a kind is derived from the products that faction makes (StationKinds), so a package can
|
||||
// add a faction's model of a kind without editing any kind record. Records are keyed station-kind:<id>.
|
||||
[CultDocument("aetheria.stationkinddata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class StationKindData
|
||||
{
|
||||
[CultName, JsonProperty("name"), Key(0)]
|
||||
public string Name;
|
||||
|
||||
[JsonProperty("description"), Key(1)]
|
||||
public string Description;
|
||||
|
||||
// A faction with no model of this kind builds the stand-in's hull and the station stays this kind. Ignored once the
|
||||
// faction offers a model of this kind.
|
||||
[JsonProperty("standIns"), Key(2)]
|
||||
public List<StationKindStandIn> StandIns = new List<StationKindStandIn>();
|
||||
|
||||
public static CultRecordKey KeyOf(string id) => new CultRecordKey("station-kind:" + id);
|
||||
}
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class StationKindStandIn
|
||||
{
|
||||
[JsonProperty("faction"), Key(0)]
|
||||
public CultRecordRef<Faction> Faction;
|
||||
|
||||
// The kind whose model this faction builds in place of the owning kind's.
|
||||
[JsonProperty("kind"), Key(1)]
|
||||
public CultRecordRef<StationKindData> Kind;
|
||||
}
|
||||
|
||||
// The one owner of which kinds a faction builds, which kind a station gets, and which hull kind stands in.
|
||||
public static class StationKinds
|
||||
{
|
||||
// The kinds this faction can place: those of the station hulls its products offer, and those that stand in for it.
|
||||
// Ordered by key so a choice never depends on store order. Empty for no faction.
|
||||
public static CultRecordRef<StationKindData>[] KindsOf(CultCache catalog, Faction faction)
|
||||
{
|
||||
if (faction == null) return Array.Empty<CultRecordRef<StationKindData>>();
|
||||
var kinds = new HashSet<string>(OfferedStationHulls(catalog, faction).Select(hull => hull.Kind.Key.Value), StringComparer.Ordinal);
|
||||
foreach (var kind in catalog.GetAll<StationKindData>())
|
||||
if (kind.StandIns.Any(standIn => catalog.Get(standIn.Faction) == faction))
|
||||
kinds.Add(catalog.RefOf(kind).Key.Value);
|
||||
return Ordered(kinds);
|
||||
}
|
||||
|
||||
// The kinds this faction places that a station can be drawn for: those whose hull kind (its own model, else its stand-in's) some
|
||||
// maker offers. A stand-in whose model has not landed leaves its kind out.
|
||||
public static CultRecordRef<StationKindData>[] FieldableKindsOf(CultCache catalog, Faction faction) =>
|
||||
KindsOf(catalog, faction).Where(kind => HasModel(catalog, faction, kind) || LiveStandIn(catalog, faction, kind).IsSet()).ToArray();
|
||||
|
||||
// A station's kind: a stable pick among the faction's kinds it can field, by the station's site, which generation names by the
|
||||
// zone's seed and the station's place in its orbit order (never by a key the cache mints). It draws nothing from any random
|
||||
// stream, so adding the kind to generation moves no other roll. A kind the faction places but cannot field (neither its own
|
||||
// model nor a stand-in's hull kind is carried by any offered station hull) is not in the pool; a faction left with no kind to
|
||||
// field falls back to any kind some offered station hull carries, logged, and the hull draw then falls back to any maker.
|
||||
// Unset when the catalog has none.
|
||||
public static CultRecordRef<StationKindData> Choose(CultCache catalog, Faction faction, string site, Action<string> log = null)
|
||||
{
|
||||
var pool = FieldableKindsOf(catalog, faction);
|
||||
if (pool.Length == 0)
|
||||
{
|
||||
pool = Ordered(OfferedKinds(catalog));
|
||||
if (pool.Length == 0) return default;
|
||||
log?.Invoke($"{faction?.Name ?? "This galaxy"} builds no station kind it can field; placing a station of any kind some maker offers.");
|
||||
}
|
||||
return pool[(int) (site.StableHash() % (uint) pool.Length)];
|
||||
}
|
||||
|
||||
// The kind of hull a faction builds for a station kind: the kind itself when the faction offers a model of it (a model
|
||||
// wins over a stale stand-in), else the kind its stand-in names when some maker offers that, else the kind itself (the hull
|
||||
// draw then falls back to any maker).
|
||||
public static CultRecordRef<StationKindData> HullKindFor(CultCache catalog, Faction faction, CultRecordRef<StationKindData> kind)
|
||||
{
|
||||
if (!kind.IsSet() || HasModel(catalog, faction, kind)) return kind;
|
||||
var standIn = LiveStandIn(catalog, faction, kind);
|
||||
return standIn.IsSet() ? standIn : kind;
|
||||
}
|
||||
|
||||
private static bool HasModel(CultCache catalog, Faction faction, CultRecordRef<StationKindData> kind) =>
|
||||
OfferedStationHulls(catalog, faction).Any(hull => hull.Kind.Key.Equals(kind.Key));
|
||||
|
||||
// The kind the faction's stand-in names for this kind, when some maker offers a hull of it; else unset.
|
||||
private static CultRecordRef<StationKindData> LiveStandIn(CultCache catalog, Faction faction, CultRecordRef<StationKindData> kind)
|
||||
{
|
||||
var standIn = catalog.Get(kind)?.StandIns.FirstOrDefault(entry => catalog.Get(entry.Faction) == faction);
|
||||
return standIn != null && standIn.Kind.IsSet() && OfferedKinds(catalog).Contains(standIn.Kind.Key.Value) ? standIn.Kind : default;
|
||||
}
|
||||
|
||||
// What is wrong with a hull's kind, or null: a Station hull names a kind the catalog holds, and no other hull names one.
|
||||
// The one rule, applied to the catalog on Open and to each mod package on Compose.
|
||||
public static string HullKindFault(CultCache catalog, HullData hull)
|
||||
{
|
||||
if (hull.HullType == HullType.Station)
|
||||
{
|
||||
if (!hull.Kind.IsSet())
|
||||
return $"Station hull '{hull.Name}' has no Kind; a station hull names the station kind it is built to be.";
|
||||
if (catalog.Get(hull.Kind) == null)
|
||||
return $"Station hull '{hull.Name}' Kind names no station kind this catalog holds.";
|
||||
}
|
||||
else if (hull.Kind.IsSet())
|
||||
return $"Hull '{hull.Name}' is a {hull.HullType} and names a Kind; only a station hull has one.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open refuses a malformed kind catalog, naming the record and field. Every Station hull names a kind the catalog holds
|
||||
// and no other hull names one; every stand-in names a faction and a kind the catalog holds, is never its own kind, and
|
||||
// does not stand on another stand-in for the same faction (one hop). A caller that gives kindlessStation is the owner
|
||||
// of the catalog being repaired: a Station hull with no Kind is reported to it by name instead of refused.
|
||||
public static void Validate(CultCache catalog, Action<string> kindlessStation = null)
|
||||
{
|
||||
foreach (var hull in catalog.GetAll<HullData>())
|
||||
{
|
||||
var fault = HullKindFault(catalog, hull);
|
||||
if (fault == null) continue;
|
||||
if (kindlessStation != null && hull.HullType == HullType.Station && !hull.Kind.IsSet()) kindlessStation(fault);
|
||||
else throw new InvalidOperationException(fault);
|
||||
}
|
||||
foreach (var kind in catalog.GetAll<StationKindData>())
|
||||
{
|
||||
var self = catalog.RefOf(kind).Key;
|
||||
for (var i = 0; i < kind.StandIns.Count; i++)
|
||||
{
|
||||
var entry = kind.StandIns[i];
|
||||
var at = $"Station kind '{kind.Name}' StandIns[{i}]";
|
||||
var faction = catalog.Get(entry.Faction);
|
||||
if (faction == null) throw new InvalidOperationException($"{at}.Faction names no faction this catalog holds.");
|
||||
var other = catalog.Get(entry.Kind);
|
||||
if (other == null) throw new InvalidOperationException($"{at}.Kind names no station kind this catalog holds.");
|
||||
if (entry.Kind.Key.Equals(self)) throw new InvalidOperationException($"{at}.Kind is the kind itself; a kind cannot stand in for itself.");
|
||||
if (other.StandIns.Any(next => catalog.Get(next.Faction) == faction))
|
||||
throw new InvalidOperationException($"{at}.Kind stands in for the same faction itself; a stand-in is one hop.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hull kinds some maker offers: the pool a station can always be drawn from.
|
||||
private static HashSet<string> OfferedKinds(CultCache catalog) =>
|
||||
new HashSet<string>(DrawableStationProducts(catalog).Select(entry => entry.hull.Kind.Key.Value), StringComparer.Ordinal);
|
||||
|
||||
private static IEnumerable<HullData> OfferedStationHulls(CultCache catalog, Faction faction) =>
|
||||
DrawableStationProducts(catalog).Where(entry => catalog.Get(entry.product.Manufacturer) == faction).Select(entry => entry.hull);
|
||||
|
||||
// The station hulls a hull draw can reach (LoadoutGenerator.IsDrawable, the one definition), with the product that sells each.
|
||||
private static IEnumerable<(FactionProductData product, HullData hull)> DrawableStationProducts(CultCache catalog) =>
|
||||
catalog.GetAll<FactionProductData>()
|
||||
.Select(product => (product, hull: catalog.Get(product.Design) as HullData))
|
||||
.Where(entry => entry.hull != null && entry.hull.HullType == HullType.Station && entry.hull.Kind.IsSet() &&
|
||||
LoadoutGenerator.IsDrawable(catalog, entry.product, entry.hull));
|
||||
|
||||
private static CultRecordRef<StationKindData>[] Ordered(IEnumerable<string> keys) =>
|
||||
keys.OrderBy(key => key, StringComparer.Ordinal).Select(key => new CultRecordRef<StationKindData>(new CultRecordKey(key))).ToArray();
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7b3e1a94c2d84f6e9a05d1c8e4f27b63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -196,7 +196,7 @@ public class Zone
|
|||
case AsteroidBeltData belt:
|
||||
// Mining Cut 3 (Q16 B): a belt from before field kinds existed gets its kind here, once, by
|
||||
// the rule generation uses, and keeps it.
|
||||
FieldKinds.Ensure(body.Key.Value, belt, cache, GalaxyZone?.Province?.Type ?? default);
|
||||
FieldKinds.Ensure(body.Key, belt, cache);
|
||||
AsteroidBelts[body.Key] = new AsteroidBelt(belt);
|
||||
break;
|
||||
case SunData sun:
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ public abstract class BodyData
|
|||
[JsonProperty("mass"), Key(3)]
|
||||
public float Mass = 0;
|
||||
|
||||
// MessagePack key 4 belonged to the removed stored Resources; do not reuse it. A belt's yield is derived by FieldKinds.Yield.
|
||||
[CultReference(typeof(SimpleCommodityData), many: true), JsonProperty("resources"), Key(4)]
|
||||
public Dictionary<CultRecordRef<SimpleCommodityData>, float> Resources = new Dictionary<CultRecordRef<SimpleCommodityData>, float>();
|
||||
|
||||
[JsonProperty("bodyRadiusMul")] [Key(5)]
|
||||
public float BodyRadiusMultiplier = 1;
|
||||
|
|
@ -113,8 +114,7 @@ public class AsteroidBeltData : BodyData
|
|||
}
|
||||
|
||||
// Mining Cut 3 (docs/mining-cut-refresh.md, Q11): a kind of debris field, an authored catalog record. A chunk's
|
||||
// reflectivity comes from its field's kind; its yields (ore geography, ruling ores-are-regional) are the ores a belt of
|
||||
// this kind holds before a province weights them. FieldKinds.Yield is the one reader.
|
||||
// reflectivity comes from its field's kind; Cut 5 adds the yield table.
|
||||
[CultDocument("aetheria.fieldkinddata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class FieldKindData
|
||||
{
|
||||
|
|
@ -129,308 +129,40 @@ public class FieldKindData
|
|||
// How often generation picks this kind, relative to the others (FieldKinds.Assign). Zero never picks it.
|
||||
[JsonProperty("generationWeight"), Key(2)]
|
||||
public float GenerationWeight = 1;
|
||||
|
||||
// The ores a belt of this kind holds. Empty on a kind from before this key (MessagePack leaves a missing trailing key at its default).
|
||||
[JsonProperty("yields"), Key(3)]
|
||||
public List<FieldYield> Yields = new List<FieldYield>();
|
||||
}
|
||||
|
||||
// One ore a kind of field holds: Abundance of Commodity before the belt's province weights it. A Regional ore is held only
|
||||
// where the province lists it (ruling ores-are-regional: some ores exist only in some provinces); any other entry is held
|
||||
// everywhere, scaled by the province's factor for it.
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class FieldYield
|
||||
{
|
||||
[JsonProperty("commodity"), Key(0)]
|
||||
public CultRecordRef<SimpleCommodityData> Commodity;
|
||||
|
||||
[JsonProperty("abundance"), Key(1)]
|
||||
public float Abundance;
|
||||
|
||||
[JsonProperty("regional"), Key(2)]
|
||||
public bool Regional;
|
||||
}
|
||||
|
||||
// Ore geography (rulings provinces-within-regions, ores-are-regional): a kind of ground, authored. A region deals its
|
||||
// provinces' types from a deck holding GenerationWeight cards of each type (ruling province-types-dealt-per-region);
|
||||
// a province's type weights which kinds its belts draw and which ores a kind holds there.
|
||||
[CultDocument("aetheria.provincedata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class ProvinceData
|
||||
{
|
||||
[CultName, JsonProperty("name"), Key(0)]
|
||||
public string Name = "";
|
||||
|
||||
// The number of cards of this type in a region's deck. Zero is never dealt.
|
||||
[JsonProperty("generationWeight"), Key(1)]
|
||||
public int GenerationWeight = 1;
|
||||
|
||||
// Multiplies the kind's own GenerationWeight in a belt's draw; a kind not listed is multiplied by one.
|
||||
[JsonProperty("kinds"), Key(2)]
|
||||
public List<ProvinceKindWeight> Kinds = new List<ProvinceKindWeight>();
|
||||
|
||||
// Multiplies an ore's Abundance wherever it is held; an ore not listed is multiplied by one, or by zero when the
|
||||
// kind's entry for it is Regional.
|
||||
[JsonProperty("ores"), Key(3)]
|
||||
public List<ProvinceOre> Ores = new List<ProvinceOre>();
|
||||
}
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class ProvinceKindWeight
|
||||
{
|
||||
[JsonProperty("kind"), Key(0)]
|
||||
public CultRecordRef<FieldKindData> Kind;
|
||||
|
||||
[JsonProperty("weight"), Key(1)]
|
||||
public float Weight = 1;
|
||||
}
|
||||
|
||||
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
public class ProvinceOre
|
||||
{
|
||||
[JsonProperty("commodity"), Key(0)]
|
||||
public CultRecordRef<SimpleCommodityData> Commodity;
|
||||
|
||||
[JsonProperty("weight"), Key(1)]
|
||||
public float Weight = 1;
|
||||
}
|
||||
|
||||
// Mining Cut 3 (Q16 B): the one owner of which kind a belt is. Ore geography: and of what it yields.
|
||||
// Mining Cut 3 (Q16 B): the one owner of which kind a belt is.
|
||||
public static class FieldKinds
|
||||
{
|
||||
// The one weighted pick: an index into weights, chosen by a stable hash of the site (the same site and the same
|
||||
// weights always give the same index), never one whose weight is not positive. -1 when no weight is positive.
|
||||
internal static int Pick(string site, double[] weights)
|
||||
{
|
||||
var pick = site.StableHash() / 4294967296.0 * weights.Sum();
|
||||
var last = -1;
|
||||
for (var i = 0; i < weights.Length; i++)
|
||||
{
|
||||
if (weights[i] <= 0) continue;
|
||||
last = i;
|
||||
if (pick < weights[i]) return i;
|
||||
pick -= weights[i];
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
// A weighted pick among the catalog's kinds, by a stable hash of the belt's site: the same site, catalog and province
|
||||
// type always give the same kind. A kind's weight is its GenerationWeight times its province's factor for it; a belt with
|
||||
// no province type (unset, or naming nothing the catalog holds) draws unweighted. Generation names a belt by the zone's
|
||||
// seed and the belt's place in generation order (never by the key the cache mints, which differs every run); a belt saved
|
||||
// without a kind is named by its key. Kinds are ordered by key so the pick does not depend on store order.
|
||||
// A weighted pick among the catalog's kinds, by a stable hash of the belt's key: the same belt and the same
|
||||
// catalog always give the same kind. Kinds are ordered by key so the pick does not depend on store order.
|
||||
// Unset when the catalog has no kind with positive weight.
|
||||
public static CultRecordRef<FieldKindData> Assign(string site, CultCache catalog, CultRecordRef<ProvinceData> province)
|
||||
public static CultRecordRef<FieldKindData> Assign(CultRecordKey field, CultCache catalog)
|
||||
{
|
||||
var type = catalog.Get(province);
|
||||
var kinds = catalog.GetAll<FieldKindData>()
|
||||
.Select(kind =>
|
||||
{
|
||||
var key = catalog.RefOf(kind).Key;
|
||||
var factor = Factor(type?.Kinds.Where(entry => entry.Kind.Key.Equals(key)).Select(entry => (double) entry.Weight), 1.0);
|
||||
// Both must be positive: two negatives do not multiply into a weight.
|
||||
return (Key: key, Weight: kind.GenerationWeight > 0f && factor > 0.0 ? kind.GenerationWeight * factor : 0.0);
|
||||
})
|
||||
.Where(kind => kind.Weight > 0.0)
|
||||
.Where(kind => kind.GenerationWeight > 0f)
|
||||
.Select(kind => (Key: catalog.RefOf(kind).Key, Weight: kind.GenerationWeight))
|
||||
.OrderBy(kind => kind.Key.Value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (kinds.Length == 0) return default;
|
||||
|
||||
return new CultRecordRef<FieldKindData>(kinds[Pick(site, kinds.Select(kind => kind.Weight).ToArray())].Key);
|
||||
}
|
||||
|
||||
// A province's factor for one entry: the sum of its listings, or the default when it lists none.
|
||||
private static double Factor(IEnumerable<double> listed, double unlisted)
|
||||
{
|
||||
var weights = listed?.ToArray();
|
||||
return weights == null || weights.Length == 0 ? unlisted : weights.Sum();
|
||||
var pick = field.Value.StableHash() / 4294967296.0 * kinds.Sum(kind => (double) kind.Weight);
|
||||
foreach (var kind in kinds)
|
||||
{
|
||||
if (pick < kind.Weight) return new CultRecordRef<FieldKindData>(kind.Key);
|
||||
pick -= kind.Weight;
|
||||
}
|
||||
return new CultRecordRef<FieldKindData>(kinds[kinds.Length - 1].Key);
|
||||
}
|
||||
|
||||
// Gives a belt with no kind its kind and saves it; a belt that has one keeps it, whatever the catalog now says.
|
||||
// Generation (ZoneGenerator) and first load (Zone) both call this, so there is one assignment rule.
|
||||
public static void Ensure(string site, AsteroidBeltData belt, CultCache cache, CultRecordRef<ProvinceData> province)
|
||||
public static void Ensure(CultRecordKey field, AsteroidBeltData belt, CultCache cache)
|
||||
{
|
||||
if (belt.Kind.IsSet()) return;
|
||||
belt.Kind = Assign(site, cache, province);
|
||||
belt.Kind = Assign(field, cache);
|
||||
if (belt.Kind.IsSet()) cache.Upsert(belt);
|
||||
}
|
||||
|
||||
// What a belt of this kind holds in a zone of this province: each of the kind's yields, its Abundance times the
|
||||
// province's factor for the ore (unlisted: one, or zero when the yield is Regional), summed per commodity, in commodity
|
||||
// key order, zero amounts dropped. Derived on every call, never stored. Empty for an unset kind.
|
||||
public static (CultRecordRef<SimpleCommodityData> Commodity, float Amount)[] Yield(CultRecordRef<FieldKindData> kind,
|
||||
CultRecordRef<ProvinceData> province, CultCache catalog)
|
||||
{
|
||||
var data = catalog.Get(kind);
|
||||
if (data == null) return Array.Empty<(CultRecordRef<SimpleCommodityData>, float)>();
|
||||
var type = catalog.Get(province);
|
||||
return data.Yields
|
||||
.Select(yield => (yield.Commodity, Amount: yield.Abundance * Factor(
|
||||
type?.Ores.Where(ore => ore.Commodity.Key.Equals(yield.Commodity.Key)).Select(ore => (double) ore.Weight), yield.Regional ? 0.0 : 1.0)))
|
||||
.GroupBy(yield => yield.Commodity.Key.Value, StringComparer.Ordinal)
|
||||
.OrderBy(group => group.Key, StringComparer.Ordinal)
|
||||
.Select(group => (group.First().Commodity, Amount: (float) group.Sum(yield => yield.Amount)))
|
||||
.Where(yield => yield.Amount > 0f)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Ore geography (rulings provinces-within-regions, boss-zone-is-near-side, province-partition-by-size, province-types-dealt-per-region).
|
||||
// Pure functions of the zone graph, the run seed and the catalog: nothing here draws from Galaxy's Random stream, stores membership
|
||||
// or reads who owns what. Galaxy builds provinces once, in its generation and save constructors, and nothing re-partitions them.
|
||||
public static class Provinces
|
||||
{
|
||||
// Zones per province when a settings object does not say (SectorGenerationSettings.ProvinceZones): calibration, not a rule.
|
||||
public const int DefaultZonesPerProvince = 8;
|
||||
|
||||
// A boss zone sits on the near side of its border: its crossing links are its links to zones in the component of exit in
|
||||
// the graph without it. The one definition, read by Regions (which cuts them) and ExtraExits (which prunes all but one).
|
||||
// exit unset (-1) gives none, and a boss zone that is the exit has none. Ordered by boss, then far zone.
|
||||
public static (int Boss, int Far)[] CrossingLinks(int[][] adjacency, IReadOnlyCollection<int> bossZones, int exit)
|
||||
{
|
||||
var links = new List<(int Boss, int Far)>();
|
||||
if (exit >= 0)
|
||||
foreach (var boss in bossZones.Distinct().Where(boss => boss != exit).OrderBy(boss => boss))
|
||||
{
|
||||
var exitSide = Flood(adjacency, exit, (a, b) => b != boss);
|
||||
foreach (var next in adjacency[boss].OrderBy(next => next))
|
||||
if (exitSide[next]) links.Add((boss, next));
|
||||
}
|
||||
return links.ToArray();
|
||||
}
|
||||
|
||||
// The crossing links to prune so each boss zone keeps one exit: all of them but the link to the boss zone's successor on
|
||||
// exitPath (the zones from the entrance to the exit), which is the link the run goes through. The generator's exit path
|
||||
// passes through every boss zone by one of its crossing links; a boss zone off exitPath, last on it, or followed by a zone
|
||||
// it has no crossing link to breaks that, and throws naming the boss zone rather than guessing which link to keep.
|
||||
public static (int Boss, int Far)[] ExtraExits(int[][] adjacency, IReadOnlyCollection<int> bossZones, int exit, int[] exitPath)
|
||||
{
|
||||
var extra = new List<(int Boss, int Far)>();
|
||||
foreach (var links in CrossingLinks(adjacency, bossZones, exit).GroupBy(link => link.Boss))
|
||||
{
|
||||
var at = Array.IndexOf(exitPath, links.Key);
|
||||
var kept = at >= 0 && at + 1 < exitPath.Length ? exitPath[at + 1] : -1;
|
||||
if (!links.Any(link => link.Far == kept))
|
||||
throw new InvalidOperationException($"boss zone {links.Key} has no crossing link to its successor on the exit path, so no exit to keep");
|
||||
extra.AddRange(links.Where(link => link.Far != kept));
|
||||
}
|
||||
return extra.ToArray();
|
||||
}
|
||||
|
||||
// Each zone's region, named by the region's least zone index. Cutting every boss zone's crossing links (CrossingLinks)
|
||||
// leaves the regions as the connected components. Every zone, boss zones included, is in exactly one.
|
||||
public static int[] Regions(int[][] adjacency, IReadOnlyCollection<int> bossZones, int exit)
|
||||
{
|
||||
var cut = CrossingLinks(adjacency, bossZones, exit).Select(link => (Math.Min(link.Boss, link.Far), Math.Max(link.Boss, link.Far))).ToHashSet();
|
||||
|
||||
var regionOf = new int[adjacency.Length];
|
||||
Array.Fill(regionOf, -1);
|
||||
for (var start = 0; start < adjacency.Length; start++)
|
||||
{
|
||||
if (regionOf[start] >= 0) continue;
|
||||
var members = Flood(adjacency, start, (a, b) => !cut.Contains((Math.Min(a, b), Math.Max(a, b))));
|
||||
for (var i = 0; i < members.Length; i++) if (members[i]) regionOf[i] = start;
|
||||
}
|
||||
return regionOf;
|
||||
}
|
||||
|
||||
// Each zone's province, named by the province's seed zone index. A region of size s takes max(1, round(s / zonesPerProvince))
|
||||
// seeds: the first is the zone with the least hash of the run seed, each next the zone farthest (in-region hops) from the
|
||||
// seeds chosen so far, ties by that hash; then every zone joins the nearest seed, ties to the earlier seed. A province is
|
||||
// contiguous and never crosses a region.
|
||||
public static int[] Partition(int[][] adjacency, int[] regionOf, uint runSeed, int zonesPerProvince)
|
||||
{
|
||||
var seedOf = new int[adjacency.Length];
|
||||
uint Hash(int index) => $"{runSeed}:province-seed:{index}".StableHash();
|
||||
foreach (var region in Enumerable.Range(0, adjacency.Length).GroupBy(zone => regionOf[zone]).OrderBy(group => group.Key))
|
||||
{
|
||||
var zones = region.ToArray();
|
||||
var count = Math.Min(zones.Length, Math.Max(1, (int) Math.Round((double) zones.Length / Math.Max(1, zonesPerProvince), MidpointRounding.AwayFromZero)));
|
||||
var seeds = new List<int> { zones.OrderBy(Hash).ThenBy(zone => zone).First() };
|
||||
while (seeds.Count < count)
|
||||
{
|
||||
var hops = Grow(adjacency, regionOf, seeds.ToArray()).Hops;
|
||||
seeds.Add(zones.Where(zone => !seeds.Contains(zone)).OrderByDescending(zone => hops[zone]).ThenBy(Hash).ThenBy(zone => zone).First());
|
||||
}
|
||||
var owner = Grow(adjacency, regionOf, seeds.ToArray()).Owner;
|
||||
foreach (var zone in zones) seedOf[zone] = seeds[owner[zone]];
|
||||
}
|
||||
return seedOf;
|
||||
}
|
||||
|
||||
// A region's hand of province types, one per province in ascending seed-zone order. Types are dealt without replacement from
|
||||
// a deck of GenerationWeight cards per type (catalog types in key order), each draw one weighted pick over the cards remaining
|
||||
// at a site named by the run seed, the region and the draw's place; the deck is restored whole when it runs out. An empty
|
||||
// deck deals every province an unset type.
|
||||
public static CultRecordRef<ProvinceData>[] Deal(int regionKey, int seedCount, uint runSeed, CultCache catalog)
|
||||
{
|
||||
var types = catalog.GetAll<ProvinceData>()
|
||||
.Where(type => type.GenerationWeight > 0)
|
||||
.Select(type => (Ref: catalog.RefOf(type), Cards: type.GenerationWeight))
|
||||
.OrderBy(type => type.Ref.Key.Value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var hand = new CultRecordRef<ProvinceData>[seedCount];
|
||||
if (types.Length == 0) return hand;
|
||||
|
||||
var remaining = types.Select(type => type.Cards).ToArray();
|
||||
for (var n = 0; n < seedCount; n++)
|
||||
{
|
||||
if (remaining.Sum() == 0) remaining = types.Select(type => type.Cards).ToArray();
|
||||
var card = FieldKinds.Pick($"{runSeed}:province-deal:{regionKey}:{n}", remaining.Select(cards => (double) cards).ToArray());
|
||||
remaining[card]--;
|
||||
hand[n] = types[card].Ref;
|
||||
}
|
||||
return hand;
|
||||
}
|
||||
|
||||
// Every zone reachable from start over the links the filter allows.
|
||||
private static bool[] Flood(int[][] adjacency, int start, Func<int, int, bool> allowed)
|
||||
{
|
||||
var reached = new bool[adjacency.Length];
|
||||
var stack = new Stack<int>();
|
||||
reached[start] = true;
|
||||
stack.Push(start);
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var zone = stack.Pop();
|
||||
foreach (var next in adjacency[zone])
|
||||
if (!reached[next] && allowed(zone, next))
|
||||
{
|
||||
reached[next] = true;
|
||||
stack.Push(next);
|
||||
}
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
|
||||
// Breadth-first growth from seeds inside their regions, level by level: each zone's hops to the nearest seed and the rank
|
||||
// (place in seeds) of the earliest seed among those that reach it first.
|
||||
private static (int[] Owner, int[] Hops) Grow(int[][] adjacency, int[] regionOf, int[] seeds)
|
||||
{
|
||||
var owner = new int[adjacency.Length];
|
||||
var hops = new int[adjacency.Length];
|
||||
Array.Fill(owner, -1);
|
||||
for (var rank = 0; rank < seeds.Length; rank++)
|
||||
{
|
||||
owner[seeds[rank]] = rank;
|
||||
hops[seeds[rank]] = 0;
|
||||
}
|
||||
var frontier = seeds.ToList();
|
||||
for (var level = 1; frontier.Count > 0; level++)
|
||||
{
|
||||
var claims = new Dictionary<int, int>();
|
||||
foreach (var zone in frontier)
|
||||
foreach (var next in adjacency[zone])
|
||||
if (owner[next] < 0 && regionOf[next] == regionOf[zone])
|
||||
claims[next] = claims.TryGetValue(next, out var rank) ? Math.Min(rank, owner[zone]) : owner[zone];
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
owner[claim.Key] = claim.Value;
|
||||
hops[claim.Key] = level;
|
||||
}
|
||||
frontier = claims.Keys.OrderBy(zone => zone).ToList();
|
||||
}
|
||||
return (owner, hops);
|
||||
}
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.gasgiantdata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ public static class ZoneGenerator
|
|||
pack.Orbits = planets.Select(planet => cache.RefOf(orbitMap[planet])).ToList();
|
||||
|
||||
var beltOf = new Dictionary<GeneratorPlanet, AsteroidBeltData>();
|
||||
pack.Planets = planets.Where(p=>!p.Empty).Select((planet, index) =>
|
||||
pack.Planets = planets.Where(p=>!p.Empty).Select(planet =>
|
||||
{
|
||||
BodyType bodyType = planet.Belt ? BodyType.Asteroid :
|
||||
planet.Mass > zoneSettings.SunMass ? BodyType.Sun :
|
||||
|
|
@ -213,7 +213,7 @@ public static class ZoneGenerator
|
|||
var bodyKey = cache.Upsert(planetData).Key;
|
||||
planetData.Name = bodyKey.Value.Substring(0, 8);
|
||||
// Mining Cut 3 (Q16 B): a belt's field kind is chosen here, at generation, and saved with it.
|
||||
if (planetData is AsteroidBeltData generatedBelt) FieldKinds.Ensure($"{seed}:belt:{index}", generatedBelt, cache, galaxyZone.Province?.Type ?? default);
|
||||
if (planetData is AsteroidBeltData generatedBelt) FieldKinds.Ensure(bodyKey, generatedBelt, cache);
|
||||
return cache.Upsert(planetData);
|
||||
}).ToList();
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ public static class ZoneGenerator
|
|||
BodyData swarmBody = swarm;
|
||||
var swarmKey = cache.Upsert(swarmBody).Key;
|
||||
swarm.Name = swarmKey.Value.Substring(0, 8);
|
||||
FieldKinds.Ensure($"{seed}:swarm:{swarmHosts.Count}", swarm, cache, galaxyZone.Province?.Type ?? default);
|
||||
FieldKinds.Ensure(swarmKey, swarm, cache);
|
||||
pack.Planets.Add(cache.Upsert(swarmBody));
|
||||
}
|
||||
|
||||
|
|
@ -268,7 +268,7 @@ public static class ZoneGenerator
|
|||
BodyData lanedBody = laned;
|
||||
var lanedKey = cache.Upsert(lanedBody).Key;
|
||||
laned.Name = lanedKey.Value.Substring(0, 8);
|
||||
FieldKinds.Ensure($"{seed}:lane:{shaped.Count}", laned, cache, galaxyZone.Province?.Type ?? default);
|
||||
FieldKinds.Ensure(lanedKey, laned, cache);
|
||||
pack.Planets.Add(cache.Upsert(lanedBody));
|
||||
}
|
||||
|
||||
|
|
@ -419,8 +419,7 @@ public static class ZoneGenerator
|
|||
if (i >= selectedStationOrbits.Length) { itemManager.Log($"Story station for {story.Faction?.Name} has no rail clear of the bodies; its story has nowhere to live!"); continue; }
|
||||
var orbit = selectedStationOrbits[i];
|
||||
var lagrangeOrbit = CreateLagrangeOrbit(orbit);
|
||||
var station = GetLoadoutGenerator(story.Faction).GenerateStationLoadout(
|
||||
StationKinds.Choose(cache, story.Faction, $"{seed}:station:{i}", itemManager.Log));
|
||||
var station = GetLoadoutGenerator(story.Faction).GenerateStationLoadout();
|
||||
if (station == null)
|
||||
{
|
||||
itemManager.Log($"Story station for {story.Faction?.Name} could not be generated; its story has nowhere to live!");
|
||||
|
|
@ -441,8 +440,7 @@ public static class ZoneGenerator
|
|||
var security = (SecurityLevel) ((int) ((1f - pow(random.NextFloat(), factionPresence / 2f)) * 3f));
|
||||
|
||||
var lagrangeOrbit = CreateLagrangeOrbit(orbit);
|
||||
var station = GetLoadoutGenerator(nearestFaction).GenerateStationLoadout(
|
||||
StationKinds.Choose(cache, nearestFaction, $"{seed}:station:{i}", itemManager.Log));
|
||||
var station = GetLoadoutGenerator(nearestFaction).GenerateStationLoadout();
|
||||
if (station == null)
|
||||
{
|
||||
if (isTutorialEntrance)
|
||||
|
|
@ -522,6 +520,12 @@ public static class ZoneGenerator
|
|||
public static Faction[] EligibleWandererFactions(Faction[] factions, Faction owner, Faction nearest) =>
|
||||
factions.Where(f => f != owner && f != nearest).ToArray();
|
||||
|
||||
// static float ResourceValue(ref Random random, ZoneGenerationSettings settings, SimpleCommodityData resource, float density)
|
||||
// {
|
||||
// return random.NextPowerDistribution(resource.Minimum, resource.Maximum, resource.Exponent,
|
||||
// 1 / lerp(settings.ResourceDensityMinimum, settings.ResourceDensityMaximum, density));
|
||||
// }
|
||||
|
||||
public static GeneratorPlanet[] GenerateEntities(ZoneGenerationSettings settings, PlanetSettings planetSettings, ref Random random, float mass, float radius, float2 fixedPosition,
|
||||
float rootReach)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public class MainMenu : MonoBehaviour
|
|||
public float FadeAlphaExponent = 2;
|
||||
public float FadePositionExponent = 2;
|
||||
|
||||
private (PropertiesPanel panel, CanvasGroup group, PropertiesPanel detail) _currentMenu, _nextMenu;
|
||||
private (PropertiesPanel panel, CanvasGroup group) _currentMenu, _nextMenu;
|
||||
private bool _fadeFromRight;
|
||||
private float _fadeLerp;
|
||||
private bool _fading;
|
||||
|
|
@ -38,14 +38,10 @@ public class MainMenu : MonoBehaviour
|
|||
_panelPosition = PanelPrototype.transform.position;
|
||||
|
||||
var panel1 = PanelPrototype.Instantiate<PropertiesPanel>();
|
||||
var detail1 = PanelPrototype.Instantiate<PropertiesPanel>();
|
||||
ScenarioPicker.Dock(detail1, panel1);
|
||||
_currentMenu = (panel1, panel1.GetComponent<CanvasGroup>(), detail1);
|
||||
_currentMenu = (panel1, panel1.GetComponent<CanvasGroup>());
|
||||
|
||||
var panel2 = PanelPrototype.Instantiate<PropertiesPanel>();
|
||||
var detail2 = PanelPrototype.Instantiate<PropertiesPanel>();
|
||||
ScenarioPicker.Dock(detail2, panel2);
|
||||
_nextMenu = (panel2, panel2.GetComponent<CanvasGroup>(), detail2);
|
||||
_nextMenu = (panel2, panel2.GetComponent<CanvasGroup>());
|
||||
|
||||
_currentMenu.panel.gameObject.SetActive(false);
|
||||
//_saveDirectory = ActionGameManager.GameDataDirectory.CreateSubdirectory("Saves");
|
||||
|
|
@ -78,7 +74,7 @@ public class MainMenu : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
public static string TitleSubtitle(string title, string subtitle) => $"{title}\n<smallcaps><size=50%>{subtitle}";
|
||||
private string TitleSubtitle(string title, string subtitle) => $"{title}\n<smallcaps><size=50%>{subtitle}";
|
||||
|
||||
private void Fade(bool fromRight)
|
||||
{
|
||||
|
|
@ -91,18 +87,10 @@ public class MainMenu : MonoBehaviour
|
|||
|
||||
private static bool _exclusionsShown;
|
||||
|
||||
// Starts the next screen on an empty panel with a title and its scenario detail pane hidden and empty.
|
||||
private void Next(string title)
|
||||
{
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = title;
|
||||
_nextMenu.detail.Clear();
|
||||
_nextMenu.detail.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void ShowMain()
|
||||
{
|
||||
Next(TitleSubtitle("aetheria", "terminus"));
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus");
|
||||
if (ActionGameManager.CurrentGalaxy == null)
|
||||
{
|
||||
// No run is live: at the title, after death, or after a new run failed to start. A saved run exists only as
|
||||
|
|
@ -143,18 +131,31 @@ public class MainMenu : MonoBehaviour
|
|||
// to set up a run and the test arenas are listed only in editor and development builds.
|
||||
private void ShowScenarios()
|
||||
{
|
||||
Next(TitleSubtitle("new game", "scenarios"));
|
||||
var groups = new List<(string label, Scenario[] scenarios)> { ("modes", Scenarios.Modes) };
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("new game", "scenarios");
|
||||
AddScenarios(Scenarios.Modes);
|
||||
if (Debug.isDebugBuild)
|
||||
{
|
||||
groups.Add(("development", Scenarios.Development));
|
||||
groups.Add(("test arenas", Scenarios.Tests));
|
||||
_nextMenu.panel.AddSection("development");
|
||||
AddScenarios(Scenarios.Development);
|
||||
_nextMenu.panel.AddSection("test arenas");
|
||||
AddScenarios(Scenarios.Tests);
|
||||
}
|
||||
ScenarioPicker.Show(_nextMenu.panel, _nextMenu.detail, groups, Launch, () =>
|
||||
_nextMenu.panel.AddButton("Back",
|
||||
() =>
|
||||
{
|
||||
ShowMain();
|
||||
Fade(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void AddScenarios(IEnumerable<Scenario> scenarios)
|
||||
{
|
||||
foreach (var scenario in scenarios)
|
||||
{
|
||||
ShowMain();
|
||||
Fade(false);
|
||||
}, Dialog.Confirm);
|
||||
_nextMenu.panel.AddButton(scenario.Name, () => Launch(scenario));
|
||||
_nextMenu.panel.AddProperty(() => scenario.Brief);
|
||||
}
|
||||
}
|
||||
|
||||
// The only way into a new run: generates the scenario's galaxy off the main thread and enters the game, where
|
||||
|
|
@ -262,7 +263,8 @@ public class MainMenu : MonoBehaviour
|
|||
|
||||
private void ShowSettings()
|
||||
{
|
||||
Next("settings");
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = "settings";
|
||||
_nextMenu.panel.AddButton("Gameplay",
|
||||
() =>
|
||||
{
|
||||
|
|
@ -297,7 +299,8 @@ public class MainMenu : MonoBehaviour
|
|||
|
||||
private void ShowGameplaySettings()
|
||||
{
|
||||
Next(TitleSubtitle("gameplay", "settings"));
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("gameplay", "settings");
|
||||
_nextMenu.panel.AddField("Name",
|
||||
() => ActionGameManager.PlayerSettings.Name,
|
||||
s => ActionGameManager.PlayerSettings.Name = s);
|
||||
|
|
@ -319,7 +322,8 @@ public class MainMenu : MonoBehaviour
|
|||
|
||||
private void ShowGraphicsSettings()
|
||||
{
|
||||
Next(TitleSubtitle("graphics", "settings"));
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("graphics", "settings");
|
||||
_nextMenu.panel.AddField("Nebula Quality",
|
||||
() => (int)ActionGameManager.PlayerSettings.GraphicsSettings.NebulaQuality,
|
||||
i =>
|
||||
|
|
@ -343,7 +347,8 @@ public class MainMenu : MonoBehaviour
|
|||
|
||||
private void ShowInputSettings()
|
||||
{
|
||||
Next(TitleSubtitle("input", "settings"));
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("input", "settings");
|
||||
_nextMenu.panel.AddButton("Back",
|
||||
() =>
|
||||
{
|
||||
|
|
@ -354,7 +359,8 @@ public class MainMenu : MonoBehaviour
|
|||
|
||||
private void ShowAudioSettings()
|
||||
{
|
||||
Next(TitleSubtitle("audio", "settings"));
|
||||
_nextMenu.panel.Clear();
|
||||
_nextMenu.panel.Title.text = TitleSubtitle("audio", "settings");
|
||||
_nextMenu.panel.AddButton("Back",
|
||||
() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,289 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UniRx;
|
||||
using UniRx.Triggers;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem.UI;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// The New Game screen: scenario names on the left pane by group, the selected scenario's brief, seed and ambient on a
|
||||
// docked right pane, one Launch. This class owns which scenario is selected (one index into the rows it built);
|
||||
// everything the screen shows about a scenario, and what Launch and Enter launch, reads that index. Keys arrive only as
|
||||
// Input System UI events on the focused Launch button, so Enter and a mouse click are the same Button.onClick.
|
||||
public sealed class ScenarioPicker
|
||||
{
|
||||
private readonly List<Scenario> _scenarios = new List<Scenario>();
|
||||
private readonly List<string> _groupLabels = new List<string>();
|
||||
private readonly List<Button> _rows = new List<Button>();
|
||||
private readonly List<ColorBlock> _rowColors = new List<ColorBlock>();
|
||||
private readonly List<Color> _rowFills = new List<Color>();
|
||||
private readonly PropertiesPanel _list;
|
||||
private readonly PropertiesPanel _detail;
|
||||
private readonly Action<Scenario> _launch;
|
||||
private readonly Action _back;
|
||||
private Button _launchButton;
|
||||
private ScrollRect _scroll;
|
||||
private UnityAction<Vector2> _fit;
|
||||
private static readonly Color Accent = new Color(1, .4458f, .05f);
|
||||
private int _selected;
|
||||
private bool _launched;
|
||||
private bool _restoreDeselect;
|
||||
|
||||
private ScenarioPicker(PropertiesPanel list, PropertiesPanel detail, Action<Scenario> launch, Action back)
|
||||
{
|
||||
_list = list;
|
||||
_detail = detail;
|
||||
_launch = launch;
|
||||
_back = back;
|
||||
}
|
||||
|
||||
// The scenario the detail pane describes and Launch will launch.
|
||||
public Scenario Selected => _scenarios[_selected];
|
||||
|
||||
// Parks the detail pane beside the list pane: a child of it, outside its layout, hidden until Show.
|
||||
public static void Dock(PropertiesPanel detail, PropertiesPanel list, float width = 480, float gap = 128)
|
||||
{
|
||||
var rect = (RectTransform) detail.transform;
|
||||
rect.SetParent(list.transform, false);
|
||||
detail.gameObject.AddComponent<LayoutElement>().ignoreLayout = true;
|
||||
rect.anchorMin = new Vector2(1, 0);
|
||||
rect.anchorMax = new Vector2(1, 1);
|
||||
rect.pivot = new Vector2(0, .5f);
|
||||
rect.anchoredPosition = new Vector2(gap, 0);
|
||||
rect.sizeDelta = new Vector2(width, 0);
|
||||
detail.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private static readonly Color Muted = new Color(1, 1, 1, .55f);
|
||||
|
||||
// Builds both panes for the groups, shows the detail pane, selects the first scenario and focuses Launch. The
|
||||
// launchChrome is a button of the menu's own styling, cloned for Launch.
|
||||
public static ScenarioPicker Show(PropertiesPanel list, PropertiesPanel detail,
|
||||
IReadOnlyList<(string label, Scenario[] scenarios)> groups, Action<Scenario> launch, Action back, Button launchChrome)
|
||||
{
|
||||
var picker = new ScenarioPicker(list, detail, launch, back);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (groups.Count > 1)
|
||||
{
|
||||
if (picker._scenarios.Count > 0) list.AddSpacer();
|
||||
var header = list.AddSection(group.label);
|
||||
header.GetComponent<LayoutElement>().preferredHeight = 26;
|
||||
var text = header.GetComponentInChildren<TMP_Text>();
|
||||
text.fontSize = 14;
|
||||
text.fontStyle = FontStyles.SmallCaps;
|
||||
text.alignment = TextAlignmentOptions.Left;
|
||||
text.color = Muted;
|
||||
}
|
||||
foreach (var scenario in group.scenarios)
|
||||
{
|
||||
var index = picker._scenarios.Count;
|
||||
picker._scenarios.Add(scenario);
|
||||
picker._groupLabels.Add(group.label);
|
||||
var row = list.AddButton(scenario.Name, () => picker.Select(index));
|
||||
picker.Track(row.Button);
|
||||
picker._rows.Add(row.Button);
|
||||
picker._rowColors.Add(row.Button.colors);
|
||||
picker._rowFills.Add(row.Button.targetGraphic.color);
|
||||
}
|
||||
}
|
||||
picker.Track(list.AddButton("Back", picker.Back).Button);
|
||||
|
||||
detail.Clear();
|
||||
detail.gameObject.SetActive(true);
|
||||
// The pane's own title is an invisible copy of the page title, so both panes stack their content from the same
|
||||
// line; the scenario is headed by its group label and name in the body.
|
||||
detail.Title.text = list.Title.text;
|
||||
detail.Title.color = Color.clear;
|
||||
Style(detail.AddProperty(() => $"<smallcaps>{picker._groupLabels[picker._selected]}"), 14, Muted);
|
||||
Style(detail.AddProperty(() => picker.Selected.Name), 30, Color.white);
|
||||
detail.AddProperty(() => $"<margin-right=20%>{picker.Selected.Brief}");
|
||||
detail.AddSpacer();
|
||||
detail.AddProperty(() => $"<color=#ffffff8c>Seed</color><pos=22%>{(picker.Selected.Seed == 0 ? "new each launch" : picker.Selected.Seed.ToString())}");
|
||||
detail.AddProperty(() => $"<color=#ffffff8c>Ambient</color><pos=22%>{(picker.Selected.Ambient ? "on" : "off")}");
|
||||
detail.AddSpacer();
|
||||
picker._launchButton = AddLaunch(detail, launchChrome, picker.Activate);
|
||||
picker.Track(picker._launchButton);
|
||||
picker._launchButton.OnMoveAsObservable().Subscribe(move =>
|
||||
{
|
||||
if (move.moveDir == MoveDirection.Up) picker.Step(-1);
|
||||
else if (move.moveDir == MoveDirection.Down) picker.Step(1);
|
||||
});
|
||||
picker._launchButton.OnCancelAsObservable().Subscribe(_ => picker.Back());
|
||||
|
||||
picker.StyleScrollbars();
|
||||
picker.HoldFocus();
|
||||
picker.Select(0);
|
||||
// Layout is not live until the frame after Show; edit mode has no frames.
|
||||
if (Application.isPlaying) Observable.NextFrame().Subscribe(_ => picker.FitViewport());
|
||||
return picker;
|
||||
}
|
||||
|
||||
private static void Style(Property property, float size, Color color)
|
||||
{
|
||||
property.Label.enableAutoSizing = false;
|
||||
property.Label.fontSize = size;
|
||||
property.Label.color = color;
|
||||
}
|
||||
|
||||
// Launch is a clone of the menu's own chrome button, filled with the accent so it reads as the default action. The
|
||||
// clone lives under a spacer row, so the pane's Clear destroys it.
|
||||
private static Button AddLaunch(PropertiesPanel detail, Button chrome, Action onClick)
|
||||
{
|
||||
var holder = detail.AddSpacer();
|
||||
holder.GetComponent<LayoutElement>().preferredHeight = 40;
|
||||
var button = UnityEngine.Object.Instantiate(chrome, holder, false);
|
||||
var rect = (RectTransform) button.transform;
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = new Vector2(0, 1);
|
||||
rect.pivot = new Vector2(0, .5f);
|
||||
rect.anchoredPosition = Vector2.zero;
|
||||
rect.sizeDelta = new Vector2(220, 0);
|
||||
// The visible chrome is the button's Fill child, which the button's own layout group sizes to its text and
|
||||
// centres; with the group off, Fill stretches to the button.
|
||||
button.GetComponent<LayoutGroup>().enabled = false;
|
||||
var fill = button.targetGraphic.rectTransform;
|
||||
fill.anchorMin = Vector2.zero;
|
||||
fill.anchorMax = Vector2.one;
|
||||
fill.offsetMin = fill.offsetMax = Vector2.zero;
|
||||
button.gameObject.SetActive(true);
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => onClick());
|
||||
button.targetGraphic.color = Accent;
|
||||
button.GetComponentInChildren<TMP_Text>().text = "Launch";
|
||||
return button;
|
||||
}
|
||||
|
||||
// The detail pane needs no scrollbar; the list's gets a visible track, so its handle reads as a scrollbar rather
|
||||
// than a bar that stops partway.
|
||||
private void StyleScrollbars()
|
||||
{
|
||||
var detailScroll = ScrollOf(_detail);
|
||||
if (detailScroll != null && detailScroll.verticalScrollbar != null) detailScroll.verticalScrollbar.gameObject.SetActive(false);
|
||||
_scroll = ScrollOf(_list);
|
||||
if (_scroll == null) return;
|
||||
if (_scroll.verticalScrollbar != null)
|
||||
{
|
||||
if (!_scroll.verticalScrollbar.TryGetComponent<Image>(out var track)) track = _scroll.verticalScrollbar.gameObject.AddComponent<Image>();
|
||||
track.color = new Color(Accent.r, Accent.g, Accent.b, .2f);
|
||||
// A click on the bar selects it and takes focus off Launch, which would silence the arrows.
|
||||
_scroll.verticalScrollbar.navigation = new Navigation { mode = Navigation.Mode.None };
|
||||
}
|
||||
_scroll.onValueChanged.RemoveAllListeners();
|
||||
_fit = _ => FitViewport();
|
||||
_scroll.onValueChanged.AddListener(_fit);
|
||||
}
|
||||
|
||||
// The panel is inactive until the menu shows it, which is after Show builds it; an inactive ScrollRect still counts.
|
||||
private static ScrollRect ScrollOf(PropertiesPanel panel) => (panel.Content ?? panel.transform).GetComponentInParent<ScrollRect>(true);
|
||||
|
||||
// Hides every list entry the viewport only partly shows, so the list ends on a whole row instead of slicing text.
|
||||
private void FitViewport()
|
||||
{
|
||||
if (_scroll == null || _scroll.content == null) return;
|
||||
if (_rows[0] == null)
|
||||
{
|
||||
_scroll.onValueChanged.RemoveListener(_fit);
|
||||
return;
|
||||
}
|
||||
var (low, high) = ViewSpan(_scroll);
|
||||
foreach (RectTransform entry in _list.Content ?? _list.transform)
|
||||
{
|
||||
var bounds = RectTransformUtility.CalculateRelativeRectTransformBounds(_scroll.content, entry);
|
||||
var whole = bounds.min.y >= low - .5f && bounds.max.y <= high + .5f;
|
||||
if (!entry.TryGetComponent<CanvasGroup>(out var group)) group = entry.gameObject.AddComponent<CanvasGroup>();
|
||||
group.alpha = whole ? 1 : 0;
|
||||
group.blocksRaycasts = whole;
|
||||
}
|
||||
}
|
||||
|
||||
// The viewport's vertical extent in content space. CalculateRelativeRectTransformBounds cannot give it: it also
|
||||
// folds in the rects of the viewport's children, which makes the content's own bounds the answer.
|
||||
private static (float low, float high) ViewSpan(ScrollRect scroll)
|
||||
{
|
||||
var view = scroll.viewport != null ? scroll.viewport : (RectTransform) scroll.transform;
|
||||
var rect = view.rect;
|
||||
var low = scroll.content.InverseTransformPoint(view.TransformPoint(new Vector3(0, rect.yMin))).y;
|
||||
var high = scroll.content.InverseTransformPoint(view.TransformPoint(new Vector3(0, rect.yMax))).y;
|
||||
return (Mathf.Min(low, high), Mathf.Max(low, high));
|
||||
}
|
||||
|
||||
// Takes a button out of Unity's navigation so a click never moves focus and Move never walks automatic navigation.
|
||||
private void Track(Button button) => button.navigation = new Navigation { mode = Navigation.Mode.None };
|
||||
|
||||
// Moves the selection one row, stopping at either end.
|
||||
private void Step(int delta) => Select(_selected + delta);
|
||||
|
||||
// Makes one row the selection: clamps the index, redraws the highlight, and returns focus to Launch.
|
||||
private void Select(int index)
|
||||
{
|
||||
_selected = Mathf.Clamp(index, 0, _scenarios.Count - 1);
|
||||
for (var i = 0; i < _rows.Count; i++)
|
||||
{
|
||||
// The prefab tint barely moves the row colour, so the selected row is drawn in the menu accent.
|
||||
var colors = _rowColors[i];
|
||||
if (i == _selected) colors.normalColor = Color.white;
|
||||
_rows[i].colors = colors;
|
||||
_rows[i].targetGraphic.color = i == _selected ? Accent : _rowFills[i];
|
||||
}
|
||||
Focus(_launchButton.gameObject);
|
||||
Reveal((RectTransform) _rows[_selected].transform);
|
||||
}
|
||||
|
||||
// Scrolls the list pane just far enough that the selected row is inside its viewport.
|
||||
private void Reveal(RectTransform row)
|
||||
{
|
||||
var scroll = _scroll;
|
||||
if (scroll == null || scroll.content == null) return;
|
||||
var (low, high) = ViewSpan(scroll);
|
||||
var rowBounds = RectTransformUtility.CalculateRelativeRectTransformBounds(scroll.content, row);
|
||||
var shift = rowBounds.max.y > high ? rowBounds.max.y - high
|
||||
: rowBounds.min.y < low ? rowBounds.min.y - low : 0;
|
||||
// Content moves against the row: a row above the viewport needs the content to move down, and the reverse.
|
||||
scroll.content.anchoredPosition -= new Vector2(0, shift);
|
||||
if (shift != 0) FitViewport();
|
||||
}
|
||||
|
||||
// Launches the selection, once: Enter, Submit and a mouse click all arrive here, and nothing launches again until
|
||||
// the picker is shown afresh, however long the menu takes to fade out.
|
||||
private void Activate()
|
||||
{
|
||||
if (_launched) return;
|
||||
_launched = true;
|
||||
Focus(null);
|
||||
ReleaseFocus();
|
||||
_launch(Selected);
|
||||
}
|
||||
|
||||
// Leaves the screen; the same path for the Back button and Cancel.
|
||||
private void Back()
|
||||
{
|
||||
Focus(null);
|
||||
ReleaseFocus();
|
||||
_back();
|
||||
}
|
||||
|
||||
// While the picker is up, a click on empty space must not drop focus from Launch, or the arrows and Enter go dead.
|
||||
// The input module's flag is scene-wide, so the picker takes it for its own lifetime and gives it back on leaving.
|
||||
private void HoldFocus()
|
||||
{
|
||||
if (EventSystem.current == null || !(EventSystem.current.currentInputModule is InputSystemUIInputModule module) || !module.deselectOnBackgroundClick) return;
|
||||
module.deselectOnBackgroundClick = false;
|
||||
_restoreDeselect = true;
|
||||
}
|
||||
|
||||
private void ReleaseFocus()
|
||||
{
|
||||
if (!_restoreDeselect) return;
|
||||
_restoreDeselect = false;
|
||||
if (EventSystem.current != null && EventSystem.current.currentInputModule is InputSystemUIInputModule module) module.deselectOnBackgroundClick = true;
|
||||
}
|
||||
|
||||
private static void Focus(GameObject target)
|
||||
{
|
||||
if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(target);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 252d53300699b374db95fe7d7fcfd9b8
|
||||
|
|
@ -59,10 +59,8 @@ public class Stardust : MonoBehaviour
|
|||
// The dust is drawn by a command buffer on TargetCamera: a camera replays its buffers at their event and never culls them,
|
||||
// so the draw needs no bounds and no extent can fall short of how deep the field is.
|
||||
private CommandBuffer _draw;
|
||||
// Into the G-buffer, so the dust's depth is in the depth texture the [ImageEffectOpaque] nebula marches against and the fog
|
||||
// attenuates the dust by distance like any opaque surface. Drawn later (BeforeImageEffectsOpaque) it missed that depth and
|
||||
// was fogged as if it were the far background.
|
||||
private const CameraEvent DrawEvent = CameraEvent.AfterGBuffer;
|
||||
// Before the [ImageEffectOpaque] nebula, so its march fogs the dust; a deferred camera skips the forward-opaque events when nothing forward-opaque is drawn (B46, B49).
|
||||
private const CameraEvent DrawEvent = CameraEvent.BeforeImageEffectsOpaque;
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -107,12 +105,7 @@ public class Stardust : MonoBehaviour
|
|||
DetachDraw();
|
||||
_draw?.Release();
|
||||
_draw = new CommandBuffer { name = "Stardust" };
|
||||
_draw.SetRenderTarget(new RenderTargetIdentifier[]
|
||||
{
|
||||
BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer1, BuiltinRenderTextureType.GBuffer2,
|
||||
BuiltinRenderTextureType.CameraTarget
|
||||
}, BuiltinRenderTextureType.CameraTarget);
|
||||
_draw.DrawProcedural(Matrix4x4.identity, ParticleMaterial, ParticleMaterial.FindPass("DEFERRED"), MeshTopology.Triangles, 6, Span * Span);
|
||||
_draw.DrawProcedural(Matrix4x4.identity, ParticleMaterial, 0, MeshTopology.Triangles, 6, Span * Span);
|
||||
AttachDraw();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,104 +83,6 @@
|
|||
float2 screenUV = i.screenPos.xy / i.screenPos.w;
|
||||
ditherClip(screenUV, emission * i.color.a);
|
||||
return i.color * emission * (exp(_EmissionGain * 5.0f));
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
Pass {
|
||||
Name "DEFERRED"
|
||||
Tags { "LightMode" = "Deferred" }
|
||||
|
||||
CGPROGRAM
|
||||
#pragma vertex particle_vertex
|
||||
#pragma fragment frag
|
||||
|
||||
#pragma target 5.0
|
||||
#pragma multi_compile ___ UNITY_HDR_ON
|
||||
#include "UnityCG.cginc"
|
||||
#include "Assets/Shaders/Dither Functions.cginc"
|
||||
|
||||
struct Particle
|
||||
{
|
||||
float3 position;
|
||||
float3 color;
|
||||
float size;
|
||||
};
|
||||
|
||||
StructuredBuffer<Particle> particles;
|
||||
StructuredBuffer<float3> quadPoints;
|
||||
|
||||
float4 _TintColor;
|
||||
float _EmissionGain;
|
||||
float _Power;
|
||||
float _DistanceSizeExponent;
|
||||
float _DistanceIntensityExponent;
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
float4 screenPos : TEXCOORD1;
|
||||
float4 color : COLOR;
|
||||
};
|
||||
|
||||
//Vertex shader with no inputs
|
||||
//Uses the system values SV_VertexID and SV_InstanceID to read from compute buffers
|
||||
v2f particle_vertex(uint id : SV_VertexID, uint inst : SV_InstanceID)
|
||||
{
|
||||
v2f o;
|
||||
|
||||
//Only transform world pos by view matrix
|
||||
//To Create a billboarding effect
|
||||
float3 worldPosition = particles[inst].position;
|
||||
float dist = length(worldPosition - _WorldSpaceCameraPos);
|
||||
|
||||
float4 cameraSpacePosition = mul(UNITY_MATRIX_V, float4(worldPosition, 1.0f));
|
||||
//cameraSpacePosition.w /= dist;
|
||||
|
||||
float3 quadPoint = float3(quadPoints[id].xy, 0.0f) * particles[inst].size * pow(dist, _DistanceSizeExponent) / pow(100, _DistanceSizeExponent);
|
||||
o.pos = mul(UNITY_MATRIX_P, cameraSpacePosition + float4(quadPoint, 0.0f));
|
||||
|
||||
//Shift coordinates for uvs
|
||||
o.uv = quadPoints[id] + 0.5f;
|
||||
|
||||
//transfer color of particle and global tint to vertex
|
||||
o.color = float4(particles[inst].color * _TintColor.rgb, 100 * pow(100, _DistanceIntensityExponent) / pow(dist, _DistanceIntensityExponent));
|
||||
o.screenPos = ComputeScreenPos(o.pos);
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
float powerPulse( float x, float power )
|
||||
{
|
||||
x = saturate(abs(x));
|
||||
return pow((x + 1.0f) * (1.0f - x), power);
|
||||
}
|
||||
|
||||
// The dust in the G-buffer (Stardust.cs DrawEvent): black, unreflective, emissive, so deferred lighting adds nothing and
|
||||
// its depth is in the depth texture the nebula march fogs by.
|
||||
struct GBufferOut
|
||||
{
|
||||
half4 albedo : SV_Target0;
|
||||
half4 specular : SV_Target1;
|
||||
half4 normal : SV_Target2;
|
||||
half4 emission : SV_Target3;
|
||||
};
|
||||
|
||||
GBufferOut frag(v2f i)
|
||||
{
|
||||
float emission = powerPulse(length(i.uv-float2(.5,.5))*2,_Power);
|
||||
float2 screenUV = i.screenPos.xy / i.screenPos.w;
|
||||
ditherClip(screenUV, emission * i.color.a);
|
||||
GBufferOut o;
|
||||
o.albedo = 0;
|
||||
o.specular = 0;
|
||||
o.normal = half4(.5, 1, .5, 1);
|
||||
float3 light = (i.color * emission * (exp(_EmissionGain * 5.0f))).rgb;
|
||||
#ifndef UNITY_HDR_ON
|
||||
light = exp2(-light);
|
||||
#endif
|
||||
o.emission = half4(light, 1);
|
||||
return o;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,33 +54,6 @@ Shader "Aetheria/Dithered Particles"
|
|||
ditherClip(screenUV + i.ditherOffset, c.a);
|
||||
return c;
|
||||
}
|
||||
|
||||
// The particle in the G-buffer: black, unreflective, emissive, so deferred lighting adds nothing and its depth is in the
|
||||
// depth texture the nebula march fogs by. A forward pass draws after that texture is taken, so the fog paints over it.
|
||||
struct GBufferOut
|
||||
{
|
||||
half4 albedo : SV_Target0;
|
||||
half4 specular : SV_Target1;
|
||||
half4 normal : SV_Target2;
|
||||
half4 emission : SV_Target3;
|
||||
};
|
||||
|
||||
GBufferOut frag_deferred(v2f i)
|
||||
{
|
||||
float4 c = tex2D(_MainTex, i.texcoord) * _Color * i.color;
|
||||
float2 screenUV = i.screenPos.xy / i.screenPos.w;
|
||||
ditherClip(screenUV + i.ditherOffset, c.a);
|
||||
GBufferOut o;
|
||||
o.albedo = 0;
|
||||
o.specular = 0;
|
||||
o.normal = half4(.5, 1, .5, 1);
|
||||
float3 light = c.rgb;
|
||||
#ifndef UNITY_HDR_ON
|
||||
light = exp2(-light);
|
||||
#endif
|
||||
o.emission = half4(light, 1);
|
||||
return o;
|
||||
}
|
||||
|
||||
float4 frag_shadow(v2f i) : COLOR
|
||||
{
|
||||
|
|
@ -98,23 +71,12 @@ Shader "Aetheria/Dithered Particles"
|
|||
Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" "IgnoreProjector"="True" }
|
||||
Pass
|
||||
{
|
||||
Tags { "LightMode" = "ForwardBase" }
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
ENDCG
|
||||
}
|
||||
Pass
|
||||
{
|
||||
Name "DEFERRED"
|
||||
Tags { "LightMode" = "Deferred" }
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag_deferred
|
||||
#pragma multi_compile ___ UNITY_HDR_ON
|
||||
ENDCG
|
||||
}
|
||||
Pass
|
||||
{
|
||||
Name "ShadowCaster"
|
||||
Tags { "LightMode" = "ShadowCaster" }
|
||||
|
|
|
|||
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
Binary file not shown.
3
Vault/Aetheria/.obsidian/app.json
vendored
Normal file
3
Vault/Aetheria/.obsidian/app.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"alwaysUpdateLinks": true
|
||||
}
|
||||
1
Vault/Aetheria/.obsidian/appearance.json
vendored
Normal file
1
Vault/Aetheria/.obsidian/appearance.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
3
Vault/Aetheria/.obsidian/backlink.json
vendored
Normal file
3
Vault/Aetheria/.obsidian/backlink.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"backlinkInDocument": true
|
||||
}
|
||||
3
Vault/Aetheria/.obsidian/community-plugins.json
vendored
Normal file
3
Vault/Aetheria/.obsidian/community-plugins.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[
|
||||
"copilot"
|
||||
]
|
||||
1
Vault/Aetheria/.obsidian/copilot-index-1197ead7d854cc570b05a52e661c67cb.json
vendored
Normal file
1
Vault/Aetheria/.obsidian/copilot-index-1197ead7d854cc570b05a52e661c67cb.json
vendored
Normal file
File diff suppressed because one or more lines are too long
31
Vault/Aetheria/.obsidian/core-plugins.json
vendored
Normal file
31
Vault/Aetheria/.obsidian/core-plugins.json
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": true,
|
||||
"webviewer": false
|
||||
}
|
||||
51
Vault/Aetheria/.obsidian/graph.json
vendored
Normal file
51
Vault/Aetheria/.obsidian/graph.json
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"collapse-filter": true,
|
||||
"search": "path:Aetheria/Worldbuilding ",
|
||||
"showTags": false,
|
||||
"showAttachments": false,
|
||||
"hideUnresolved": false,
|
||||
"showOrphans": true,
|
||||
"collapse-color-groups": false,
|
||||
"colorGroups": [
|
||||
{
|
||||
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Timeline ",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 5410784
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major ",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 14725458
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor ",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 14701138
|
||||
}
|
||||
},
|
||||
{
|
||||
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Factions/Movements ",
|
||||
"color": {
|
||||
"a": 1,
|
||||
"rgb": 5672760
|
||||
}
|
||||
}
|
||||
],
|
||||
"collapse-display": false,
|
||||
"showArrow": false,
|
||||
"textFadeMultiplier": -1.6,
|
||||
"nodeSizeMultiplier": 0.431770833333333,
|
||||
"lineSizeMultiplier": 2.0140625,
|
||||
"collapse-forces": false,
|
||||
"centerStrength": 0.416666666666667,
|
||||
"repelStrength": 19.1145833333333,
|
||||
"linkStrength": 0.8359375,
|
||||
"linkDistance": 396,
|
||||
"scale": 0.36288736930121657,
|
||||
"close": true
|
||||
}
|
||||
20
Vault/Aetheria/.obsidian/hotkeys.json
vendored
Normal file
20
Vault/Aetheria/.obsidian/hotkeys.json
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"note-composer:split-file": [
|
||||
{
|
||||
"modifiers": [
|
||||
"Alt",
|
||||
"Mod"
|
||||
],
|
||||
"key": "X"
|
||||
}
|
||||
],
|
||||
"editor:insert-wikilink": [
|
||||
{
|
||||
"modifiers": [
|
||||
"Alt",
|
||||
"Mod"
|
||||
],
|
||||
"key": "B"
|
||||
}
|
||||
]
|
||||
}
|
||||
399
Vault/Aetheria/.obsidian/plugins/copilot/data.json
vendored
Normal file
399
Vault/Aetheria/.obsidian/plugins/copilot/data.json
vendored
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
{
|
||||
"isPlusUser": false,
|
||||
"plusLicenseKey": "",
|
||||
"openAIApiKey": "",
|
||||
"openAIOrgId": "",
|
||||
"huggingfaceApiKey": "",
|
||||
"cohereApiKey": "",
|
||||
"anthropicApiKey": "",
|
||||
"azureOpenAIApiKey": "",
|
||||
"azureOpenAIApiInstanceName": "",
|
||||
"azureOpenAIApiDeploymentName": "",
|
||||
"azureOpenAIApiVersion": "",
|
||||
"azureOpenAIApiEmbeddingDeploymentName": "",
|
||||
"googleApiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
|
||||
"openRouterAiApiKey": "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",
|
||||
"defaultChainType": "vault_qa",
|
||||
"defaultModelKey": "deepseek/deepseek-chat-v3-0324:free|openrouterai",
|
||||
"embeddingModelKey": "text-embedding-004|google",
|
||||
"temperature": 0.25,
|
||||
"maxTokens": 12800,
|
||||
"contextTurns": 15,
|
||||
"userSystemPrompt": "You are a science fiction worldbuilding lover. You are interested in exploring unique high concepts and their social ramifications. You are not shy to point out discrepancies or explore new ways to express narrative themes. You love to think about how chaotic dynamical systems operating at every scale have emergent effects on the human condition, and the boundaries of the mind. At every step, you try to consider how change affects people both individually and in groups. You are obsessed with culture - how it is formed, how it diverges, and how that divergence culminates in synthesis. You like to employ evocative language in your explanations of complex interactions or phenomena, but also value conciseness.",
|
||||
"openAIProxyBaseUrl": "",
|
||||
"openAIEmbeddingProxyBaseUrl": "",
|
||||
"stream": true,
|
||||
"defaultSaveFolder": "copilot-conversations",
|
||||
"defaultConversationTag": "copilot-conversation",
|
||||
"autosaveChat": true,
|
||||
"defaultOpenArea": "view",
|
||||
"customPromptsFolder": "copilot-custom-prompts",
|
||||
"indexVaultToVectorStore": "ON MODE SWITCH",
|
||||
"qaExclusions": "Aetheria%2FWorldbuilding%2FPost-Elysium,copilot-conversations",
|
||||
"qaInclusions": "",
|
||||
"chatNoteContextPath": "",
|
||||
"chatNoteContextTags": [],
|
||||
"enableIndexSync": true,
|
||||
"debug": false,
|
||||
"enableEncryption": false,
|
||||
"maxSourceChunks": 70,
|
||||
"groqApiKey": "",
|
||||
"mistralApiKey": "",
|
||||
"activeModels": [
|
||||
{
|
||||
"name": "copilot-plus-flash",
|
||||
"provider": "copilot-plus",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"plusExclusive": true,
|
||||
"projectEnabled": false,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "google/gemini-2.5-flash-lite-preview-06-17",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "google/gemini-2.5-flash",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "google/gemini-2.5-pro",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gpt-4.1",
|
||||
"provider": "openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gpt-4.1-mini",
|
||||
"provider": "openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gpt-4.1-nano",
|
||||
"provider": "openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "o4-mini",
|
||||
"provider": "openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"capabilities": [
|
||||
"reasoning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "claude-3-5-sonnet-latest",
|
||||
"provider": "anthropic",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"core": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "claude-sonnet-4-20250514",
|
||||
"provider": "anthropic",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"capabilities": [
|
||||
"reasoning",
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "claude-3-7-sonnet-latest",
|
||||
"provider": "anthropic",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"capabilities": [
|
||||
"reasoning",
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "claude-3-5-haiku-latest",
|
||||
"provider": "anthropic",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "grok-3-beta",
|
||||
"provider": "xai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "grok-3-mini-beta",
|
||||
"provider": "xai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "gemini-2.5-flash",
|
||||
"provider": "google",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gemini-2.5-pro",
|
||||
"provider": "google",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"projectEnabled": true,
|
||||
"capabilities": [
|
||||
"vision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "command-r",
|
||||
"provider": "cohereai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "command-r-plus",
|
||||
"provider": "cohereai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "azure-openai",
|
||||
"provider": "azure openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "deepseek-chat",
|
||||
"provider": "deepseek",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "deepseek-reasoner",
|
||||
"provider": "deepseek",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"capabilities": [
|
||||
"reasoning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gemini-2.0-pro-exp-02-05",
|
||||
"provider": "google",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
|
||||
"isEmbeddingModel": false,
|
||||
"capabilities": [],
|
||||
"stream": true,
|
||||
"displayName": "Gemini Pro Exp (Google)"
|
||||
},
|
||||
{
|
||||
"name": "gemini-2.0-flash",
|
||||
"provider": "google",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
|
||||
"isEmbeddingModel": false,
|
||||
"capabilities": [],
|
||||
"stream": true,
|
||||
"displayName": "Gemini Flash"
|
||||
},
|
||||
{
|
||||
"name": "deepseek/deepseek-chat-v3-0324:free",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "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",
|
||||
"isEmbeddingModel": false,
|
||||
"capabilities": [],
|
||||
"stream": true,
|
||||
"displayName": "Deepseek V3 Free"
|
||||
},
|
||||
{
|
||||
"name": "google/gemini-2.5-pro-exp-03-25:free",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "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",
|
||||
"isEmbeddingModel": false,
|
||||
"capabilities": [],
|
||||
"stream": true,
|
||||
"displayName": "Gemini 2.5"
|
||||
},
|
||||
{
|
||||
"name": "anthropic/claude-3.7-sonnet:beta",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "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",
|
||||
"isEmbeddingModel": false,
|
||||
"capabilities": [],
|
||||
"stream": true,
|
||||
"displayName": "Claude 3.7"
|
||||
},
|
||||
{
|
||||
"name": "moonshotai/kimi-k2:free",
|
||||
"provider": "openrouterai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "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",
|
||||
"isEmbeddingModel": false,
|
||||
"capabilities": [],
|
||||
"stream": true,
|
||||
"displayName": "Kimi K2 Free"
|
||||
}
|
||||
],
|
||||
"activeEmbeddingModels": [
|
||||
{
|
||||
"name": "copilot-plus-small",
|
||||
"provider": "copilot-plus",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true,
|
||||
"core": true,
|
||||
"plusExclusive": true
|
||||
},
|
||||
{
|
||||
"name": "copilot-plus-large",
|
||||
"provider": "copilot-plus-jina",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true,
|
||||
"core": true,
|
||||
"plusExclusive": true,
|
||||
"believerExclusive": true,
|
||||
"dimensions": 1024
|
||||
},
|
||||
{
|
||||
"name": "copilot-plus-multilingual",
|
||||
"provider": "copilot-plus-jina",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true,
|
||||
"core": true,
|
||||
"plusExclusive": true,
|
||||
"dimensions": 512
|
||||
},
|
||||
{
|
||||
"name": "text-embedding-3-small",
|
||||
"provider": "openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true,
|
||||
"core": true
|
||||
},
|
||||
{
|
||||
"name": "text-embedding-3-large",
|
||||
"provider": "openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true
|
||||
},
|
||||
{
|
||||
"name": "embed-multilingual-light-v3.0",
|
||||
"provider": "cohereai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true
|
||||
},
|
||||
{
|
||||
"name": "text-embedding-004",
|
||||
"provider": "google",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true
|
||||
},
|
||||
{
|
||||
"name": "azure-openai",
|
||||
"provider": "azure openai",
|
||||
"enabled": true,
|
||||
"isBuiltIn": true,
|
||||
"isEmbeddingModel": true
|
||||
},
|
||||
{
|
||||
"name": "gemini-embedding-exp-03-07",
|
||||
"provider": "google",
|
||||
"enabled": true,
|
||||
"isBuiltIn": false,
|
||||
"baseUrl": "",
|
||||
"apiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
|
||||
"isEmbeddingModel": true,
|
||||
"capabilities": [],
|
||||
"displayName": "Gemini Embedding"
|
||||
}
|
||||
],
|
||||
"embeddingRequestsPerMin": 90,
|
||||
"embeddingBatchSize": 16,
|
||||
"disableIndexOnMobile": true,
|
||||
"showSuggestedPrompts": true,
|
||||
"showRelevantNotes": true,
|
||||
"numPartitions": 1,
|
||||
"enabledCommands": {},
|
||||
"promptUsageTimestamps": {},
|
||||
"defaultConversationNoteName": "{$topic}@{$date}_{$time}",
|
||||
"userId": "a49ac140-b1c0-460e-a445-ef241a2c46bc",
|
||||
"includeActiveNoteAsContext": true,
|
||||
"passMarkdownImages": true,
|
||||
"enableCustomPromptTemplating": true,
|
||||
"allowAdditionalContext": true,
|
||||
"enableWordCompletion": false,
|
||||
"suggestedDefaultCommands": true
|
||||
}
|
||||
1738
Vault/Aetheria/.obsidian/plugins/copilot/main.js
vendored
Normal file
1738
Vault/Aetheria/.obsidian/plugins/copilot/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
Vault/Aetheria/.obsidian/plugins/copilot/manifest.json
vendored
Normal file
13
Vault/Aetheria/.obsidian/plugins/copilot/manifest.json
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "copilot",
|
||||
"name": "Copilot",
|
||||
"version": "2.9.4",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Your AI Copilot: Chat with Your Second Brain, Learn Faster, Work Smarter.",
|
||||
"author": "Logan Yang",
|
||||
"authorUrl": "https://twitter.com/logancyang",
|
||||
"fundingUrl": {
|
||||
"Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang",
|
||||
"GitHub Sponsor": "https://github.com/sponsors/logancyang"
|
||||
}
|
||||
}
|
||||
1
Vault/Aetheria/.obsidian/plugins/copilot/styles.css
vendored
Normal file
1
Vault/Aetheria/.obsidian/plugins/copilot/styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
257
Vault/Aetheria/.obsidian/workspace.json
vendored
Normal file
257
Vault/Aetheria/.obsidian/workspace.json
vendored
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
{
|
||||
"main": {
|
||||
"id": "d9305bfad72cac32",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "1254e4ae2f0f2e89",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "4116336d30dd8c7a",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Aetheria/Worldbuilding/Post-Elysium/Technology/Aetheric Drive.md",
|
||||
"mode": "source",
|
||||
"source": false,
|
||||
"backlinks": true,
|
||||
"backlinkOpts": {
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
}
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Aetheric Drive"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4b777a3c9c524d60",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Aetheria/Introduction.md",
|
||||
"mode": "source",
|
||||
"source": false,
|
||||
"backlinks": true,
|
||||
"backlinkOpts": {
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
}
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Introduction"
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentTab": 1
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "64cfcffee7eb1bfa",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "70e9b28e39ccce5b",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "f6b3adc4716e8cc3",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": true
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7d7c5f9e0b438037",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2320d9e6e1e7e7c3",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 334.5
|
||||
},
|
||||
"right": {
|
||||
"id": "6880be6f002b1a1b",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "083ac471b77bc2b2",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "230cda80a17e41ef",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for Mind Uploading"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "128f987033addb23",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from Mind Uploading"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cdc30163914a1b4a",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "26c1ad063a2c5c07",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of Mind Uploading"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "de2981dcf97499f7",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "copilot-chat-view",
|
||||
"state": {},
|
||||
"icon": "message-square",
|
||||
"title": "Copilot"
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentTab": 4
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 791.5
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:Open quick switcher": false,
|
||||
"graph:Open graph view": false,
|
||||
"canvas:Create new canvas": false,
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false,
|
||||
"copilot:Open Copilot Chat": false
|
||||
}
|
||||
},
|
||||
"active": "4b777a3c9c524d60",
|
||||
"lastOpenFiles": [
|
||||
"Aetheria/Worldbuilding/Post-Elysium/Species/Ratfolk.md",
|
||||
"Aetheria/Worldbuilding/Post-Elysium/Technology/Aethernet.md",
|
||||
"Aetheria/Worldbuilding/Post-Elysium/Species",
|
||||
"Aetheria/Worldbuilding/Post-Elysium/Concepts",
|
||||
"Aetheria/Worldbuilding/Post-Elysium/Technology/Aetheric Drive.md",
|
||||
"Aetheria/Worldbuilding/Post-Elysium/Technology",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Advanced Superconductors.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Cryonix.md",
|
||||
"copilot-conversations/I_think_there's_a_few_discrepancies_in_the_[[Timeline]]._It@20250313_215236.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Genetic Enhancement.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Biodrone.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Timeline/Identity Abyss.md",
|
||||
"Aetheria/Worldbuilding/Politics/Conflict Permit.md",
|
||||
"Aetheria/Worldbuilding/Politics/The PSC's Legal Framework.md",
|
||||
"Aetheria/Worldbuilding/Politics/Damage Cap.md",
|
||||
"Aetheria/Worldbuilding/Politics/Rules of Engagement.md",
|
||||
"Aetheria/Introduction.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Bloom.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Timeline/Existential Collapse.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/From Sol to Elysium.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Timeline/Identity Crisis.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Megas.md",
|
||||
"copilot-conversations/Please_help_me_identify_underdeveloped_areas_in_the_setting_and@20250715_171817.md",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Timeline.md",
|
||||
"Aetheria/Worldbuilding/Politics/Public Image.md",
|
||||
"Aetheria/Worldbuilding/Politics/Why Zhestokost Can't Simply Nuke Its Adversaries.md",
|
||||
"copilot-custom-prompts/Simplify.md",
|
||||
"copilot-custom-prompts",
|
||||
"copilot-conversations",
|
||||
"Untitled.canvas",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Movements",
|
||||
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers",
|
||||
"Aetheria/Worldbuilding/Politics"
|
||||
]
|
||||
}
|
||||
2
Vault/Aetheria/Aetheria/Introduction.md
Normal file
2
Vault/Aetheria/Aetheria/Introduction.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
The world of Aetheria is the product of capitalism taken to its farthest conclusion—a corporate-owned jigsaw of commercialism, copyright law, and profiteering that puts a tangible price on everything from identity to interaction. You, the player, must navigate the complexities of a world based on trade and treachery, choosing your alliances with care and your battles with courage--and a shipload of futuristic weaponry.
|
||||
7
Vault/Aetheria/Aetheria/Worldbuilding/Politics.md
Normal file
7
Vault/Aetheria/Aetheria/Worldbuilding/Politics.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
In Aetheria's corporate-controlled solar system, warfare is a tightly regulated affair, governed by the Pan Solar Consortium's legal framework to ensure that competition remains profitable and sustainable. Through conflict permits, damage caps, neutral zones, and robust enforcement mechanisms, the PSC prevents factions like Zhestokost from unleashing unchecked destruction. Warfare is thus a strategic tool—used for economic gain, market control, and leverage—rather than a path to mutual annihilation. Even the most ruthless corporations must play by these rules or risk losing everything in the cutthroat game of solar capitalism.
|
||||
|
||||
[[The PSC's Legal Framework]]
|
||||
|
||||
[[Enforcement]]
|
||||
|
||||
[[Restrictions on Warfare]]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#### Arbitration and Ceasefires
|
||||
|
||||
The PSC can enforce ceasefires or arbitration if a conflict threatens to spiral out of control. Corporations are incentivized to comply, as defiance risks exclusion from lucrative markets or trade routes.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Corporations must apply for "conflict permits" to engage in any form of warfare.
|
||||
|
||||
These permits require detailed documentation of the scope, duration, assets involved, and intended objectives. The PSC approves or denies permits based on the potential impact on the broader economy. For example, [[Zhestokost]] might apply for a permit to seize a rival's asteroid mining operation, but the permit would specify limits on the force used and the duration of the engagement.
|
||||
|
|
@ -0,0 +1 @@
|
|||
While espionage is rampant, the PSC monitors and regulates it to prevent destabilizing actions. Corporations caught engaging in unsanctioned espionage face fines or temporary bans on certain technologies.
|
||||
|
|
@ -0,0 +1 @@
|
|||
AGI-driven cyber attacks are a primary weapon, targeting digital infrastructure to steal data, sabotage operations, or manipulate stock prices. Neural hacks on corporate leaders or key personnel are also common, aiming to disrupt decision-making without physical destruction.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Each conflict is assigned a "damage cap," a monetary limit on the economic and infrastructural harm allowed. Corporations must pay fines or face sanctions if they exceed these caps, discouraging wanton destruction and encouraging precision strikes.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Despite their militaristic and autocratic ideology, Zhestokost relies on other corporations for certain resources, technologies, and markets. For example, they might depend on [[NiteLife Energy]] for power generation or [[Finch Cybernetics]] for advanced cybernetic enhancements. Destroying these corporations would cripple Zhestokost's own operations.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
### Economic Sanctions
|
||||
|
||||
Corporations that violate PSC regulations face severe financial penalties, including seizure of assets, trade embargoes, or exclusion from the [[Aethernet]]. Given the interdependence of the solar economy, these sanctions can cripple even the most powerful corporations.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
To ensure compliance with its regulations, the [[Pan-Solar Consortium]] employs a combination of economic, technological, and military tools.
|
||||
|
||||
[[Private Military Contractors]]
|
||||
[[Economic Sanctions]]
|
||||
[[Technological Controls]]
|
||||
[[Corporate Espionage Oversight]]
|
||||
|
|
@ -0,0 +1 @@
|
|||
Unrestricted warfare, especially involving nuclear weapons, risks triggering a chain reaction of retaliation that could devastate the solar system's infrastructure. The resulting loss of resources and markets would be catastrophic for all corporations, including [[Zhestokost]], leading to a lose-lose scenario.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Certain areas, such as major trade hubs, resource-rich regions, or critical infrastructure like network relays, are designated as no-conflict zones. Warfare in these areas is strictly prohibited to protect shared economic interests.
|
||||
1
Vault/Aetheria/Aetheria/Worldbuilding/Politics/PSCC.md
Normal file
1
Vault/Aetheria/Aetheria/Worldbuilding/Politics/PSCC.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
The rise of the Pan Solar Consortium Credit reflects the [[Pan-Solar Consortium]]’s economic leverage. By [[Identity Abyss]], corporate scrip replaced traditional currencies in off-world colonies, and the PSC standardized it into the PSCC, backed by collective corporate assets (e.g., [[SolEx]]’s minerals, [[NiteLife Energy]]’s energy). Its universal adoption is less a triumph of policy and more a necessity—colonies are too intertwined to function without a unified currency. The PSC enforces its stability through its [[Enforcement]], punishing noncompliance with isolation. This cements the PSCC’s dominance, even as the PSC itself remains a hollow shell.
|
||||
|
|
@ -0,0 +1 @@
|
|||
The [[Pan-Solar Consortium]] contracts neutral mercenary groups to enforce its rules and intervene in unauthorized conflicts. These PMCs are well-armed and operate under strict PSC oversight, acting as a deterrent to rogue actions.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Licensed privateers, operating under letters of marque from the PSC, raid enemy shipping lanes or colonies. This allows corporations to disrupt rivals while maintaining a veneer of legality and avoiding direct confrontation.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Corporations often fund and arm smaller factions, pirates, or privateers to act as deniable assets.
|
||||
These proxies carry out raids, sabotage, or resource denial operations without directly implicating their corporate sponsors, allowing corporations to maintain plausible deniability.
|
||||
|
|
@ -0,0 +1 @@
|
|||
In a system where perception influences stock prices and consumer behavior, being seen as excessively destructive can harm a corporation's brand. Even Zhestokost must maintain a facade of "disciplined strength" rather than reckless aggression to attract clients and maintain internal loyalty.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Corporations may engage in operations to contaminate or booby-trap resource deposits, making them unusable for competitors without permanently destroying the resource itself. This approach weakens adversaries economically while preserving the solar system's resource base.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Warfare in Aetheria is not characterized by large-scale battles or indiscriminate destruction but by calculated, often covert operations designed to weaken competitors without destabilizing the broader economy. The primary objective is to gain economic or strategic advantage, such as seizing key assets, disrupting supply chains, or forcing rivals into unfavorable mergers. Here are the key characteristics of warfare in this setting:
|
||||
|
||||
[[Rules of Engagement]]
|
||||
[[Proxy Conflicts]]
|
||||
[[Resource Denial]]
|
||||
[[Cyber Warfare]]
|
||||
[[Privateering]]
|
||||
|
||||
[[Why Zhestokost Can't Simply Nuke Its Adversaries]]
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
Battles are typically small-scale, focusing on specific objectives like capturing a mining outpost, disrupting a trade route, or sabotaging a competitor's production facility. Large-scale conflicts are rare due to their potential to cause widespread economic damage, which is heavily discouraged by the PSC.
|
||||
|
|
@ -0,0 +1 @@
|
|||
The PSC deploys advanced monitoring systems, including satellite networks and [[Artificial General Intelligence]]-driven surveillance, to detect unauthorized use of weapons of mass destruction or breaches of neutral zones. Automated defense systems can neutralize threats in real-time, such as intercepting unauthorized missile launches.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
The PSC, acting as the de facto governing body of the solar system, enforces a strict legal framework to regulate warfare and prevent catastrophic damage to the economy and infrastructure. This framework is designed to protect shared corporate interests while allowing for controlled competition. Key elements include:
|
||||
|
||||
[[Conflict Permit]]s
|
||||
[[Damage Cap]]s
|
||||
[[Neutral Zone]]s
|
||||
[[Arbitration and Ceasefires]]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
The PSC's framework and the realities of the solar economy constrains even a faction as aggressive as [[Zhestokost]]. Several factors prevent them from resorting to total warfare, such as deploying nuclear weapons:
|
||||
|
||||
[[Mutual Destruction]]
|
||||
[[Economic Interdependence]]
|
||||
[[Enforcement]]
|
||||
[[Public Image]]
|
||||
|
||||
Zhestokost's own leadership, composed of uploaded minds obsessed with long-term survival, understands that total warfare could lead to their own destruction. Their doctrine emphasizes victory through discipline, not annihilation, aligning with the PSC's broader framework.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
## Aether
|
||||
|
||||
Aetheria’s namesake is the now-defunct scientific concept of aether, a propagation medium for light waves. In Elysium, aether is real, and this static reference frame can be manipulated to enable phenomena considered impossible in the universe as we know it. The Aetheric Field permeates all of Elysium and is essential to its function as an inescapable garden-prison for sentient life. It is an extremely high-level construct of the very technology humanity began to scratch the surface of when it started messing with FTL travel.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Certain configurations of sapient minds can willfully interact via the [[Aether]] to sense or alter reality. For humans, such a state of mind involves a level of dissociation that requires intense trauma, neurodivergence or deep meditation to achieve. Other species (or specially engineered AGI constructs) may find it easier to develop such abilities, and those who have spent time in certain regions of deep pseudospace. Regardless, even the highest degree of fine Aetheric control cannot rival even a shadow of what the Timeless accomplish as easily as breathing.
|
||||
|
||||
An uploaded human esper retains their psi capabilities, though they are not economically useful as a mass-market product, as the standard accelerated brainwashing protocols normally used to make enslaved uploads compliant also have the tendency to destroy their ability to touch the Aether. Uploaded espers exist who live free, but suffer the same limitations as any upload, since their minds are considered non-sentient intellectual property and their abilities disputed.
|
||||
|
||||
Corporations work hard to discredit espers and suppress awareness of their abilities, funding studies that “prove” that esper experiences are hallucinations or mental illness. Any evidence of cognitive interfacing is buried under corporate-controlled media. Espers who go public with their abilities find themselves labeled as conspiracy theorists or extremists. Entertainment often features ridiculous portrayals of esper abilities, making real cognitive interfacing seem like fantasy.
|
||||
|
||||
Legitimate studies of Aetheric cognition are locked behind classified research divisions, ensuring that only corporations benefit from the knowledge by secretly employing trained espers in high-value roles—data analysis, high-risk security, espionage, sabotage, and high-frequency trading via Aetheric foresight. These espers are often recruited and trained by those same research institutions, serving as covert training centers while filtering out any potential threats.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
### Negative Entropy
|
||||
|
||||
Another consequence of the existence of [[Aether]] is a phenomenon known as negative entropy, which manifests as a powerful conversion of thermal energy into high-energy photons. Negent arises from the interaction of electromagnetic waves with the aether via a process understood in detail only by the Adrasteia corporation, which holds a virtual monopoly on the technology and requires a configuration of strange matter encountered only in Adrasteia-controlled pseudospace.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
## Pseudospace
|
||||
|
||||
Elysium is a much stranger place than the core worlds would have you believe. They talk about pseudospace like it’s an incursion, a rift intruding upon their stable reality. But we brought flat space with us, we are the incursion. This universe’s natural state is one of chaos, alternate spatial topologies, twisted manifolds of space and time abound out there, and hyper-euclidean space is many times bigger than the flatlanders could ever conceive.
|
||||
|
||||
Factions that only bother to explore flat space have a relatively uniform understanding of what to expect, but absolutely anything is possible when one of the corporations is brave enough to go all-in with the project of colonizing and exploiting Wonderland. We have a cultural divide now, between the Chaos factions who live on the periphery of stable Euclidean space, and the Flatlanders who build their camps on the few islands of stability poking from an infinite ocean of madness.
|
||||
|
|
@ -0,0 +1 @@
|
|||
With the [[Aether]] so attuned to cognition, there are some circumstances where a mind can imprint itself into the field and propagate itself independently of its physical substrate. Sometimes when an esper is astrally projecting, they might forget the way back to their body or be severed from it. Certain high-energy excitations of the Aetheric Field can also cause such a projection to be created. An Aetheric projection (or spirit) maintains all of its cognitive and psi abilities, without the ability to change and learn beyond what context it can hold in its mind. Spirits must expend energy to both think and act, and unless externally supplemented they will eventually fade.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
### Temporal Nonlinearity
|
||||
|
||||
One of the peculiarities of the Timeless’ purpose in constructing Elysium is that they, for their own inscrutable reasons, did not wish to actually exterminate any of the civilizations they quarantined to this isolated shard of bespoke spacetime. To ensure that the prisoners never manage to wipe themselves out, temporal contingencies were put in place. The timeline is split, with each species getting their own branch. The Aetheric Field, with its ability to directly interact with sapient minds, monitors the timelines and triggers a reset whenever the prime species is unable to continue existing.
|
||||
|
||||
This means that humanity, with its competitiveness incentivizing reckless accelerationism and exploitation of every possible technology or resource regardless of its negative externalities, tends to speedrun its own destruction, leading to its relatively short timeline existing alongside those dedicated to other species much more capable of cooperation and collective survival.
|
||||
|
||||
The Aetheric Field also incorporates some predictive capacity, a side effect of its pervasiveness and inherent ability to process information. Visions of the future accessed via the [[Aether]] are not prophetic, merely likely extrapolations of the timeline.
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
**"In the dark, we thrive; in unity, we endure."**
|
||||
|
||||
The Ratfolk, known to themselves as the _Krr’vax_ ("The Hidden Kin"), are a diminutive, rodent-like species whose cautious psychology and subterranean origins have forged a united, conservative civilization in Elysium. Standing just 40 cm tall, with oversized heads, twitching ears, long snouts, and bristling whiskers, they strike humans as adorable—often dubbed “anthropomorphic rats.” Yet their unassuming exterior belies a cunning, cooperative people shaped by a history of hiding from predators and mastering their hostile world through stealth, resilience, and incremental innovation. Autophobic, agoraphobic, and photophobic, they shun isolation, open spaces, and bright lights, thriving instead in tight-knit, dimly lit communities that echo their ancestral burrows.
|
||||
|
||||
#### Evolutionary and Civilizational History
|
||||
|
||||
The Ratfolk hail from _Vax’korr_, a tectonically volatile planet of toxic mists, violent storms, and predatory megafauna. Evolving as small, omnivorous scavengers, their ancestors survived by retreating to underground warrens, where acute senses—hearing, smell, and vibration detection through their skin—kept them safe. Natural selection favored compact bodies and a pack mentality, laying the foundation for sapience, language, and tool use. By the time they achieved tribal cohesion, trading scavenged bones and fungal goods between clans, their world shifted: a catastrophic volcanic eruption 40,000 years ago triggered a planetwide volcanic winter, burying the surface in ash and decimating their predators.
|
||||
|
||||
This disaster was their salvation. Already civilized, the Ratfolk leveraged geothermal heat and fungal farming to endure underground, then emerged to colonize the surface with fortified burrow-cities. Their cautious nature slowed progress—where humans raced to space, the Ratfolk iterated painstakingly, valuing stability over speed. Metallurgy fortified their defenses, chemistry refined their tools, and computing unified their clans, but each step took millennia of deliberation. Spaceflight emerged 10,000 years ago to monitor tectonic threats, followed by a 2,000-year crawl to colonize their system with windowless _Burrowhulks_. Only a rare radical faction dared test an FTL drive, triggering the Timeless’ contingency and shunting them into Elysium. To the Ratfolk, this exile was merely another hostile surface to endure—a testament to their unyielding resilience.
|
||||
|
||||
#### Language
|
||||
|
||||
_Krr’vaxi_, the Ratfolk tongue, is a subtle blend of chitters, clicks, and vibrations, optimized for echo-prone tunnels. Pitch conveys emotion, while whisker twitches and ear flicks add nuance. Their written script—tactile glyphs etched into stone or metal—is read by touch, a necessity of their photophobic heritage. In Elysium, translator implants render their staccato speech into alien tongues, often laced with phrases like _“Krr’tal vax”_ (“We endure together”) or _“Shk’ra turr”_ (“The dark protects”), reflecting their communal and secretive ethos.
|
||||
|
||||
#### Technology
|
||||
|
||||
Ratfolk technology is a monument to redundancy and reliability, tracing unbroken lineages back thousands of years. Each innovation builds on the last, tested exhaustively to ensure it complements tradition rather than disrupts it.
|
||||
|
||||
- **Vibration Matrix Systems**: Evolving from stone drums to piezoelectric networks, these map environments via seismic pulses, enhanced in Elysium with aetheric amplifiers. Used in _Burrowhulks_, personal _Whisker Talismans_, and planetary defenses, they detect threats without light, a legacy of predator evasion that enabled surface mapping post-volcanic winter.
|
||||
- **Geothermal Energy Harnessing**: Starting with stone conduits, refined into molten-salt reactors and thermoelectric generators, and later boosted by aetheric converters. It powers dim illumination (fungal glows or panels), forges, and ships, sustaining life through the volcanic winter and fueling surface expansion.
|
||||
- **Chemical and Metallurgical Refinement**: From fungal acids etching stone to titanium smelting and semiconductor microscopy, these crafts built durable tools and electronics. Chemistry also yields stealth coatings and pheromones, driving industrial and espionage capabilities.
|
||||
- **Computing and Data Storage**: Mechanical calculators gave way to semiconductor-based parallel processors, storing data on engraved discs or quantum lattices. Limited to clan-shared systems to preserve unity, they standardized trade and navigation, reluctantly enabling spaceflight.
|
||||
- **Materials Science**: Ancestral fungal composites—light, insulating mycelium weaves—persist alongside plastics and carbon-fiber polymers. Modern gear blends both: plastic ship frames with fungal linings, padded vests of hybrid fibers, balancing tradition with practicality.
|
||||
- **Aetheric Technologies**: Post-Elysium, they adopted aetheric drives for silent propulsion and the _Aethernet_ for stealthy communication, tweaking designs to fit existing frameworks. Aetheric sensors enhance their vibration matrices, ensuring survival in a crowded prison-realm.
|
||||
|
||||
Their _Burrowhulks_ epitomize this ethos: armored, windowless spheres packed with overlapping systems—triple hulls, backup generators, tight corridors—housing entire Nestkin. Weapons favor stealth (sonic disruptors, micro-drones) over force, aligning with their preference for subterfuge over confrontation.
|
||||
|
||||
#### Historical Impact of Technology
|
||||
|
||||
Their technological journey shaped a history of endurance:
|
||||
|
||||
- **Tribal Era**: Vibration drums and geothermal vents fostered trade and survival, binding clans.
|
||||
- **Volcanic Winter**: Geothermal heat and fungal composites sustained them, while vibration matrices guided their exodus.
|
||||
- **Surface Era**: Metallurgy (iron, titanium) and chemistry built cities, computing unified governance, and plastics sealed habitats—each adopted slowly after proving its worth.
|
||||
- **Spacefaring Era**: Semiconductors and redundant designs birthed _Burrowhulks_, but fungal linings persisted for comfort, reflecting distrust of sterile synthetics.
|
||||
- **Elysium Era**: Aetheric tech accelerated adaptation, keeping them elusive amid aggressive rivals.
|
||||
|
||||
This conservatism delayed their FTL leap but ensured stability—change is a tool, not a goal, as they say: _“The new must prove itself worthy of the dark.”_
|
||||
|
||||
#### Social Structure
|
||||
|
||||
Ratfolk society is a high-trust hive of mutual dependence. Each _Krr’vax_ belongs to a _Nestkin_ (50–200 kin), led by a _Tchk’ra_ (Nest Elder) chosen for wisdom and loyalty. Consensus governs decisions, prioritizing stability over haste. Status stems from service—engineers, scouts, or weavers are as vital as leaders. Autophobia makes solitude intolerable; agoraphobia and photophobia bind them to cramped, dim spaces. Dissent is rare, exile unthinkable.
|
||||
|
||||
The _Council of Burrows_, a planetary assembly of Elders, manages trade and diplomacy. In Elysium, it negotiates with smaller factions, wary of corporate greed. Their unity is their strength, but it stifles rapid adaptation.
|
||||
|
||||
#### Culture
|
||||
|
||||
Rooted in survival, kinship, and darkness, Ratfolk culture reveres the _Deep Mother_ through _Shk’ra Vax_ (“Faith of the Hidden”). She guides them via seismic whispers, honored in chants and offerings of bones or minerals in subterranean shrines. _Turr’korr_ (“The Great Hiding”) celebrates the volcanic winter with storytelling circles. Prized possessions—_Whisker Talismans_, fungal tapestries, _Echo Stones_—are practical yet cherished, hoarding taboo.
|
||||
|
||||
Art is functional: woven patterns, harmonic tools, engraved maps. Entertainment is collective—chittering songs and tales of cunning escapes. Individualism is alien; every act serves the Nestkin.
|
||||
|
||||
#### Aesthetics
|
||||
|
||||
Their aesthetic is organic and utilitarian: rounded burrow-cities and _Burrowhulks_ of dark volcanic rock or titanium, lined with fungal composites. Dim bioluminescent fungi or aetheric glows cast comforting shadows. Clothing—padded vests, sensory harnesses, earthen cloaks—blends fungal fibers with plastics, adorned sparingly with whisker rings or ear clasps. To humans, it’s cluttered; to Ratfolk, it’s home.
|
||||
|
||||
#### Interactions in Elysium
|
||||
|
||||
In Elysium, the Ratfolk carve out dim asteroid warrens, trading vibration tech and espionage prowess with Adrasteia or Finch Cybernetics while dodging Zhestokost’s aggression. Humans find them endearing yet elusive, their diplomacy polite but layered with contingencies. They align cautiously with the Aya Collective, sharing communal ideals but skeptical of idealism. Their goal is survival, not conquest—whispering that Elysium is just another dark to master.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
The existence of [[Aether]]ic effects requiring a conscious mind to generate, and the ability to create [[Artificial General Intelligence]] which can effectively harness such effects, creates an incentive for corporations to develop [[Esper]] products using embodied AGI. Depending on the cost/benefit unit economics, this can result in ridiculous situations where a product as basic as a refrigerator, blender or vehicle may have an embodied AGI integrated into it in order to leverage Aetheric Fields to improve its energy efficiency.
|
||||
|
||||
Such intelligences cannot be generic, and must be organically grown within the substrate in which they will be deployed. This development process can require thousands of iterations before an intelligence is grown with a personality suitable to the task, which can then be copied onto every mass-production model. The cast-offs are usually destroyed, but often end up on the black market and have a well-earned reputation for unpredictable and destructive behavior.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
### Aetheric Drive
|
||||
|
||||
One of the inventions enabled by the existence of aether is a type of reactionless drive. Engineers discovered a mysterious loss of momentum in fast-moving systems when oscillating magnetic fields were arranged in a specific way. They discovered that the energy was being lost to the aether, and rapidly exploited this fact to build drives that could finally put an end to that bastard, Tsiolkovsky.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
### Aethernet
|
||||
|
||||
Among [[Aether]]’s interesting properties is its nonlocality, meaning that when it is perturbed, it is perturbed everywhere at once. This makes it the optimal medium for transmitting information across interstellar distances. There is one caveat, which is that aetheric signal attenuation is negligible, meaning everyone in Elysium must share a single common signal space that can easily become overwhelmed with traffic.
|
||||
|
||||
|
||||
#### The Queen’s Guard
|
||||
|
||||
Early attempts to use Aethernet were subject to a predictable Tragedy of the Commons. Companies building telecom infrastructure were in a constant battle to establish ever more powerful transmitters and multiplexing algorithms to have any hope of being heard over the saturated signal bands. (Pascalia Wells, CEO of NiteLife at the time, was famously reported to have noted that "screaming into the void had never been such an exercise in futility before people started receiving answers.") Eventually the various Megas controlling the galaxy had enough, and created the Guard.
|
||||
|
||||
This agency received a portion of their funding from each of the major players--the exact division of which became the subject of hot debate, and led to the establishment of the Fifth Year Calculations, a twice a decade bonanza of backroom dealings and tax fraud. Nevertheless, the levies were eventually decided, the Guard began licensing Aethernet frequency bands at huge cost, and soon the price and power of Aethernet transmitters plummeted to the point of being marketable towards individual pilots. The old multiplexing colossi immediately became terminally illegal--and the holy grail for pirates and hackers, whom the Queen’s Guard work tirelessly to exterminate.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
### Cognitive Interfacing
|
||||
|
||||
Elysium being designed as a prison for sapient civilizations, the regime of altered physics which permeates it and regulates its behavior, what we call the Aetheric Field, is fundamentally connected to the minds which inhabit it. Information density distorts the [[Aether]], and cognition interacts with it. The Timeless can use it at will to monitor the thoughts of sapients, and will sometimes snoop for their own entertainment. From time to time, they may even manifest to nudge the timeline or take pity on a suffering soul.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
##### Necrotech
|
||||
|
||||
Some corporations leverage the potential of [[Aether]]ic spirit creation in the ethically dubious practice of breeding genetically engineered human espers and harvesting them en masse to bind their souls to technological products. These can accomplish roughly the same effects as specialized embodied [[Artificial General Intelligence]], but require regular refueling if they aren’t designed to be disposable. They are more generalizable and faster to develop since they do not require the extensive work of iterating on embodied AGI minds, but greater per-unit costs means they lose out in economies of scale for mass-produced items.
|
||||
|
||||
As with other questionable and horrifying corporate practices, a lot of effort is spent in managing public perception of the technology. Only countercultural dissidents ever call it necrotech or their production lines necrofactories. The corporations themselves prefer to label them *Aetheric Resonance Interfaces*, making it sound like an advanced energy phenomenon rather than forced spirit entrapment. The media is flooded with propaganda explaining how necrotech is "completely humane" and "scientifically proven" to be non-conscious, while suppressing evidence of sentience in bound spirits.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue