Merge cuts loot-1-present and loot-2 (Soul verdicts cut-loot-1-present.s1, cut-loot-2.s2)
Conflicts: Behaviors.cs keeps Union(48, PickupData) and Union(49, LaunchSuppressorData); Zone.cs keeps the departure record on _entities with master's Visibility (emissions-heard); ContactRevealTests.cs keeps master's removal of the emission-departure test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
fbe05c7992
84 changed files with 1881 additions and 657 deletions
227
Assets/Scripts/Editor/FloatingItemsDrawnSmoke.cs
Normal file
227
Assets/Scripts/Editor/FloatingItemsDrawnSmoke.cs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
// Batch proof that ZoneRenderer draws Zone.FloatingItems and nothing else, in a play session with no scene of its own:
|
||||
// the pickup prefabs are the ones ARPG.unity's ZoneRenderer names (so the wiring is read here too), a zone is loaded
|
||||
// into a fresh ZoneRenderer, and the zone's own Release, step, expiry and TryPickUp are what move and remove the
|
||||
// pickups. Zone.Release adds exactly one ItemPickup, placed at the body's position; stepping the zone moves it with the
|
||||
// body; expiry and TryPickUp each destroy it; an item already in the zone when the renderer loads it is drawn too.
|
||||
//
|
||||
// Unity -batchmode -nographics -projectPath <project> -executeMethod FloatingItemsDrawnSmoke.Run -logFile <log>
|
||||
//
|
||||
// Do not pass -quit: Run enters play mode (a domain reload), the check runs there, and the Editor exits itself, 0 on
|
||||
// success and 1 on any failure. Destroy only takes effect in a play session, which is why this is not an edit-mode check.
|
||||
[InitializeOnLoad]
|
||||
public static class FloatingItemsDrawnSmoke
|
||||
{
|
||||
private const string Flag = "FloatingItemsDrawnSmoke.running";
|
||||
private const string Wiring = "FloatingItemsDrawnSmoke.wiring";
|
||||
private static IEnumerator _check;
|
||||
private static int _frames;
|
||||
private static string _work;
|
||||
|
||||
static FloatingItemsDrawnSmoke()
|
||||
{
|
||||
if (SessionState.GetBool(Flag, false)) EditorApplication.playModeStateChanged += OnPlayMode;
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
SessionState.SetString(Wiring, ReadWiring());
|
||||
SessionState.SetBool(Flag, true);
|
||||
EditorApplication.EnterPlaymode();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnPlayMode(PlayModeStateChange change)
|
||||
{
|
||||
if (change != PlayModeStateChange.EnteredPlayMode) return;
|
||||
_work = Path.Combine(Path.GetTempPath(), $"floating-items-smoke-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_work);
|
||||
_check = Check();
|
||||
EditorApplication.update += Step;
|
||||
}
|
||||
|
||||
private static void Step()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (++_frames > 3000) throw new InvalidOperationException("The check did not finish in 3000 frames.");
|
||||
if (_check.MoveNext()) return;
|
||||
Finish(0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
Finish(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Finish(int code)
|
||||
{
|
||||
EditorApplication.update -= Step;
|
||||
SessionState.SetBool(Flag, false);
|
||||
try { if (Directory.Exists(_work)) Directory.Delete(_work, true); } catch (Exception) { /* a temp directory */ }
|
||||
EditorApplication.Exit(code);
|
||||
}
|
||||
|
||||
private static void Require(bool condition, string what)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(what);
|
||||
}
|
||||
|
||||
private static IEnumerator Check()
|
||||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
|
||||
// The wiring: the scene's ZoneRenderer names an ItemPickup prefab in each of the four fields, and none of those
|
||||
// prefabs keeps a script that no longer exists.
|
||||
var wired = SessionState.GetString(Wiring, "").Split('|').Select(entry => entry.Split('=')).Select(pair =>
|
||||
(Field: pair[0], Pickup: pair[1] == "" ? null : AssetDatabase.LoadAssetAtPath<GameObject>(pair[1])?.GetComponent<ItemPickup>())).ToArray();
|
||||
Require(wired.Length == 4, "The wiring was not carried into the play session.");
|
||||
foreach (var (name, pickup) in wired)
|
||||
{
|
||||
Require(pickup != null, $"ZoneRenderer.{name} is not an ItemPickup in ARPG.unity.");
|
||||
Require(pickup.ScanLabel != null && pickup.ScanLabelContainer != null, $"{name}'s prefab has no scan label.");
|
||||
Require(!pickup.GetComponentsInChildren<Component>(true).Any(c => c == null), $"{name}'s prefab has a missing script.");
|
||||
}
|
||||
|
||||
var shape = new Shape(5, 5);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
var cargoShape = new Shape(4, 4);
|
||||
foreach (var cell in cargoShape.AllCoordinates) cargoShape[cell] = true;
|
||||
var oneCell = new Shape(1, 1);
|
||||
oneCell[new CultMath.int2(0, 0)] = true;
|
||||
var cache = AetheriaStores.Open(Path.Combine(_work, "smoke.cc"), catalogWritable: true);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
cache.Upsert(new HullData { Name = "Smoke Hub", HullType = HullType.Station, Shape = shape, Durability = 1000, Mass = 1000 });
|
||||
cache.Upsert(new CargoBayData { Name = "Smoke Crate", Shape = new Shape(), InteriorShape = cargoShape, Durability = 4 });
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Smoke Spare", Hardpoint = HardpointType.Tool, Shape = oneCell, Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
|
||||
});
|
||||
cache.Upsert(new SimpleCommodityData { Name = "Smoke Ore", Shape = oneCell, MaxStack = 100 });
|
||||
cache.FlushAsync().Wait();
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 8; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(cache, ledger, settings, _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), null, null);
|
||||
EquippableItem Make(string name, int lot) => new EquippableItem
|
||||
{
|
||||
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<EquippableItemData>(name)), Durability = 1, Lot = lot
|
||||
};
|
||||
|
||||
// A station with a crate takes the picked-up stack. It is not admitted: the zone holds no entity for the renderer to draw.
|
||||
var hubRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Smoke Hub"));
|
||||
var taker = new OrbitalEntity(items, zone, new EquippableItem { Data = hubRef, Durability = 1000, Lot = 1 }, default, new EntitySettings());
|
||||
Require(taker.TryEquip(Make("Smoke Crate", 2), new CultMath.int2(1, 1)), "The smoke station took no crate.");
|
||||
|
||||
// An item already in the zone when the renderer loads it: drawn from the load, not only from the add.
|
||||
var early = zone.Release(Make("Smoke Spare", 3), new float2(5, 6), float2.zero);
|
||||
|
||||
// The renderer's side. Inactive while it is configured, so it runs no Awake or Start of the scene's own.
|
||||
var rootObject = new GameObject("Smoke Zone Root");
|
||||
var rendererObject = new GameObject("Smoke Zone Renderer");
|
||||
rendererObject.SetActive(false);
|
||||
var renderer = rendererObject.AddComponent<ZoneRenderer>();
|
||||
renderer.ZoneRoot = rootObject.transform;
|
||||
foreach (var (name, pickup) in wired)
|
||||
typeof(ZoneRenderer).GetField(name).SetValue(renderer, pickup);
|
||||
renderer.ItemManager = items;
|
||||
renderer.SectorBrushes = new GameObject("Smoke Brushes").transform;
|
||||
renderer.SlimeGravityCamera = new GameObject("Smoke Gravity Camera").AddComponent<Camera>();
|
||||
var slimeObject = new GameObject("Smoke Slime");
|
||||
slimeObject.SetActive(false); // the real Slime needs its compute shader; LoadZone only sets its radius
|
||||
renderer.SlimeRenderer = slimeObject.AddComponent<Slime>();
|
||||
renderer.LoadZone(zone);
|
||||
yield return null;
|
||||
|
||||
ItemPickup[] Pickups() => rootObject.GetComponentsInChildren<ItemPickup>(true).Where(p => p != null).ToArray();
|
||||
void At(ItemPickup pickup, FloatingItem floating, string what)
|
||||
{
|
||||
var at = floating.Body.Position;
|
||||
var want = new Vector3(at.x, zone.GetHeight(at), at.y);
|
||||
Require(Vector3.Distance(pickup.transform.position, want) < 1e-3f,
|
||||
$"{what}: the pickup is at {pickup.transform.position}, the body at {want}.");
|
||||
}
|
||||
|
||||
Require(Pickups().Length == 1, $"load: {Pickups().Length} pickups for one existing floating item.");
|
||||
At(Pickups()[0], early, "load");
|
||||
|
||||
// Release adds exactly one pickup, at the body's position, bound to its item.
|
||||
var ore = new SimpleCommodity { Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<SimpleCommodityData>("Smoke Ore")), Quantity = 7 };
|
||||
var released = zone.Release(ore, new float2(3, 4), new float2(10, 0));
|
||||
yield return null;
|
||||
Require(Pickups().Length == 2, $"release: {Pickups().Length} pickups for two floating items.");
|
||||
var drawn = Pickups().Single(p => p.Item == ore);
|
||||
At(drawn, released, "release");
|
||||
|
||||
// Stepping the zone moves the sim body; the pickup follows it on its next update, with nothing of its own.
|
||||
var before = drawn.transform.position;
|
||||
zone.Update(.5f);
|
||||
yield return null;
|
||||
Require(released.Body.Position.x > 3.5f, "the sim body did not drift.");
|
||||
At(drawn, released, "step");
|
||||
Require(drawn.transform.position != before, "the pickup did not move with the body.");
|
||||
|
||||
// Pickup destroys exactly the picked-up item's pickup.
|
||||
Require(zone.TryPickUp(taker, released.Id), "the station refused a one-cell stack.");
|
||||
yield return null;
|
||||
yield return null;
|
||||
Require(drawn == null, "pickup: the pickup is still there.");
|
||||
Require(Pickups().Length == 1 && Pickups()[0].Item == early.Item, "pickup: the wrong pickup went.");
|
||||
|
||||
// Expiry destroys the pickup of an item nobody took.
|
||||
zone.Update(3600f); // far past any lifetime
|
||||
yield return null;
|
||||
yield return null;
|
||||
Require(zone.FloatingItems.Count == 0, "expiry: the item did not expire.");
|
||||
Require(Pickups().Length == 0, $"expiry: {Pickups().Length} pickups left.");
|
||||
|
||||
// A later release is drawn again, and clearing the renderer takes what is left.
|
||||
zone.Release(Make("Smoke Spare", 4), new float2(1, 1), float2.zero);
|
||||
yield return null;
|
||||
Require(Pickups().Length == 1, "re-release: not drawn.");
|
||||
renderer.ClearZone();
|
||||
yield return null;
|
||||
yield return null;
|
||||
Require(Pickups().Length == 0, "clear: pickups left after ClearZone.");
|
||||
Debug.Log("FLOATING_ITEMS_SMOKE ok");
|
||||
}
|
||||
|
||||
// What ARPG.unity's ZoneRenderer names in each pickup field, read from the saved scene before the play session (a
|
||||
// scene cannot be opened inside one) and carried across the domain reload as the prefabs' asset paths.
|
||||
private static string ReadWiring()
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Additive);
|
||||
try
|
||||
{
|
||||
var found = UnityEngine.Object.FindObjectsByType<ZoneRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None)
|
||||
.Where(r => r.gameObject.scene == scene).ToArray();
|
||||
Require(found.Length == 1, $"ARPG.unity has {found.Length} ZoneRenderers.");
|
||||
var wired = found[0];
|
||||
return string.Join("|", new[]
|
||||
{
|
||||
("SimpleCommodityPickup", wired.SimpleCommodityPickup), ("CompoundCommodityPickup", wired.CompoundCommodityPickup),
|
||||
("GearPickup", wired.GearPickup), ("WeaponPickup", wired.WeaponPickup)
|
||||
}.Select(f => f.Item1 + "=" + (f.Item2 == null ? "" : AssetDatabase.GetAssetPath(f.Item2))));
|
||||
}
|
||||
finally { EditorSceneManager.CloseScene(scene, true); }
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/FloatingItemsDrawnSmoke.cs.meta
Normal file
2
Assets/Scripts/Editor/FloatingItemsDrawnSmoke.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 67544a932f794a2458741c731ef04d11
|
||||
|
|
@ -64,10 +64,8 @@ public static class ImpactPresenterSmoke
|
|||
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Smoke Hull"));
|
||||
var ship = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 1 }, new EntitySettings());
|
||||
var other = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 2 }, new EntitySettings());
|
||||
zone.Entities.Add(ship);
|
||||
zone.Entities.Add(other);
|
||||
ship.Activate();
|
||||
other.Activate();
|
||||
zone.Admit(ship, piloted: false);
|
||||
zone.Admit(other, piloted: false);
|
||||
|
||||
// The scene's side: the tuning object with a pooled effect, and the game manager that names the player's ship.
|
||||
var feedbackObject = new GameObject("Smoke Impact Feedback");
|
||||
|
|
|
|||
75
Assets/Scripts/Editor/WireFloatingItems.cs
Normal file
75
Assets/Scripts/Editor/WireFloatingItems.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
// Strips the deleted integrator component from the pickup prefabs and points ZoneRenderer's four pickup fields in ARPG.unity at
|
||||
// each prefab's ItemPickup, then saves the prefabs and the scene. Idempotent: a prefab with no missing script is left as
|
||||
// it is, and the fields are set to the same values again.
|
||||
//
|
||||
// Unity -batchmode -projectPath <project> -executeMethod WireFloatingItems.Run -logFile <log>
|
||||
//
|
||||
// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure. The menu item runs the same wiring.
|
||||
public static class WireFloatingItems
|
||||
{
|
||||
private const string ScenePath = "Assets/Scenes/ARPG.unity";
|
||||
private const string PickupFolder = "Assets/Prefabs/RPG/Pickups/";
|
||||
// The field each prefab filled before this cut (read from ARPG.unity at the cut's base). Icosahedron fills none.
|
||||
private static readonly (string Field, string Prefab)[] Fields =
|
||||
{
|
||||
("SimpleCommodityPickup", "Tetrahedron"),
|
||||
("CompoundCommodityPickup", "Hexahedron"),
|
||||
("GearPickup", "Octahedron"),
|
||||
("WeaponPickup", "Dodecahedron"),
|
||||
};
|
||||
private static readonly string[] Prefabs = { "Tetrahedron", "Hexahedron", "Octahedron", "Dodecahedron", "Icosahedron" };
|
||||
|
||||
[MenuItem("Aetheria/Wire Floating Items")]
|
||||
public static void Menu() => Wire();
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
Wire();
|
||||
EditorApplication.Exit(0);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
EditorApplication.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Wire()
|
||||
{
|
||||
foreach (var name in Prefabs)
|
||||
{
|
||||
var path = PickupFolder + name + ".prefab";
|
||||
var contents = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(contents);
|
||||
if (contents.GetComponent<ItemPickup>() == null) throw new InvalidOperationException($"{path} has no ItemPickup.");
|
||||
if (removed > 0) PrefabUtility.SaveAsPrefabAsset(contents, path);
|
||||
Debug.Log($"WIRE_FLOATING_ITEMS {name} removed={removed}");
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(contents); }
|
||||
}
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
var renderer = UnityEngine.Object.FindFirstObjectByType<ZoneRenderer>();
|
||||
if (renderer == null) throw new InvalidOperationException("ARPG.unity has no ZoneRenderer.");
|
||||
var serialized = new SerializedObject(renderer);
|
||||
foreach (var (field, prefab) in Fields)
|
||||
{
|
||||
var pickup = AssetDatabase.LoadAssetAtPath<GameObject>(PickupFolder + prefab + ".prefab").GetComponent<ItemPickup>();
|
||||
var property = serialized.FindProperty(field) ?? throw new InvalidOperationException($"ZoneRenderer has no {field}.");
|
||||
property.objectReferenceValue = pickup;
|
||||
}
|
||||
serialized.ApplyModifiedProperties();
|
||||
EditorSceneManager.MarkSceneDirty(scene);
|
||||
if (!EditorSceneManager.SaveScene(scene)) throw new InvalidOperationException("The scene did not save.");
|
||||
Debug.Log("WIRE_FLOATING_ITEMS ok");
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/WireFloatingItems.cs.meta
Normal file
2
Assets/Scripts/Editor/WireFloatingItems.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9a56d7dc8471d224f9bed434b39f7c87
|
||||
|
|
@ -739,7 +739,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
if (CurrentEntity != null)
|
||||
{
|
||||
CurrentEntity.Deactivate();
|
||||
CurrentEntity.Zone.Entities.Remove(CurrentEntity);
|
||||
CurrentEntity.Zone.Remove(CurrentEntity);
|
||||
CurrentEntity.Zone = Zone;
|
||||
Zone.Admit(CurrentEntity, piloted: false);
|
||||
}
|
||||
|
|
@ -1332,9 +1332,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
TargetShieldsFill.fillAmount = target.Shield == null ? 0 : lerp(.25f, .75f, target.Shield.Progress);
|
||||
}
|
||||
|
||||
var tractorPower = Input.Player.TractorBeam.ReadValue<float>();
|
||||
CurrentEntity.TractorPower =
|
||||
saturate(CurrentEntity.TractorPower + sign(tractorPower - CurrentEntity.TractorPower) * Time.deltaTime * 2);
|
||||
CurrentEntity.TractorCommand = Input.Player.TractorBeam.ReadValue<float>();
|
||||
}
|
||||
}
|
||||
// One clock steps the zone, paused or not: a paused frame feeds it no real time.
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ using UnityEngine;
|
|||
public class CapabilityPresentationBinder : MonoBehaviour
|
||||
{
|
||||
private readonly List<IDisposable> _subscriptions = new List<IDisposable>();
|
||||
private Func<int, Transform> _grabTargetResolver;
|
||||
private Func<int, Vector3> _grabInitialVelocityResolver;
|
||||
private Func<FloatingBodyId, Transform> _grabTargetResolver;
|
||||
private Func<FloatingBodyId, Vector3> _grabInitialVelocityResolver;
|
||||
|
||||
// Subscribes every presenter interface implemented by components on this object or its children to
|
||||
// the matching stream of `events`. `grabTargetResolver`/`grabInitialVelocityResolver` resolve a
|
||||
// GrabEvent's plain target handle to the live Transform/velocity a grab presenter needs; the owner
|
||||
// GrabEvent's target id to the live Transform/velocity a grab presenter needs; the owner
|
||||
// of `events` supplies them (the test scene today; the real pickup capability's caller later).
|
||||
public void Bind(CapabilityEvents events, Func<int, Transform> grabTargetResolver = null,
|
||||
Func<int, Vector3> grabInitialVelocityResolver = null)
|
||||
public void Bind(CapabilityEvents events, Func<FloatingBodyId, Transform> grabTargetResolver = null,
|
||||
Func<FloatingBodyId, Vector3> grabInitialVelocityResolver = null)
|
||||
{
|
||||
Unbind();
|
||||
if (events == null) return;
|
||||
|
|
@ -40,8 +40,8 @@ public class CapabilityPresentationBinder : MonoBehaviour
|
|||
var p = presenter;
|
||||
_subscriptions.Add(events.Grab.Subscribe(e =>
|
||||
{
|
||||
var target = _grabTargetResolver?.Invoke(e.TargetHandle);
|
||||
var velocity = _grabInitialVelocityResolver?.Invoke(e.TargetHandle) ?? Vector3.zero;
|
||||
var target = _grabTargetResolver?.Invoke(e.Target);
|
||||
var velocity = _grabInitialVelocityResolver?.Invoke(e.Target) ?? Vector3.zero;
|
||||
p.Grab(e, target, velocity);
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public interface IAbsorbPresenter
|
|||
}
|
||||
|
||||
// Grab needs a live Transform to animate, which the engine-free GrabEvent cannot carry (its
|
||||
// TargetHandle is a plain identity, not a Unity reference). The binder resolves the handle through a
|
||||
// Target is a plain identity, not a Unity reference). The binder resolves the id through a
|
||||
// delegate supplied by the event source's owner and hands the presenter the result, along with the
|
||||
// initial velocity the target was already moving with (also owner-supplied, for the same reason).
|
||||
public interface IGrabPresenter
|
||||
|
|
|
|||
|
|
@ -301,27 +301,6 @@ public class EntityInstance : MonoBehaviour
|
|||
{
|
||||
if (!this) return;
|
||||
_destroyed = true;
|
||||
foreach (var gear in Entity.Equipment)
|
||||
{
|
||||
if (gear != Entity.EquippedHull && Random.value < ZoneRenderer.Settings.LootDropProbability)
|
||||
{
|
||||
ZoneRenderer.DropItem(
|
||||
Entity.Position.ToUnity(),
|
||||
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
|
||||
gear.EquippableItem);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cargo in Entity.CargoBays)
|
||||
{
|
||||
foreach (var item in cargo.Cargo.Keys)
|
||||
{
|
||||
ZoneRenderer.DropItem(
|
||||
Entity.Position.ToUnity(),
|
||||
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
|
||||
item);
|
||||
}
|
||||
}
|
||||
if (DestroyEffect != null)
|
||||
{
|
||||
var t = Instantiate(DestroyEffect).transform;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ using UnityEngine;
|
|||
public class GameSettings : ScriptableObject
|
||||
{
|
||||
public string StartingHullName = "Longinus";
|
||||
public float PickupLifetime = 30;
|
||||
public float LootDropProbability = .25f;
|
||||
public float LootDropVelocity = 25;
|
||||
public float HeatstrokePhasingFloor = 0;
|
||||
public float HeatstrokePhasingFrequency = 5;
|
||||
public float WormholeDistanceRatio;
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
using System;
|
||||
using CultMath;
|
||||
using CultMath.UnityBridge;
|
||||
using UnityEngine;
|
||||
using static CultMath.math;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
public class GridObject : MonoBehaviour
|
||||
{
|
||||
public float RotationSpeed;
|
||||
public float GridOffset;
|
||||
public float GridAttraction;
|
||||
public float Gravity;
|
||||
public float Drag = .1f;
|
||||
public float LaunchDrag;
|
||||
|
||||
private float2 _selfVelocity;
|
||||
private float _timeOffset;
|
||||
|
||||
public Zone Zone { get; set; }
|
||||
public Vector3 Velocity { get; set; }
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_timeOffset = Random.value * 100;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
var t = transform;
|
||||
t.localRotation = Quaternion.Euler(sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, cos(Time.time - _timeOffset * RotationSpeed) * 90);
|
||||
|
||||
if (Zone == null) return;
|
||||
|
||||
var position = t.position;
|
||||
var gridHeight = Zone.GetHeight(position.Flatland().ToCultMath()) + GridOffset;
|
||||
Velocity += Vector3.up * (sign(gridHeight - position.y) * GridAttraction * Time.deltaTime);
|
||||
Velocity *= max(0, 1 - LaunchDrag * Time.deltaTime);
|
||||
var normal = Zone.GetNormal(position.Flatland().ToCultMath());
|
||||
var force = new float2(normal.x, normal.z);
|
||||
var forceMagnitude = lengthsq(force);
|
||||
if (forceMagnitude > .001f)
|
||||
{
|
||||
var fa = 1 / (1 - forceMagnitude) - 1;
|
||||
_selfVelocity += normalize(force) * Zone.Settings.GravityStrength * fa * Gravity;
|
||||
}
|
||||
|
||||
_selfVelocity *= max(0, 1 - Drag * Time.deltaTime);
|
||||
t.position = position + (Velocity + new Vector3(_selfVelocity.x, 0, _selfVelocity.y)) * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 737f47c158214f0995ba2835f040ed96
|
||||
timeCreated: 1618441439
|
||||
|
|
@ -4,9 +4,15 @@ using CultMath;
|
|||
using CultMath.UnityBridge;
|
||||
using UnityEngine;
|
||||
using static CultMath.math;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
// How one floating item looks. Zone owns the item's every fact (existence, position, expiry, pickup); once bound,
|
||||
// this reads the latest sim body and draws, spun for show, and writes nothing back (rulings unity-presents-only-r3
|
||||
// and sim-speed-presentation). An unbound pickup (FieldTester's test scene) leaves its transform alone.
|
||||
public class ItemPickup : MonoBehaviour
|
||||
{
|
||||
public float RotationSpeed = 1f;
|
||||
public float GridOffset;
|
||||
public float LabelFadeDuration = .5f;
|
||||
public float LabelPersistDuration = 3;
|
||||
public float LabelDisplayAngle = 15;
|
||||
|
|
@ -15,14 +21,38 @@ public class ItemPickup : MonoBehaviour
|
|||
public TextMeshPro ScanLabel;
|
||||
|
||||
public ItemInstance Item { get; set; }
|
||||
public ZoneRenderer ZoneRenderer { get; set; }
|
||||
public float3 ViewOrigin { get; set; }
|
||||
public float3 ViewDirection { get; set; }
|
||||
|
||||
private float _displayTime;
|
||||
private float _timeOffset;
|
||||
private Zone _zone;
|
||||
private FloatingItem _floating;
|
||||
|
||||
public void Bind(Zone zone, FloatingItem floating)
|
||||
{
|
||||
_zone = zone;
|
||||
_floating = floating;
|
||||
Item = floating.Item;
|
||||
_timeOffset = Random.value * 100;
|
||||
Place();
|
||||
}
|
||||
|
||||
private void Place()
|
||||
{
|
||||
var p = _floating.Body.Position;
|
||||
transform.position = new Vector3(p.x, _zone.GetHeight(p) + GridOffset, p.y);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_floating != null)
|
||||
{
|
||||
Place();
|
||||
transform.localRotation = Quaternion.Euler(
|
||||
sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, cos(Time.time - _timeOffset * RotationSpeed) * 90);
|
||||
}
|
||||
|
||||
var diff = transform.position.ToCultMath() - ViewOrigin;
|
||||
var toThis = normalize(diff);
|
||||
var viewAngle = acos(dot(toThis, ViewDirection)) * Mathf.Rad2Deg;
|
||||
|
|
@ -34,9 +64,4 @@ public class ItemPickup : MonoBehaviour
|
|||
ScanLabel.color = c;
|
||||
ScanLabelContainer.rotation = Quaternion.LookRotation((-toThis).ToUnity());
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ZoneRenderer?.DestroyLoot(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,40 +52,6 @@ public class ShieldManager : MonoBehaviour, IAbsorbPresenter
|
|||
Debug.LogWarning("ShieldManager has no ShieldEnvelope; falling back to its own transform for the hit direction.", this);
|
||||
}
|
||||
|
||||
private void OnCollisionEnter(Collision other)
|
||||
{
|
||||
var otherShield = other.collider.GetComponent<ShieldManager>();
|
||||
if (!otherShield)
|
||||
{
|
||||
var gridObject = other.collider.GetComponent<GridObject>();
|
||||
if (gridObject)
|
||||
{
|
||||
var itemPickup = other.collider.GetComponent<ItemPickup>();
|
||||
if (itemPickup)
|
||||
{
|
||||
if (Entity.CargoBays.Any(c => c.TryStore(itemPickup.Item)))
|
||||
{
|
||||
// TODO: Pickup notification!
|
||||
Destroy(itemPickup.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Pickup failed notification!
|
||||
var cp = other.GetContact(0);
|
||||
gridObject.Velocity += cp.normal * 25;
|
||||
Debug.Log("Attempted item pickup, but no space in cargo bay!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Shield collision occurred with grid object, but other collider isn't an item pickup!");
|
||||
}
|
||||
}
|
||||
else
|
||||
Debug.Log("Shield collision occurred, but other collider isn't a shield or a grid object!");
|
||||
}
|
||||
}
|
||||
|
||||
// A shot the shield took ripples it where the shot landed, drawn on the hull as it is drawn. The hull layer is
|
||||
// ImpactPresenter's.
|
||||
public void Absorb(AbsorbEvent e)
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class TimedDestroy : MonoBehaviour
|
||||
{
|
||||
public float Duration;
|
||||
|
||||
private float _startTime;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_startTime = Time.time;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if(Time.time - _startTime > Duration) Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5d5539a23b814254b038a16f32f1d8a9
|
||||
timeCreated: 1618453298
|
||||
|
|
@ -14,20 +14,9 @@ public class TractorBeam : MonoBehaviour
|
|||
public void Update()
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(Direction);
|
||||
// The beam draws nothing until it presents a sim grab (loot-3): no particles, and no pull; the simulation
|
||||
// decides every floating item's motion.
|
||||
var emission = ParticleSystem.emission;
|
||||
if (Power > .01f)
|
||||
{
|
||||
var objectFound = false;
|
||||
foreach (var o in Physics.SphereCastAll(transform.position, Radius, Direction, Distance, 1 << 21))
|
||||
{
|
||||
var gridObject = o.collider.GetComponent<GridObject>();
|
||||
if (gridObject == null) continue;
|
||||
objectFound = true;
|
||||
gridObject.Velocity += Vector3.Normalize(transform.position - gridObject.transform.position) * (Traction * Time.deltaTime);
|
||||
}
|
||||
|
||||
emission.rateOverTimeMultiplier = objectFound ? Power : 0;
|
||||
}
|
||||
else emission.rateOverTimeMultiplier = 0;
|
||||
emission.rateOverTimeMultiplier = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -189,6 +189,7 @@ public interface IPowerConsumer
|
|||
Union(45, typeof(MunitionSeekerData)),
|
||||
Union(46, typeof(MunitionControlData)),
|
||||
Union(47, typeof(MunitionFuelData)),
|
||||
Union(48, typeof(PickupData)),
|
||||
Union(49, typeof(LaunchSuppressorData)),
|
||||
JsonObject(MemberSerialization.OptIn)]
|
||||
public abstract class BehaviorData
|
||||
|
|
|
|||
171
Assets/Scripts/ServerShared/Behaviors/Pickup.cs
Normal file
171
Assets/Scripts/ServerShared/Behaviors/Pickup.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
// The tractor beam: the capability that lets a ship collect floating items. It spins its beam up in sim time toward
|
||||
// the pilot's command, and once the beam is up claims the free item nearest its axis and runs one timed grab
|
||||
// (extend, envelop, pull) that ends in Zone.TryPickUp, the one commit of a pickup. Its stats are flat
|
||||
// (Min == Max), as the other behaviours that carry their own timings.
|
||||
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
|
||||
public class PickupData : BehaviorData
|
||||
{
|
||||
// How far along the beam's axis an item can be claimed, and how far off the axis.
|
||||
[Inspectable, JsonProperty("range"), Key(1), RuntimeInspectable]
|
||||
public PerformanceStat Range = new PerformanceStat();
|
||||
|
||||
[Inspectable, JsonProperty("beamRadius"), Key(2), RuntimeInspectable]
|
||||
public PerformanceStat BeamRadius = new PerformanceStat();
|
||||
|
||||
// The spin-up (0..1) needed to claim an item, and to keep a grab going.
|
||||
[Inspectable, JsonProperty("grabPower"), Key(3), RuntimeInspectable]
|
||||
public PerformanceStat GrabPower = new PerformanceStat();
|
||||
|
||||
// Seconds of each phase of a grab.
|
||||
[Inspectable, JsonProperty("extendTime"), Key(4), RuntimeInspectable]
|
||||
public PerformanceStat ExtendTime = new PerformanceStat();
|
||||
|
||||
[Inspectable, JsonProperty("envelopTime"), Key(5), RuntimeInspectable]
|
||||
public PerformanceStat EnvelopTime = new PerformanceStat();
|
||||
|
||||
[Inspectable, JsonProperty("pullTime"), Key(6), RuntimeInspectable]
|
||||
public PerformanceStat PullTime = new PerformanceStat();
|
||||
|
||||
// Spin-up and spin-down, in power per sim second.
|
||||
[Inspectable, JsonProperty("spinRate"), Key(7), RuntimeInspectable]
|
||||
public PerformanceStat SpinRate = new PerformanceStat();
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
return new Pickup(this, item);
|
||||
}
|
||||
|
||||
// A beam is a piece of gear the pilot holds on; no consumable carries one.
|
||||
public override Behavior CreateInstance(ConsumableItemEffect item) =>
|
||||
throw new NotSupportedException("pickup: gear only");
|
||||
}
|
||||
|
||||
// Owns this ship's beam power and its one grab. Power is written only here; Zone owns the held item's pose.
|
||||
public class Pickup : Behavior, IAlwaysUpdatedBehavior
|
||||
{
|
||||
private readonly PickupData _data;
|
||||
// Power and the grab clock are kept in double so a fixed tick count lands on the same side of a boundary
|
||||
// whatever dt the zone steps by.
|
||||
private double _power;
|
||||
private bool _grabbing;
|
||||
private FloatingBodyId _target;
|
||||
private double _elapsed;
|
||||
// How many of Envelop and Pull have been published so far (0, 1 or 2).
|
||||
private int _published;
|
||||
|
||||
public Pickup(PickupData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
|
||||
// The beam's spun-up state in [0, 1]: what the pilot's command has driven it to.
|
||||
public float Power => (float) _power;
|
||||
|
||||
public void Update(float dt)
|
||||
{
|
||||
var active = Item.Active.Value;
|
||||
var command = active ? Math.Clamp(Entity.TractorCommand, 0f, 1f) : 0f;
|
||||
var step = (double) Evaluate(_data.SpinRate) * dt;
|
||||
_power = Math.Clamp(_power + Math.Clamp(command - _power, -step, step), 0.0, 1.0);
|
||||
|
||||
var zone = Entity.Zone;
|
||||
var grabPower = Evaluate(_data.GrabPower);
|
||||
var extend = (double) Evaluate(_data.ExtendTime);
|
||||
var envelop = (double) Evaluate(_data.EnvelopTime);
|
||||
var pull = (double) Evaluate(_data.PullTime);
|
||||
var range = Evaluate(_data.Range);
|
||||
var radius = Evaluate(_data.BeamRadius);
|
||||
var origin = Entity.Position.xz;
|
||||
|
||||
if (!_grabbing)
|
||||
{
|
||||
if (!active || _power < grabPower) return;
|
||||
var item = Select(zone, origin, range, radius);
|
||||
if (item == null || !zone.TryHold(Entity, item.Id)) return;
|
||||
_grabbing = true;
|
||||
_target = item.Id;
|
||||
_elapsed = 0;
|
||||
_published = 0;
|
||||
Publish(GrabPhase.Started, item, 0);
|
||||
Publish(GrabPhase.Extend, item, (float) extend);
|
||||
return;
|
||||
}
|
||||
|
||||
var held = zone.FloatingItems.FirstOrDefault(f => f.Id.Equals(_target));
|
||||
if (held == null || !ReferenceEquals(held.Holder, Entity))
|
||||
{
|
||||
_grabbing = false;
|
||||
Entity.Capabilities.PublishGrab(new GrabEvent(_target, GrabPhase.Cancelled, float3(0), 0));
|
||||
return;
|
||||
}
|
||||
|
||||
var pulling = _elapsed >= extend + envelop;
|
||||
if (!active || _power < grabPower || (!pulling && !InBeam(held.Body.Position, origin, range, radius, out _)))
|
||||
{
|
||||
_grabbing = false;
|
||||
zone.Free(_target);
|
||||
Publish(GrabPhase.Cancelled, held, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
_elapsed += dt;
|
||||
if (_published < 1 && _elapsed >= extend) { _published = 1; Publish(GrabPhase.Envelop, held, (float) envelop); }
|
||||
if (_published < 2 && _elapsed >= extend + envelop) { _published = 2; Publish(GrabPhase.Pull, held, (float) pull); }
|
||||
if (_published == 2)
|
||||
zone.SetHeldProgress(_target, pull > 0 ? (float) ((_elapsed - extend - envelop) / pull) : 1f);
|
||||
if (_elapsed < extend + envelop + pull) return;
|
||||
|
||||
_grabbing = false;
|
||||
if (zone.TryPickUp(Entity, _target)) Publish(GrabPhase.Completed, held, 0);
|
||||
else
|
||||
{
|
||||
zone.Free(_target);
|
||||
Publish(GrabPhase.Cancelled, held, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The one beam region: within Range along the aim axis (never behind) and BeamRadius off it. Selection claims
|
||||
// from it and the grab holds on to its item only while the item stays in it. A zero aim has no region.
|
||||
private bool InBeam(float2 position, float2 origin, float range, float radius, out float off)
|
||||
{
|
||||
off = 0;
|
||||
var aim = Entity.Aim.xz;
|
||||
if (lengthsq(aim) <= 0) return false;
|
||||
var axis = normalize(aim);
|
||||
var to = position - origin;
|
||||
var along = dot(to, axis);
|
||||
off = length(to - axis * along);
|
||||
return along >= 0 && along <= range && off <= radius;
|
||||
}
|
||||
|
||||
// The free item nearest the beam axis within the beam region that some cargo bay can store whole (the bay's own
|
||||
// TryFindSpace, the test TryStore opens with); ties go to the lowest id. A zero aim selects nothing.
|
||||
private FloatingItem Select(Zone zone, float2 origin, float range, float radius)
|
||||
{
|
||||
FloatingItem best = null;
|
||||
var bestOff = float.MaxValue;
|
||||
foreach (var floating in zone.FloatingItems)
|
||||
{
|
||||
if (floating.Holder != null) continue;
|
||||
if (!InBeam(floating.Body.Position, origin, range, radius, out var off)) continue;
|
||||
if (!Entity.CargoBays.Any(bay => bay.TryFindSpace(floating.Item))) continue;
|
||||
if (off < bestOff || (off == bestOff && floating.Id.Value < best.Id.Value))
|
||||
{
|
||||
best = floating;
|
||||
bestOff = off;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private void Publish(GrabPhase phase, FloatingItem item, float duration) =>
|
||||
Entity.Capabilities.PublishGrab(new GrabEvent(item.Id, phase, float3(item.Body.Position.x, Entity.Position.y, item.Body.Position.y), duration));
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Behaviors/Pickup.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Behaviors/Pickup.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ac7678d240564331be7486588d28c5ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -52,19 +52,18 @@ public enum GrabPhase
|
|||
Cancelled
|
||||
}
|
||||
|
||||
// The target is identified by a plain handle, not a Unity type: the simulation will own the real
|
||||
// target identity (a loot drop or item instance id) once the pickup capability exists. Today's only
|
||||
// publisher (the FieldShieldTest scene) hands out its own handles; nothing here interprets them.
|
||||
// One phase of a grab. The target is the floating item the Pickup behaviour holds (the FieldShieldTest scene
|
||||
// publishes its own handles as ids); nothing here interprets it.
|
||||
public readonly struct GrabEvent
|
||||
{
|
||||
public readonly int TargetHandle;
|
||||
public readonly FloatingBodyId Target;
|
||||
public readonly GrabPhase Phase;
|
||||
public readonly float3 TargetPosition;
|
||||
public readonly float PhaseDuration;
|
||||
|
||||
public GrabEvent(int targetHandle, GrabPhase phase, float3 targetPosition, float phaseDuration)
|
||||
public GrabEvent(FloatingBodyId target, GrabPhase phase, float3 targetPosition, float phaseDuration)
|
||||
{
|
||||
TargetHandle = targetHandle;
|
||||
Target = target;
|
||||
Phase = phase;
|
||||
TargetPosition = targetPosition;
|
||||
PhaseDuration = phaseDuration;
|
||||
|
|
|
|||
|
|
@ -153,7 +153,11 @@ public abstract class Entity
|
|||
|
||||
public bool OverrideShutdown { get; set; }
|
||||
|
||||
public float TractorPower { get; set; }
|
||||
// The pilot's tractor command (0..1); the Pickup behaviour spins its beam toward it.
|
||||
public float TractorCommand;
|
||||
|
||||
// The beam's spun-up state: Pickup's, or 0 without one.
|
||||
public float TractorPower => GetBehavior<Pickup>()?.Power ?? 0f;
|
||||
|
||||
public bool Active
|
||||
{
|
||||
|
|
@ -1549,7 +1553,7 @@ public abstract class Entity
|
|||
{
|
||||
bay.DockedShip = ship;
|
||||
ship.SetParent(this);
|
||||
Zone.Entities.Remove(ship);
|
||||
Zone.Remove(ship);
|
||||
ship.Deactivate();
|
||||
ship.Docked.OnNext(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ public struct KinematicBody
|
|||
}
|
||||
|
||||
// An item adrift in a zone: a death drop or jettisoned cargo. Zone.Release is its only adder; Zone's floating-item
|
||||
// step (expiry) and TryPickUp are its only removers. Runtime state, never saved.
|
||||
// step (expiry) and TryPickUp are its only removers. Zone.TryHold and Free are the only writers of Holder, the
|
||||
// entity grabbing it with a tractor beam; a held item is placed by Zone, not drifted. Runtime state, never saved.
|
||||
public sealed class FloatingItem
|
||||
{
|
||||
public FloatingBodyId Id;
|
||||
|
|
@ -50,4 +51,9 @@ public sealed class FloatingItem
|
|||
public KinematicBody Body;
|
||||
public double ReleasedAt;
|
||||
public float Lifetime;
|
||||
// The entity holding it, or null when it floats free; HeldFrom is where it was claimed and HeldProgress how far
|
||||
// along its pull to the holder (0..1).
|
||||
public Entity Holder;
|
||||
public float2 HeldFrom;
|
||||
public float HeldProgress;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public class LoadoutGenerator
|
|||
var entity = new Ship(ItemManager, null, hull, ItemManager.GameplaySettings.DefaultEntitySettings);
|
||||
entity.Faction = Faction;
|
||||
OutfitEntity(entity);
|
||||
EquipPickup(entity);
|
||||
StockConsumables(entity, ShipConsumableChance, ShipConsumableUnits);
|
||||
return EntitySerializer.Pack(entity);
|
||||
}
|
||||
|
|
@ -287,6 +288,24 @@ public class LoadoutGenerator
|
|||
FillInterior(entity);
|
||||
}
|
||||
|
||||
// Every ship carries a tractor beam: picking up needs a pickup behaviour (scenarios ruling, "everyone gets tractor
|
||||
// beams"). It goes in after the shared outfitting, which stations and turrets also run and which adds none, into
|
||||
// whatever interior the hull has left; a hull with no cell for one cannot be generated.
|
||||
private void EquipPickup(Entity entity)
|
||||
{
|
||||
var emptyShape = entity.UnoccupiedSpace;
|
||||
var (product, design) = RandomProduct<GearData>(2, item =>
|
||||
item.Behaviors.Any(b => b is PickupData) &&
|
||||
item.Shape.FitsWithin(emptyShape, out _, out _), required: true);
|
||||
if (design == null) throw new InvalidLoadoutException("No compatible tractor beam found for ship!");
|
||||
|
||||
design.Shape.FitsWithin(emptyShape, out var rotation, out var position);
|
||||
var beam = ItemManager.CreateInstance(product) as EquippableItem;
|
||||
beam.Rotation = rotation;
|
||||
if (!entity.TryEquip(beam, position))
|
||||
throw new InvalidLoadoutException("Failed to equip selected tractor beam!");
|
||||
}
|
||||
|
||||
private void EquipHardpoints(Entity entity)
|
||||
{
|
||||
var hullData = ItemManager.GetData(entity.Hull) as HullData;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,29 @@ using float2 = CultMath.float2;
|
|||
public class Zone
|
||||
{
|
||||
public Action<string> Log;
|
||||
public ReactiveCollection<Entity> Entities = new ReactiveCollection<Entity>();
|
||||
|
||||
// Membership, in admission order. Admit and Remove are the only writers; everyone else reads this view, which has
|
||||
// no mutating member, so the death watch below follows membership by construction.
|
||||
private readonly ReactiveCollection<Entity> _entities = new ReactiveCollection<Entity>();
|
||||
public MemberView Entities { get; }
|
||||
|
||||
public sealed class MemberView : IReadOnlyReactiveCollection<Entity>
|
||||
{
|
||||
private readonly ReactiveCollection<Entity> _members;
|
||||
internal MemberView(ReactiveCollection<Entity> members) => _members = members;
|
||||
public int Count => _members.Count;
|
||||
public Entity this[int index] => _members[index];
|
||||
public int IndexOf(Entity entity) => _members.IndexOf(entity);
|
||||
public bool Contains(Entity entity) => _members.Contains(entity);
|
||||
public IEnumerator<Entity> GetEnumerator() => _members.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public UniRx.IObservable<CollectionAddEvent<Entity>> ObserveAdd() => _members.ObserveAdd();
|
||||
public UniRx.IObservable<int> ObserveCountChanged(bool notifyCurrentCount = false) => _members.ObserveCountChanged(notifyCurrentCount);
|
||||
public UniRx.IObservable<CollectionMoveEvent<Entity>> ObserveMove() => _members.ObserveMove();
|
||||
public UniRx.IObservable<CollectionRemoveEvent<Entity>> ObserveRemove() => _members.ObserveRemove();
|
||||
public UniRx.IObservable<CollectionReplaceEvent<Entity>> ObserveReplace() => _members.ObserveReplace();
|
||||
public UniRx.IObservable<Unit> ObserveReset() => _members.ObserveReset();
|
||||
}
|
||||
|
||||
// Laid mines, in lay order. Zone owns their existence: Lay is the one adder, the mine step the one remover,
|
||||
// and every removal is a blast. Runtime state, never saved.
|
||||
|
|
@ -26,7 +48,8 @@ public class Zone
|
|||
public ReactiveCollection<VapourCloud> Clouds = new ReactiveCollection<VapourCloud>();
|
||||
|
||||
// Floating items (death drops, jettisoned cargo), in release order. Zone owns their existence: Release is the one
|
||||
// adder; StepFloatingItems (expiry) and TryPickUp are the only removers. Runtime state, never saved.
|
||||
// adder; StepFloatingItems (expiry) and TryPickUp are the only removers. TryHold, Free and Remove-of-the-holder
|
||||
// are the only writers of FloatingItem.Holder. Runtime state, never saved.
|
||||
public ReactiveCollection<FloatingItem> FloatingItems = new ReactiveCollection<FloatingItem>();
|
||||
public Dictionary<CultRecordKey, BodyData> Planets = new Dictionary<CultRecordKey, BodyData>();
|
||||
public Dictionary<CultRecordKey, Planet> PlanetInstances = new Dictionary<CultRecordKey, Planet>();
|
||||
|
|
@ -105,30 +128,13 @@ public class Zone
|
|||
// The death ordinal of the loot dice: the nth death in this zone rolls on SimulationDice.For(CombatSeed, LootStream, n).
|
||||
private uint _deaths;
|
||||
|
||||
// One death subscription per member entity, owned here: a membership record and its subscription are created
|
||||
// together, and only when the entity is not already a member. Leaving Entities (Remove) or resetting it (Clear)
|
||||
// disposes it; nothing else touches it.
|
||||
// One death subscription per member entity: Admit creates it together with the membership, Remove disposes it
|
||||
// together with the membership, and nothing else touches either.
|
||||
private readonly Dictionary<Entity, IDisposable> _deathWatches = new Dictionary<Entity, IDisposable>();
|
||||
|
||||
private void WatchDeath(Entity entity)
|
||||
{
|
||||
if (_deathWatches.ContainsKey(entity)) return;
|
||||
_deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Entities.Remove(entity); entity.Deactivate(); }));
|
||||
}
|
||||
|
||||
private void UnwatchDeath(Entity entity)
|
||||
{
|
||||
if (_deathWatches.Remove(entity, out var watch)) watch.Dispose();
|
||||
}
|
||||
|
||||
private void UnwatchAll()
|
||||
{
|
||||
foreach (var watch in _deathWatches.Values) watch.Dispose();
|
||||
_deathWatches.Clear();
|
||||
}
|
||||
|
||||
public Zone(ItemManager itemManager, PlanetSettings settings, ZonePack pack, GalaxyZone galaxyZone, Galaxy galaxy)
|
||||
{
|
||||
Entities = new MemberView(_entities);
|
||||
_time = pack.Time;
|
||||
GalaxyZone = galaxyZone;
|
||||
Galaxy = galaxy;
|
||||
|
|
@ -144,11 +150,8 @@ public class Zone
|
|||
// ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone.
|
||||
// The death watch lives exactly as long as the entity's membership in this zone: installed on admission,
|
||||
// disposed on removal (death, dock, jump), so an entity admitted again never holds a second one.
|
||||
Entities.ObserveAdd().Subscribe(add => WatchDeath(add.Value));
|
||||
Entities.ObserveRemove().Subscribe(remove => UnwatchDeath(remove.Value));
|
||||
Entities.ObserveReset().Subscribe(_ => UnwatchAll());
|
||||
// The one record of a removal for observers (ruling reveal-ends-on-perception): read by the next Sense pass.
|
||||
Entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz,
|
||||
_entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz,
|
||||
remove.Value.Visibility)));
|
||||
|
||||
Targets.Add(new EntityTargets());
|
||||
|
|
@ -205,17 +208,32 @@ public class Zone
|
|||
// TODO: Associate planets with stored entities for planetary colonies
|
||||
}
|
||||
|
||||
// The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of Entities.Add and the only
|
||||
// The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of membership and the only
|
||||
// creator of agents. Construction, new-run staging (RunStart), warp arrival, undock and the console's spawned
|
||||
// turret all join through here. A piloted entity gets the zone's Minion; an unpiloted one gets no agent at all.
|
||||
// Only a ship can be piloted. The Death subscription above covers every join, whichever caller admits it.
|
||||
// Only a ship can be piloted. The death watch is installed here, for every join, whichever caller admits it.
|
||||
// An entity that is already a member is refused: it would hold a second watch and a second agent.
|
||||
public void Admit(Entity entity, bool piloted)
|
||||
{
|
||||
Entities.Add(entity);
|
||||
if (_entities.Contains(entity)) throw new InvalidOperationException("entity: already a member of this zone");
|
||||
_entities.Add(entity);
|
||||
_deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Remove(entity); entity.Deactivate(); }));
|
||||
entity.Activate();
|
||||
if (piloted) Agents.Add(CreateAgent((Ship) entity));
|
||||
}
|
||||
|
||||
// The one removal from this zone (death, dock, jump): membership and its death watch leave together. False, and
|
||||
// nothing changes, when the entity is not a member. The caller deactivates; Remove does not.
|
||||
public bool Remove(Entity entity)
|
||||
{
|
||||
if (!_entities.Remove(entity)) return false;
|
||||
_deathWatches.Remove(entity, out var watch);
|
||||
watch.Dispose();
|
||||
foreach (var floating in FloatingItems)
|
||||
if (ReferenceEquals(floating.Holder, entity)) Free(floating.Id);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Agent CreateAgent(Ship ship)
|
||||
{
|
||||
var agent = new Minion(ship);
|
||||
|
|
@ -231,7 +249,7 @@ public class Zone
|
|||
{
|
||||
Radius = Pack.Radius,
|
||||
Mass = Pack.Mass,
|
||||
Entities = Entities.Select(EntitySerializer.Pack).ToList(),
|
||||
Entities = _entities.Select(EntitySerializer.Pack).ToList(),
|
||||
Orbits = Orbits.Keys.Select(key => new CultRecordRef<OrbitData>(key)).ToList(),
|
||||
Planets = Planets.Keys.Select(key => new CultRecordRef<BodyData>(key)).ToList(),
|
||||
Time = _time,
|
||||
|
|
@ -315,14 +333,44 @@ public class Zone
|
|||
|
||||
// The one commit of a pickup: the first of the taker's cargo bays that accepts the item stores it and the item
|
||||
// stops floating. Otherwise nothing changes.
|
||||
// An item held by another entity is refused: one holder per item holds at the commit as well.
|
||||
public bool TryPickUp(Entity taker, FloatingBodyId id)
|
||||
{
|
||||
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
||||
if (floating == null || !taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
|
||||
if (floating == null || (floating.Holder != null && !ReferenceEquals(floating.Holder, taker))) return false;
|
||||
if (!taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
|
||||
FloatingItems.Remove(floating);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The tractor beam's claim on a floating item: false if it is absent or already held. The item stays where it is
|
||||
// until its pull begins; StepFloatingItems places it from then on.
|
||||
public bool TryHold(Entity holder, FloatingBodyId id)
|
||||
{
|
||||
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
||||
if (floating == null || floating.Holder != null) return false;
|
||||
floating.Holder = holder;
|
||||
floating.HeldFrom = floating.Body.Position;
|
||||
floating.HeldProgress = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// How far along its pull to the holder a held item is, 0..1.
|
||||
public void SetHeldProgress(FloatingBodyId id, float progress)
|
||||
{
|
||||
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
||||
if (floating?.Holder != null) floating.HeldProgress = saturate(progress);
|
||||
}
|
||||
|
||||
// Lets a held item go: it floats on from where it is with the Velocity and Drift it had.
|
||||
public void Free(FloatingBodyId id)
|
||||
{
|
||||
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
||||
if (floating == null) return;
|
||||
floating.Holder = null;
|
||||
floating.HeldProgress = 0;
|
||||
}
|
||||
|
||||
// Called from the death subscription, before the entity leaves the zone: one generator per death. Each equipped
|
||||
// item but the hull drops on a LootDropProbability draw; every cargo stack drops whole. Each drop leaves from the
|
||||
// dead entity's position at a random heading. Draw order: Equipment order (the probability draw, then the heading
|
||||
|
|
@ -344,12 +392,18 @@ public class Zone
|
|||
}
|
||||
|
||||
// After StepClouds and before FireControl.Step: each floating item drifts as a KinematicBody on the zone's force
|
||||
// and is removed once its lifetime has run out.
|
||||
// and is removed once its lifetime has run out. A held item is neither integrated nor expired: it is placed
|
||||
// between where it was claimed and its holder, now that the entities have moved.
|
||||
private void StepFloatingItems(float deltaTime)
|
||||
{
|
||||
var settings = _itemManager.GameplaySettings;
|
||||
foreach (var floating in FloatingItems.ToArray())
|
||||
{
|
||||
if (floating.Holder != null)
|
||||
{
|
||||
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, smoothstep(0f, 1f, floating.HeldProgress));
|
||||
continue;
|
||||
}
|
||||
floating.Body.Step(deltaTime, GetForce(floating.Body.Position), settings);
|
||||
if (floating.ReleasedAt + floating.Lifetime <= _time) FloatingItems.Remove(floating);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ public class FieldTester : MonoBehaviour
|
|||
Debug.LogError("FieldTester expects a CapabilityPresentationBinder on TestField's GameObject; no capability events will reach the field.");
|
||||
else
|
||||
binder.Bind(_capabilityEvents,
|
||||
grabTargetResolver: handle => handle >= 0 && handle < _pickups.Count ? _pickups[handle].transform : null,
|
||||
grabInitialVelocityResolver: handle => Vector3.forward *
|
||||
(PickupTravelDistance / lerp(PickupTravelTimeMin, PickupTravelTimeMax, _pickups[handle].lerp)));
|
||||
grabTargetResolver: id => (int) id.Value >= 0 && (int) id.Value < _pickups.Count ? _pickups[(int) id.Value].transform : null,
|
||||
grabInitialVelocityResolver: id => Vector3.forward *
|
||||
(PickupTravelDistance / lerp(PickupTravelTimeMin, PickupTravelTimeMax, _pickups[(int) id.Value].lerp)));
|
||||
|
||||
// Impact absorption (fork L): what used to be FieldDriver's own click handler is now this
|
||||
// scene's stand-in for whatever will eventually detect a hit and publish AbsorbEvent.
|
||||
|
|
@ -102,7 +102,7 @@ public class FieldTester : MonoBehaviour
|
|||
if (!TestField.CanGrab) return;
|
||||
click.Clear();
|
||||
var p = _pickups[i1];
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(i1, GrabPhase.Started, p.transform.position.ToCultMath(), 0));
|
||||
_capabilityEvents.PublishGrab(new GrabEvent(new FloatingBodyId((uint) i1), GrabPhase.Started, p.transform.position.ToCultMath(), 0));
|
||||
p.transform = null;
|
||||
_pickups[i1] = p;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ public class ZoneRenderer : MonoBehaviour
|
|||
public LODHandler LODHandler;
|
||||
public Slime SlimeRenderer;
|
||||
public Camera SlimeGravityCamera;
|
||||
public GridObject SimpleCommodityPickup;
|
||||
public GridObject CompoundCommodityPickup;
|
||||
public GridObject GearPickup;
|
||||
public GridObject WeaponPickup;
|
||||
public ItemPickup SimpleCommodityPickup;
|
||||
public ItemPickup CompoundCommodityPickup;
|
||||
public ItemPickup GearPickup;
|
||||
public ItemPickup WeaponPickup;
|
||||
public MineInstance MinePrefab;
|
||||
public VapourCloudInstance CloudPrefab;
|
||||
public Material[] MapGravityMaterials;
|
||||
|
|
@ -94,7 +94,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
private bool _showAsteroidUI;
|
||||
|
||||
public Dictionary<Wormhole, (GameObject gravity, CompassIcon icon)> WormholeInstances = new Dictionary<Wormhole, (GameObject, CompassIcon)>();
|
||||
private List<ItemPickup> _loot = new List<ItemPickup>();
|
||||
private Dictionary<FloatingItem, ItemPickup> _floatingItems = new Dictionary<FloatingItem, ItemPickup>();
|
||||
private Dictionary<Mine, MineInstance> _mines = new Dictionary<Mine, MineInstance>();
|
||||
private Dictionary<VapourCloud, VapourCloudInstance> _clouds = new Dictionary<VapourCloud, VapourCloudInstance>();
|
||||
|
||||
|
|
@ -236,6 +236,15 @@ public class ZoneRenderer : MonoBehaviour
|
|||
instance.Blast();
|
||||
}));
|
||||
|
||||
// Every floating item, whoever released it (a death drop, jettisoned cargo), is drawn through this one
|
||||
// subscription. An item leaves Zone.FloatingItems by expiry or by pickup, and either way its pickup is gone.
|
||||
foreach (var floating in zone.FloatingItems) LoadFloatingItem(zone, floating);
|
||||
_zoneSubscriptions.Add(zone.FloatingItems.ObserveAdd().Subscribe(e => LoadFloatingItem(zone, e.Value)));
|
||||
_zoneSubscriptions.Add(zone.FloatingItems.ObserveRemove().Subscribe(e =>
|
||||
{
|
||||
if (_floatingItems.Remove(e.Value, out var pickup) && pickup) Destroy(pickup.gameObject);
|
||||
}));
|
||||
|
||||
// Every vapour cloud, whoever vented it, is drawn through this one subscription. A cloud leaves Zone.Clouds
|
||||
// when its opacity has reached nothing, so a removal destroys its fog bank with nothing left to show.
|
||||
foreach (var cloud in zone.Clouds) LoadCloud(zone, cloud);
|
||||
|
|
@ -274,6 +283,29 @@ public class ZoneRenderer : MonoBehaviour
|
|||
_mines.Add(mine, instance);
|
||||
}
|
||||
|
||||
private void LoadFloatingItem(Zone zone, FloatingItem floating)
|
||||
{
|
||||
var prefab = floating.Item switch
|
||||
{
|
||||
SimpleCommodity _ => SimpleCommodityPickup,
|
||||
CompoundCommodity _ => CompoundCommodityPickup,
|
||||
EquippableItem equippableItem when ItemManager.GetData(equippableItem) is WeaponItemData => WeaponPickup,
|
||||
EquippableItem _ => GearPickup,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
var pickup = Instantiate(prefab, ZoneRoot);
|
||||
pickup.Bind(zone, floating);
|
||||
pickup.ScanLabel.text = ItemManager.Title(floating.Item);
|
||||
if (floating.Item is CraftedItemInstance craftedItemInstance)
|
||||
{
|
||||
var c = ItemManager.GetTier(craftedItemInstance).tier.Color.ToColor();
|
||||
c.a = 0;
|
||||
pickup.ScanLabel.color = c;
|
||||
}
|
||||
else pickup.ScanLabel.color = new Color(.75f, .75f, .75f, 0);
|
||||
_floatingItems.Add(floating, pickup);
|
||||
}
|
||||
|
||||
private void LoadCloud(Zone zone, VapourCloud cloud)
|
||||
{
|
||||
var instance = Instantiate(CloudPrefab);
|
||||
|
|
@ -312,9 +344,9 @@ public class ZoneRenderer : MonoBehaviour
|
|||
if (instance) Destroy(instance.gameObject);
|
||||
_clouds.Clear();
|
||||
|
||||
foreach(var gridObject in _loot)
|
||||
if(gridObject) Destroy(gridObject.gameObject);
|
||||
_loot.Clear();
|
||||
foreach (var pickup in _floatingItems.Values)
|
||||
if (pickup) Destroy(pickup.gameObject);
|
||||
_floatingItems.Clear();
|
||||
|
||||
foreach (var entity in Zone.Entities)
|
||||
{
|
||||
|
|
@ -487,7 +519,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
void Update()
|
||||
{
|
||||
var maxDepth = 0f;
|
||||
foreach (var loot in _loot)
|
||||
foreach (var loot in _floatingItems.Values)
|
||||
{
|
||||
loot.ViewOrigin = PerspectiveEntity.Position;
|
||||
loot.ViewDirection = PerspectiveEntity.Aim;
|
||||
|
|
@ -603,41 +635,6 @@ public class ZoneRenderer : MonoBehaviour
|
|||
// MinimapGravityQuad.transform.position = gravPos;
|
||||
// MinimapTintQuad.transform.position = gravPos - Vector3.up*10;
|
||||
}
|
||||
|
||||
public void DestroyLoot(ItemPickup loot)
|
||||
{
|
||||
_loot.Remove(loot);
|
||||
}
|
||||
|
||||
public void DropItem(Vector3 position, Vector3 velocity, ItemInstance item)
|
||||
{
|
||||
var gridObject = item switch
|
||||
{
|
||||
SimpleCommodity _ => Instantiate(SimpleCommodityPickup),
|
||||
CompoundCommodity _ => Instantiate(CompoundCommodityPickup),
|
||||
EquippableItem equippableItem when ItemManager.GetData(equippableItem) is WeaponItemData => Instantiate(WeaponPickup),
|
||||
EquippableItem _ => Instantiate(GearPickup),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
var t = gridObject.transform;
|
||||
t.parent = ZoneRoot;
|
||||
gridObject.Zone = Zone;
|
||||
t.position = position;
|
||||
gridObject.Velocity = velocity;
|
||||
var itemPickup = gridObject.gameObject.GetComponent<ItemPickup>();
|
||||
itemPickup.Item = item;
|
||||
itemPickup.ZoneRenderer = this;
|
||||
itemPickup.ScanLabel.text = ItemManager.Title(item);
|
||||
if (item is CraftedItemInstance craftedItemInstance)
|
||||
{
|
||||
var c = ItemManager.GetTier(craftedItemInstance).tier.Color.ToColor();
|
||||
c.a = 0;
|
||||
itemPickup.ScanLabel.color = c;
|
||||
}
|
||||
else itemPickup.ScanLabel.color = new Color(.75f, .75f, .75f, 0);
|
||||
gridObject.gameObject.AddComponent<TimedDestroy>().Duration = Settings.PickupLifetime;
|
||||
_loot.Add(itemPickup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue