Pickup is a capability: PickupData/Pickup run a timed grab through Zone's hold API and TryPickUp (group A, sim)
GrabEvent carries a FloatingBodyId; Zone gains TryHold/SetHeldProgress/Free and places a held item instead of integrating it; Entity.TractorPower is derived from Pickup.Power and TractorCommand is the pilot's input (ActionGameManager writes it undamped, the Time.deltaTime ramp is deleted). Binder and FieldTester change the delegate type only. PickupTests and a save round-trip pin for the player's zone index. Group A builds; no beam is on any generated ship until group B. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e24d942b9d
commit
5aa989abbf
14 changed files with 736 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ public interface IPowerConsumer
|
|||
Union(45, typeof(MunitionSeekerData)),
|
||||
Union(46, typeof(MunitionControlData)),
|
||||
Union(47, typeof(MunitionFuelData)),
|
||||
Union(48, typeof(PickupData)),
|
||||
JsonObject(MemberSerialization.OptIn)]
|
||||
public abstract class BehaviorData
|
||||
{
|
||||
|
|
|
|||
157
Assets/Scripts/ServerShared/Behaviors/Pickup.cs
Normal file
157
Assets/Scripts/ServerShared/Behaviors/Pickup.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 origin = Entity.Position.xz;
|
||||
|
||||
if (!_grabbing)
|
||||
{
|
||||
if (!active || _power < grabPower) return;
|
||||
var item = Select(zone, origin, range, Evaluate(_data.BeamRadius));
|
||||
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 && length(held.Body.Position - origin) > range))
|
||||
{
|
||||
_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 free item nearest the beam axis within Range along it and BeamRadius of it; ties go to the lowest id. A
|
||||
// zero aim selects nothing.
|
||||
private FloatingItem Select(Zone zone, float2 origin, float range, float radius)
|
||||
{
|
||||
var aim = Entity.Aim.xz;
|
||||
if (lengthsq(aim) <= 0) return null;
|
||||
var axis = normalize(aim);
|
||||
FloatingItem best = null;
|
||||
var bestOff = float.MaxValue;
|
||||
foreach (var floating in zone.FloatingItems)
|
||||
{
|
||||
if (floating.Holder != null) continue;
|
||||
var to = floating.Body.Position - origin;
|
||||
var along = dot(to, axis);
|
||||
var off = length(to - axis * along);
|
||||
if (along < 0 || along > range || off > radius) 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;
|
||||
|
|
|
|||
|
|
@ -144,7 +144,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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,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>();
|
||||
|
|
@ -228,6 +229,8 @@ public class Zone
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -330,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
|
||||
|
|
@ -359,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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public sealed class CapabilityEventsTests
|
|||
events.Thrust.Subscribe(thrust.Add);
|
||||
|
||||
var absorbEvent = new AbsorbEvent(float3(1, 2, 3), float3(0, 0, 1), 5f, DamageType.Corrosive, AbsorbLayer.Hull);
|
||||
var grabEvent = new GrabEvent(7, GrabPhase.Started, float3(1, 0, 0), 1.5f);
|
||||
var grabEvent = new GrabEvent(new FloatingBodyId(7), GrabPhase.Started, float3(1, 0, 0), 1.5f);
|
||||
var meleeEvent = new MeleeEvent(float3(0, 0, 1), 10f, 90f, 0.5f);
|
||||
var thrustEvent = new ThrustEvent(float2(1, 0), 0.5f, float2(0.2f, -0.2f));
|
||||
|
||||
|
|
|
|||
448
tests/Aetheria.Shared.Tests/PickupTests.cs
Normal file
448
tests/Aetheria.Shared.Tests/PickupTests.cs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using UniRx;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
using int2 = CultMath.int2;
|
||||
|
||||
// The tractor beam in the sim (aetheria-release cut loot-2): a Pickup behaviour spins its beam up in sim time and runs
|
||||
// one timed grab of a floating item, ending in Zone.TryPickUp. Rulings: jettison-shared-floating-items,
|
||||
// unity-presents-only-r3, sim-speed-presentation; the question loot-grab-release-cancels, built hold-required. Every
|
||||
// grab here is run by Zone.Update ticking a ship's own gear: nothing calls the behaviour directly.
|
||||
public sealed class PickupTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-pickup-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly List<CultCache> _caches = new List<CultCache>();
|
||||
|
||||
public PickupTests() => Directory.CreateDirectory(_root);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var c in _caches) c.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v };
|
||||
|
||||
private static Shape Solid(int w, int h)
|
||||
{
|
||||
var shape = new Shape(w, h);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
return shape;
|
||||
}
|
||||
|
||||
private static PickupData Beam(float extend = .3f, float envelop = .2f, float pull = .5f) => new PickupData
|
||||
{
|
||||
Range = Constant(75), BeamRadius = Constant(25), GrabPower = Constant(.5f), SpinRate = Constant(2),
|
||||
ExtendTime = Constant(extend), EnvelopTime = Constant(envelop), PullTime = Constant(pull)
|
||||
};
|
||||
|
||||
private sealed class Lab
|
||||
{
|
||||
public ItemManager Items;
|
||||
public Zone Zone;
|
||||
public int Lot = 1;
|
||||
|
||||
public EquippableItem Make(string name) => new EquippableItem
|
||||
{
|
||||
Data = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<EquippableItemData>(name)), Durability = 1, Lot = Lot++
|
||||
};
|
||||
|
||||
public SimpleCommodity Ore(int quantity) => new SimpleCommodity
|
||||
{
|
||||
Data = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<SimpleCommodityData>("Ore")), Quantity = quantity
|
||||
};
|
||||
}
|
||||
|
||||
private Lab Build(PickupData beam, float lifetime = 30f)
|
||||
{
|
||||
var settings = new GameplaySettings
|
||||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
TargetDetectionInfoThreshold = .1f, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f,
|
||||
FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f,
|
||||
UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 2f, AgentMinHitProbability = 0f,
|
||||
LootDropProbability = 0, LootDropVelocity = 25, FloatingItemLifetime = lifetime
|
||||
};
|
||||
var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
|
||||
_caches.Add(cache);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000 });
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = Solid(1, 1), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { beam }
|
||||
});
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Spare", Hardpoint = HardpointType.Tool, Shape = Solid(1, 1), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
|
||||
});
|
||||
cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = Solid(4, 4), Durability = 4 });
|
||||
cache.Upsert(new SimpleCommodityData { Name = "Ore", Shape = Solid(1, 1), MaxStack = 100 });
|
||||
cache.FlushAsync().Wait();
|
||||
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 9000; 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 { Radius = 0 }, new GalaxyZone { Name = "Pickup", Owner = null }, null);
|
||||
return new Lab { Items = items, Zone = zone };
|
||||
}
|
||||
|
||||
// A ship with a cargo bay, aimed along +x and (unless told otherwise) carrying the tractor beam.
|
||||
private static Ship Spawn(Lab lab, float3 at, bool beam = true)
|
||||
{
|
||||
var hull = lab.Items.ItemData.RefOf<ItemData>(lab.Items.ItemData.GetByName<HullData>("Skiff"));
|
||||
var ship = new Ship(lab.Items, lab.Zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = lab.Lot++ }, new EntitySettings());
|
||||
var crate = lab.Make("Crate");
|
||||
Assert.True(ship.TryFindSpace(crate, out var cratePos));
|
||||
Assert.True(ship.TryEquip(crate, cratePos));
|
||||
if (beam) Equip(ship, lab.Make("Tractor"));
|
||||
lab.Zone.Admit(ship, piloted: false);
|
||||
ship.Position = at;
|
||||
ship.Aim = float3(1, 0, 0);
|
||||
return ship;
|
||||
}
|
||||
|
||||
private static void Equip(Ship ship, EquippableItem gear)
|
||||
{
|
||||
Assert.True(ship.TryFindSpace(gear, out var pos));
|
||||
Assert.True(ship.TryEquip(gear, pos));
|
||||
}
|
||||
|
||||
private static FloatingItem Drift(Lab lab, float x, float y, float2 velocity = default, int ore = 5) =>
|
||||
lab.Zone.Release(lab.Ore(ore), float2(x, y), velocity);
|
||||
|
||||
private static List<GrabEvent> Watch(Entity ship, CompositeDisposable bag)
|
||||
{
|
||||
var events = new List<GrabEvent>();
|
||||
ship.Capabilities.Grab.Subscribe(events.Add).AddTo(bag);
|
||||
return events;
|
||||
}
|
||||
|
||||
private static void Tick(Lab lab, int ticks, float dt)
|
||||
{
|
||||
for (var i = 0; i < ticks; i++) lab.Zone.Update(dt);
|
||||
}
|
||||
|
||||
private static void Kill(Entity e) => e.HeatstrokeDeath.OnNext(Unit.Default);
|
||||
|
||||
[Fact]
|
||||
public void NoPickupBehaviourNoGrab()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0), beam: false);
|
||||
var item = Drift(lab, 40, 0);
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
Tick(lab, 240, 1 / 60f);
|
||||
Assert.Null(item.Holder);
|
||||
Assert.Contains(item, lab.Zone.FloatingItems);
|
||||
Assert.Equal(0f, ship.TractorPower);
|
||||
Assert.Empty(events);
|
||||
|
||||
// The same ship with the beam equipped claims the item.
|
||||
Equip(ship, lab.Make("Tractor"));
|
||||
Tick(lab, 60, 1 / 60f);
|
||||
Assert.Same(ship, item.Holder);
|
||||
Assert.Equal(GrabPhase.Started, events.First().Phase);
|
||||
Assert.True(ship.TractorPower > 0f);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(60)]
|
||||
[InlineData(600)]
|
||||
public void TheBeamSpinsUpInSimTime(int ticksPerSecond)
|
||||
{
|
||||
var dt = 1f / ticksPerSecond;
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var item = Drift(lab, 40, 0);
|
||||
|
||||
// Power rises two per sim second and not faster, at any tick rate; the claim comes on the tick it passes GrabPower.
|
||||
ship.TractorCommand = 1;
|
||||
int claim = -1, full = -1;
|
||||
var before = 0f;
|
||||
for (var tick = 1; tick <= ticksPerSecond; tick++)
|
||||
{
|
||||
lab.Zone.Update(dt);
|
||||
if (claim < 0 && item.Holder != null)
|
||||
{
|
||||
claim = tick;
|
||||
Assert.True(ship.TractorPower >= .5f);
|
||||
Assert.True(before < .5f);
|
||||
}
|
||||
if (full < 0 && ship.TractorPower >= 1f) full = tick;
|
||||
before = ship.TractorPower;
|
||||
}
|
||||
Assert.Equal(ticksPerSecond / 4, claim);
|
||||
Assert.Equal(ticksPerSecond / 2, full);
|
||||
|
||||
// Command 0 spins it down at the same rate.
|
||||
ship.TractorCommand = 0;
|
||||
var empty = -1;
|
||||
for (var tick = 1; tick <= ticksPerSecond; tick++)
|
||||
{
|
||||
lab.Zone.Update(dt);
|
||||
if (empty < 0 && ship.TractorPower <= 0f) empty = tick;
|
||||
}
|
||||
Assert.Equal(ticksPerSecond / 2, empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(.3f, .2f, .5f, 60)]
|
||||
[InlineData(1f, 1f, 1f, 180)]
|
||||
public void AGrabTakesExactlyItsPhasesThenCommits(float extend, float envelop, float pull, int ticksAfterClaim)
|
||||
{
|
||||
foreach (var rate in new[] { 60, 600 })
|
||||
{
|
||||
var dt = 1f / rate;
|
||||
var lab = Build(Beam(extend, envelop, pull));
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var item = Drift(lab, 40, 0);
|
||||
ship.TractorCommand = 1;
|
||||
int claim = -1, commit = -1;
|
||||
for (var tick = 1; tick <= rate * 6 && commit < 0; tick++)
|
||||
{
|
||||
lab.Zone.Update(dt);
|
||||
if (claim < 0 && item.Holder != null) claim = tick;
|
||||
if (!lab.Zone.FloatingItems.Contains(item)) commit = tick;
|
||||
}
|
||||
Assert.True(claim > 0);
|
||||
Assert.Equal(ticksAfterClaim * rate / 60, commit - claim);
|
||||
Assert.Equal(5, ship.CargoBays.Single().Cargo.Keys.OfType<SimpleCommodity>().Single().Quantity);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrabEventsNameTheFloatingItem()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var other = Drift(lab, 40, 20);
|
||||
var item = Drift(lab, 40, 0);
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
Tick(lab, 150, 1 / 60f);
|
||||
Assert.Equal(new[] { GrabPhase.Started, GrabPhase.Extend, GrabPhase.Envelop, GrabPhase.Pull, GrabPhase.Completed },
|
||||
events.Take(5).Select(e => e.Phase).ToArray());
|
||||
Assert.All(events.Take(5), e => Assert.Equal(item.Id, e.Target));
|
||||
Assert.NotEqual(other.Id, item.Id);
|
||||
Assert.Equal(.3f, events[1].PhaseDuration, 4);
|
||||
Assert.Equal(.2f, events[2].PhaseDuration, 4);
|
||||
Assert.Equal(.5f, events[3].PhaseDuration, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleasingTheBeamCancelsAndTheItemFloatsFree()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
const float dt = 1 / 60f;
|
||||
var lab = Build(Beam(), lifetime: 2f);
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var item = Drift(lab, 40, 0, float2(3, 1));
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
var tick = 0;
|
||||
while (!events.Any(e => e.Phase == GrabPhase.Pull) && tick++ < 600) lab.Zone.Update(dt);
|
||||
Assert.Same(ship, item.Holder);
|
||||
var velocity = item.Body.Velocity;
|
||||
var drift = item.Body.Drift;
|
||||
|
||||
// The key comes up during the pull: Power falls below GrabPower and the grab is dropped.
|
||||
ship.TractorCommand = 0;
|
||||
while (!events.Any(e => e.Phase == GrabPhase.Cancelled) && tick++ < 600) lab.Zone.Update(dt);
|
||||
Assert.True(tick < 110);
|
||||
Assert.DoesNotContain(events, e => e.Phase == GrabPhase.Completed);
|
||||
Assert.Null(item.Holder);
|
||||
Assert.Contains(item, lab.Zone.FloatingItems);
|
||||
Assert.Equal(velocity, item.Body.Velocity);
|
||||
Assert.Equal(drift, item.Body.Drift);
|
||||
|
||||
// It drifts on with that velocity and expires on the clock it was released on, not from the cancel.
|
||||
var at = item.Body.Position;
|
||||
lab.Zone.Update(dt);
|
||||
Assert.True(item.Body.Position.x > at.x);
|
||||
while (lab.Zone.FloatingItems.Contains(item) && tick++ < 600) lab.Zone.Update(dt);
|
||||
Assert.InRange(tick, 118, 122);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AFullHoldCancelsAtTheEnd()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var bay = ship.CargoBays.Single();
|
||||
for (var i = 0; i < 16; i++) Assert.True(bay.TryStore(lab.Make("Spare")));
|
||||
Assert.False(bay.TryStore(lab.Make("Spare")));
|
||||
var item = Drift(lab, 40, 0);
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
var tick = 0;
|
||||
while (!events.Any(e => e.Phase == GrabPhase.Cancelled) && tick++ < 300) lab.Zone.Update(1 / 60f);
|
||||
Assert.Equal(new[] { GrabPhase.Started, GrabPhase.Extend, GrabPhase.Envelop, GrabPhase.Pull, GrabPhase.Cancelled },
|
||||
events.Select(e => e.Phase).ToArray());
|
||||
Assert.Null(item.Holder);
|
||||
Assert.Contains(item, lab.Zone.FloatingItems);
|
||||
Assert.Equal(16, bay.Cargo.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneHolderPerItem()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var a = Spawn(lab, float3(0, 0, 0));
|
||||
var b = Spawn(lab, float3(0, 0, 10));
|
||||
var eventsA = Watch(a, bag);
|
||||
var eventsB = Watch(b, bag);
|
||||
|
||||
// One item, two beams: exactly one claims it, the other never does, and the non-holder cannot take it.
|
||||
var only = Drift(lab, 40, 5);
|
||||
a.TractorCommand = 1;
|
||||
b.TractorCommand = 1;
|
||||
Tick(lab, 20, 1 / 60f);
|
||||
Assert.Equal(1, new[] { eventsA, eventsB }.Count(e => e.Any(x => x.Phase == GrabPhase.Started)));
|
||||
var holder = only.Holder;
|
||||
var loser = ReferenceEquals(holder, a) ? b : a;
|
||||
Assert.False(lab.Zone.TryPickUp(loser, only.Id));
|
||||
Assert.Contains(only, lab.Zone.FloatingItems);
|
||||
Assert.Empty(loser.CargoBays.Single().Cargo);
|
||||
Tick(lab, 120, 1 / 60f);
|
||||
Assert.DoesNotContain(only, lab.Zone.FloatingItems);
|
||||
Assert.Single(holder.CargoBays.Single().Cargo);
|
||||
Assert.Empty(loser.CargoBays.Single().Cargo);
|
||||
|
||||
// Two items, each nearest a different beam's axis: each ship holds its own.
|
||||
var first = Drift(lab, 60, 0);
|
||||
var second = Drift(lab, 60, 10);
|
||||
Tick(lab, 20, 1 / 60f);
|
||||
Assert.NotNull(first.Holder);
|
||||
Assert.NotNull(second.Holder);
|
||||
Assert.NotSame(first.Holder, second.Holder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionIsTheItemNearestTheBeamAxis()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
|
||||
// The nearer to the axis wins, though the other is closer to the ship.
|
||||
var near = Drift(lab, 20, 12);
|
||||
var onAxis = Drift(lab, 60, 3);
|
||||
Tick(lab, 30, 1 / 60f);
|
||||
Assert.Same(ship, onAxis.Holder);
|
||||
Assert.Null(near.Holder);
|
||||
Assert.Equal(onAxis.Id, events.First().Target);
|
||||
|
||||
// Out of the beam, past its range or behind the ship, an item is never taken.
|
||||
foreach (var (x, y) in new[] { (30f, 30f), (80f, 0f), (-10f, 0f) })
|
||||
{
|
||||
var other = Build(Beam());
|
||||
var lone = Spawn(other, float3(0, 0, 0));
|
||||
var stray = Drift(other, x, y);
|
||||
lone.TractorCommand = 1;
|
||||
Tick(other, 240, 1 / 60f);
|
||||
Assert.Null(stray.Holder);
|
||||
Assert.Contains(stray, other.Zone.FloatingItems);
|
||||
}
|
||||
|
||||
// Equally far from the axis, the lower id.
|
||||
var tie = Build(Beam());
|
||||
var pilot = Spawn(tie, float3(0, 0, 0));
|
||||
var lower = Drift(tie, 40, 5);
|
||||
var higher = Drift(tie, 40, -5);
|
||||
pilot.TractorCommand = 1;
|
||||
Tick(tie, 30, 1 / 60f);
|
||||
Assert.Same(pilot, lower.Holder);
|
||||
Assert.Null(higher.Holder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AHeldItemIsPlacedNotIntegratedAndDoesNotExpire()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
const float dt = 1 / 60f;
|
||||
var lab = Build(Beam(), lifetime: 1f);
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
var item = Drift(lab, 40, 0, float2(3, 0));
|
||||
var events = Watch(ship, bag);
|
||||
ship.TractorCommand = 1;
|
||||
while (item.Holder == null) lab.Zone.Update(dt);
|
||||
var claimed = item.Body.Position;
|
||||
Assert.Equal(claimed, item.HeldFrom);
|
||||
|
||||
// Through Extend and Envelop the item stays where it was claimed, whatever velocity it had.
|
||||
while (!events.Any(e => e.Phase == GrabPhase.Pull))
|
||||
{
|
||||
lab.Zone.Update(dt);
|
||||
if (events.Any(e => e.Phase == GrabPhase.Pull)) break;
|
||||
Assert.Equal(claimed, item.Body.Position);
|
||||
}
|
||||
|
||||
// During the pull it moves toward the holder, and a held item outlives its lifetime.
|
||||
lab.Zone.Update(dt * 10);
|
||||
Assert.True(item.Body.Position.x < claimed.x);
|
||||
Assert.True(item.Body.Position.x > 0);
|
||||
var held = Build(Beam(), lifetime: 1f);
|
||||
var carrier = Spawn(held, float3(5, 0, 7));
|
||||
var stuck = Drift(held, 40, 0);
|
||||
Assert.True(held.Zone.TryHold(carrier, stuck.Id));
|
||||
Tick(held, 180, dt);
|
||||
Assert.Contains(stuck, held.Zone.FloatingItems);
|
||||
Assert.Equal(float2(40, 0), stuck.Body.Position);
|
||||
|
||||
// At progress 1 it is at the holder; freed, an item past its lifetime goes on the next step.
|
||||
held.Zone.SetHeldProgress(stuck.Id, 1);
|
||||
held.Zone.Update(dt);
|
||||
Assert.Equal(5f, stuck.Body.Position.x, 3);
|
||||
Assert.Equal(7f, stuck.Body.Position.y, 3);
|
||||
held.Zone.Free(stuck.Id);
|
||||
held.Zone.Update(dt);
|
||||
Assert.DoesNotContain(stuck, held.Zone.FloatingItems);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheHoldersLeavingFreesItsItems()
|
||||
{
|
||||
using var bag = new CompositeDisposable();
|
||||
var lab = Build(Beam());
|
||||
var ship = Spawn(lab, float3(0, 0, 0));
|
||||
Assert.True(ship.CargoBays.Single().TryStore(lab.Ore(7)));
|
||||
var item = Drift(lab, 40, 0, ore: 3);
|
||||
ship.TractorCommand = 1;
|
||||
Tick(lab, 30, 1 / 60f);
|
||||
Assert.Same(ship, item.Holder);
|
||||
var before = lab.Zone.FloatingItems.Count;
|
||||
|
||||
// Death goes through the zone's one removal: the held item is free, and the ship's own cargo drops as always.
|
||||
Kill(ship);
|
||||
Assert.Null(item.Holder);
|
||||
Assert.Contains(item, lab.Zone.FloatingItems);
|
||||
Assert.Equal(before + 1, lab.Zone.FloatingItems.Count);
|
||||
Assert.Equal(7, ((SimpleCommodity) lab.Zone.FloatingItems.Last().Item).Quantity);
|
||||
Tick(lab, 60, 1 / 60f);
|
||||
Assert.Contains(item, lab.Zone.FloatingItems);
|
||||
Assert.Null(item.Holder);
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +80,52 @@ public sealed class RunSaveTests : IDisposable
|
|||
Assert.Equal(8, RecordsIn(Run));
|
||||
}
|
||||
|
||||
// Continue finds the player's entity by its place in the saved zone: the index Capture writes must be the entity's
|
||||
// place in the live zone, and survive the round trip to the same entity's pack.
|
||||
[Fact]
|
||||
public void ThePlayersZoneIndexSurvivesSaveAndContinue()
|
||||
{
|
||||
using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true))
|
||||
{
|
||||
var shape = new Shape(4, 4);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
foreach (var name in new[] { "Skiff", "Barge" })
|
||||
cache.Upsert(new HullData { Name = name, HullType = HullType.Ship, Shape = shape, Durability = 100, Mass = 100 });
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
RunSave.Commit(cache, Game(cache), Zones(2, StageZoneContents(cache), 0), new ProvenanceLedger());
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 2; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(cache, ledger, TestSettings(), _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy);
|
||||
galaxy.Zones[0].Contents = zone;
|
||||
Ship Make(string hull, int lot) => new Ship(items, zone, new EquippableItem
|
||||
{
|
||||
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>(hull)), Durability = 100, Lot = lot
|
||||
}, new EntitySettings());
|
||||
var other = Make("Skiff", 1);
|
||||
var player = Make("Barge", 2);
|
||||
zone.Admit(other, piloted: false);
|
||||
zone.Admit(player, piloted: false);
|
||||
var (game, zones) = RunSave.Capture(cache, galaxy, zone, player, new SavedActionBarBinding[0], 0);
|
||||
RunSave.Commit(cache, game, zones, items.Lots);
|
||||
}
|
||||
|
||||
using (var cache = Open())
|
||||
{
|
||||
var saved = cache.GetGlobal<SavedGame>();
|
||||
Assert.Equal(1, saved.CurrentZoneEntity);
|
||||
var packs = cache.Get(saved.Zones[saved.CurrentZone]).Contents.Entities;
|
||||
Assert.Equal("Barge", cache.Get(packs[saved.CurrentZoneEntity].Hull.Data).Name);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResumeThenSaveKeepsUnloadedZoneContents()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue