Layers: one owner puts orbitals above the ship plane and floaters below; per-hull GridOffset is deleted
GameplaySettings carries ShipLayerHeight 3, OrbitalLayerHeight 15, FloaterLayerHeight 0 (authored in Settings.asset). Entity.LayerHeight (Ship, OrbitalEntity) is the one offset RideHeight adds; DrawAhead.FloaterHeight is the floater height ItemPickup and MineInstance draw at. HullData.GridOffset (Key 26) and the pickup/mine/prefab GridOffset fields are gone with every reader. Fixtures (DrawnPose, FireControl*, ImpactFeedback) take heights from the layer owner; LayerTests pin each kind's layer. FloatingItemsDrawnSmoke follows. ItemPickup/MineInstance.Bind take the settings from ZoneRenderer. Builds as one commit with its tests; not compile-verified until the cut's head run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
624d5420e9
commit
be7f2b0787
23 changed files with 205 additions and 51 deletions
|
|
@ -146,6 +146,5 @@ MonoBehaviour:
|
|||
ActiveEmission: 100
|
||||
ArmedEmission: 1000
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
HitEffect: {fileID: 1121192042590650, guid: e1b08032bec16ba4eb1ae23cbd572664, type: 3}
|
||||
HitEffectScale: 10
|
||||
|
|
|
|||
|
|
@ -189,7 +189,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
RotationSpeed: 1
|
||||
GridOffset: 0
|
||||
LabelFadeDuration: 0.5
|
||||
LabelPersistDuration: 3
|
||||
LabelDisplayAngle: 15
|
||||
|
|
|
|||
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings.asset
(Stored with Git LFS)
Binary file not shown.
|
|
@ -90,6 +90,8 @@ public static class FloatingItemsDrawnSmoke
|
|||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
// A floater rides its own layer: a nonzero one (never saved back to the asset), so a presenter that drops it fails.
|
||||
settings.FloaterLayerHeight = -1.5f;
|
||||
|
||||
// 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.
|
||||
|
|
@ -159,7 +161,7 @@ public static class FloatingItemsDrawnSmoke
|
|||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var at = DrawAhead.Position(floating.Body, lead);
|
||||
var want = new Vector3(at.x, DrawAhead.Height(zone, at, lead), at.y);
|
||||
var want = new Vector3(at.x, DrawAhead.FloaterHeight(zone, settings, at, lead), at.y);
|
||||
Require(Vector3.Distance(pickup.transform.position, want) < 1e-3f,
|
||||
$"{what}: the pickup is at {pickup.transform.position}, the body at {want}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ using Random = UnityEngine.Random;
|
|||
public class ItemPickup : MonoBehaviour
|
||||
{
|
||||
public float RotationSpeed = 1f;
|
||||
public float GridOffset;
|
||||
public float LabelFadeDuration = .5f;
|
||||
public float LabelPersistDuration = 3;
|
||||
public float LabelDisplayAngle = 15;
|
||||
|
|
@ -27,11 +26,13 @@ public class ItemPickup : MonoBehaviour
|
|||
private float _displayTime;
|
||||
private float _timeOffset;
|
||||
private Zone _zone;
|
||||
private GameplaySettings _settings;
|
||||
private FloatingItem _floating;
|
||||
|
||||
public void Bind(Zone zone, FloatingItem floating)
|
||||
public void Bind(Zone zone, FloatingItem floating, GameplaySettings settings)
|
||||
{
|
||||
_zone = zone;
|
||||
_settings = settings;
|
||||
_floating = floating;
|
||||
Item = floating.Item;
|
||||
_timeOffset = Random.value * 100;
|
||||
|
|
@ -42,7 +43,7 @@ public class ItemPickup : MonoBehaviour
|
|||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var p = DrawAhead.Position(_floating.Body, lead);
|
||||
transform.position = new Vector3(p.x, DrawAhead.Height(_zone, p, lead) + GridOffset, p.y);
|
||||
transform.position = new Vector3(p.x, DrawAhead.FloaterHeight(_zone, _settings, p, lead), p.y);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
|
|
|
|||
|
|
@ -17,21 +17,22 @@ public class MineInstance : MonoBehaviour
|
|||
public float ActiveEmission = 100f;
|
||||
public float ArmedEmission = 1000f;
|
||||
public float RotationSpeed = 1f;
|
||||
public float GridOffset;
|
||||
// The blast's particles; the effect prefab destroys itself when its particle system stops.
|
||||
public GameObject HitEffect;
|
||||
public float HitEffectScale = 10f;
|
||||
|
||||
private Zone _zone;
|
||||
private GameplaySettings _settings;
|
||||
private Mine _mine;
|
||||
private Material _material;
|
||||
private float _timeOffset;
|
||||
private float _pulse;
|
||||
private float _emission;
|
||||
|
||||
public void Bind(Zone zone, Mine mine)
|
||||
public void Bind(Zone zone, Mine mine, GameplaySettings settings)
|
||||
{
|
||||
_zone = zone;
|
||||
_settings = settings;
|
||||
_mine = mine;
|
||||
_material = MeshRenderer.materials[EmissionSubmesh];
|
||||
_timeOffset = Random.value * 100;
|
||||
|
|
@ -57,7 +58,7 @@ public class MineInstance : MonoBehaviour
|
|||
{
|
||||
var lead = ActionGameManager.Clock.Lead;
|
||||
var p = DrawAhead.Position(_mine.Body, lead);
|
||||
transform.position = new Vector3(p.x, DrawAhead.Height(_zone, p, lead) + GridOffset, p.y);
|
||||
transform.position = new Vector3(p.x, DrawAhead.FloaterHeight(_zone, _settings, p, lead), p.y);
|
||||
}
|
||||
|
||||
// The mine left Zone.Mines, which only its blast does: show it where it was drawn last.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ public static class DrawAhead
|
|||
// The field's height at a point at the drawn time, its wells where their rails put them at T.
|
||||
public static float Height(Zone zone, float2 planar, float lead) => zone.GetHeight(planar, Time(zone, lead));
|
||||
|
||||
// The drawn height of a floater (loot, mines): the field at the drawn time plus the floater layer.
|
||||
public static float FloaterHeight(Zone zone, GameplaySettings settings, float2 planar, float lead) => Height(zone, planar, lead) + settings.FloaterLayerHeight;
|
||||
|
||||
// A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate,
|
||||
// so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step.
|
||||
// A child is drawn where its parent is drawn, as the step copies the parent's pose onto it.
|
||||
|
|
|
|||
|
|
@ -1974,9 +1974,13 @@ public abstract class Entity
|
|||
Manoeuvre.Observe(Acceleration, delta, ItemManager.GameplaySettings.SolutionWindow);
|
||||
}
|
||||
|
||||
// Where an unparented entity sits on the field at a planar point: the zone's height there plus the hull's grid offset.
|
||||
// The step rides at its own time and xz, the presenter at the drawn time and its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar, double time) => Zone.GetHeight(planar, time) + (ItemManager.GetData(Hull) as HullData).GridOffset;
|
||||
// The height above the field of the layer this kind of entity rides on (GameplaySettings): ships the ship plane,
|
||||
// orbital entities the orbital layer above it.
|
||||
public abstract float LayerHeight { get; }
|
||||
|
||||
// Where an unparented entity sits on the field at a planar point: the zone's height there plus the layer this kind of
|
||||
// entity rides on. The step rides at its own time and xz, the presenter at the drawn time and its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar, double time) => Zone.GetHeight(planar, time) + LayerHeight;
|
||||
|
||||
public float RideHeight(float2 planar) => RideHeight(planar, Zone.ExactTime);
|
||||
|
||||
|
|
|
|||
|
|
@ -519,8 +519,7 @@ public class HullData : EquippableItemData
|
|||
[Inspectable, JsonProperty("hullType"), Key(25)]
|
||||
public HullType HullType;
|
||||
|
||||
[Inspectable, JsonProperty("gridOffset"), Key(26)]
|
||||
public float GridOffset;
|
||||
// Key(26) retired 2026-10-16 (per-hull GridOffset; the layer is the kind's, GameplaySettings). Do not reuse.
|
||||
|
||||
[Inspectable, JsonProperty("armor"), Key(27)]
|
||||
public float Armor;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public class OrbitalEntity : Entity
|
|||
OrbitData = orbit;
|
||||
}
|
||||
|
||||
public override float LayerHeight => ItemManager.GameplaySettings.OrbitalLayerHeight;
|
||||
|
||||
public override void Update(float delta)
|
||||
{
|
||||
if (OrbitData.IsSet())
|
||||
|
|
|
|||
|
|
@ -278,6 +278,13 @@ public class GameplaySettings
|
|||
// normal is this times the speed at which it closed. 0 is perfectly inelastic, 1 perfectly elastic.
|
||||
public float ContactRestitution = .5f;
|
||||
|
||||
// Heights above the field of the ship plane, the orbital layer (stations, turrets: every OrbitalEntity) and the floater
|
||||
// layer (loot, mines): orbitals above ships and floaters below them, so no ship ever touches either (ruling
|
||||
// aetheria-gravity:orbitals-above-floaters-below).
|
||||
public float ShipLayerHeight = 3f;
|
||||
public float OrbitalLayerHeight = 15f;
|
||||
public float FloaterLayerHeight = 0f;
|
||||
|
||||
// The floor an AI's own predicted hit probability (FireControl.HitProbability) must clear before it counts
|
||||
// a firing solution worth taking (Combat.cs, TurretController.cs). Player fire is gated on arc alone
|
||||
// (Weapon.ArcAllowsFire, Q2) -- this threshold is an AI fire-discipline heuristic, not part of the roll.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ using quaternion = CultMath.quaternion;
|
|||
[MessagePackObject]
|
||||
public class Ship : Entity
|
||||
{
|
||||
public override float LayerHeight => ItemManager.GameplaySettings.ShipLayerHeight;
|
||||
|
||||
|
||||
// [Key("bindings")] public Dictionary<KeyCode,Guid> Bindings = new Dictionary<KeyCode,Guid>();
|
||||
//[IgnoreMember] public int HullHardpointCount;
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
private void LoadMine(Zone zone, Mine mine)
|
||||
{
|
||||
var instance = Instantiate(MinePrefab);
|
||||
instance.Bind(zone, mine);
|
||||
instance.Bind(zone, mine, ItemManager.GameplaySettings);
|
||||
_mines.Add(mine, instance);
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ public class ZoneRenderer : MonoBehaviour
|
|||
_ => throw new NotImplementedException()
|
||||
};
|
||||
var pickup = Instantiate(prefab, ZoneRoot);
|
||||
pickup.Bind(zone, floating);
|
||||
pickup.Bind(zone, floating, ItemManager.GameplaySettings);
|
||||
pickup.ScanLabel.text = ItemManager.Title(floating.Item);
|
||||
if (floating.Item is CraftedItemInstance craftedItemInstance)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -391,12 +391,13 @@ public sealed class ContactTests : IDisposable
|
|||
var lab = Build();
|
||||
var ship = lab.Add("Solid", float3(0, 0, 0), float2(10, 0));
|
||||
var station = lab.AddOrbital("Heavy", float3(10, 0, 0), float2(0, 0));
|
||||
var turret = lab.AddOrbital("Solid", float3(-10, 0, 0), float2(10, 0));
|
||||
// Behind the ship and faster than it, so it closes on the ship as the station does.
|
||||
var turret = lab.AddOrbital("Solid", float3(-10, 0, 0), float2(30, 0));
|
||||
lab.Step();
|
||||
Assert.Empty(lab.Events);
|
||||
Assert.Equal(float2(10, 0), ship.Velocity);
|
||||
Assert.Equal(float2(0, 0), station.Velocity);
|
||||
Assert.Equal(float2(10, 0), turret.Velocity);
|
||||
Assert.Equal(float2(30, 0), turret.Velocity);
|
||||
}
|
||||
|
||||
// An orbital overlapping a colliding pair changes nothing about the pair (ruling ship-bounce-restitution-setting):
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
private const float PlanetDistance = 1000f, PlanetPhase = .3f;
|
||||
private const float MoonDistance = 60f, MoonPhase = .1f;
|
||||
private const float StationDistance = 3f, StationPhase = .4f;
|
||||
private const float GridOffset = 2.5f;
|
||||
private const float ShipLayerHeight = 2.5f;
|
||||
private const float OrbitalLayerHeight = 7.5f;
|
||||
// The sun's rail is parentless and off-centre, the shape ZoneGenerator gives a cluster root (FixedPosition = cluster centre).
|
||||
private static readonly float2 SunCentre = new float2(700f, -300f);
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
using (var build = AetheriaStores.Open(catalog, catalogWritable: true))
|
||||
{
|
||||
build.Upsert(new VerseGrammar { Revision = 1 });
|
||||
build.Upsert(new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = shape, Durability = 1, Mass = 1000, GridOffset = GridOffset });
|
||||
build.Upsert(new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = shape, Durability = 1, Mass = 1000 });
|
||||
build.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +86,8 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
{
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp()
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
ShipLayerHeight = ShipLayerHeight, OrbitalLayerHeight = OrbitalLayerHeight
|
||||
}, _ => { });
|
||||
|
||||
CultRecordKey Orbit(CultRecordKey parent, float distance, float phase, float2 fixedPosition = default)
|
||||
|
|
@ -270,9 +272,9 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
// A ship that stands still 40 u from the planet rides the field of the drawn well, plus its hull's offset.
|
||||
var ship = new Ship(w.Items, w.Zone, w.Hull, new EntitySettings()) { Position = float3(PlanetAt(w.Zone.ExactTime).x + 40f, 0, PlanetAt(w.Zone.ExactTime).y) };
|
||||
var drawnShip = DrawAhead.Position(ship, lead);
|
||||
Assert.Equal(FieldAt(w, drawnShip.xz, time) + GridOffset, drawnShip.y, 3);
|
||||
Assert.Equal(FieldAt(w, drawnShip.xz, time) + ShipLayerHeight, drawnShip.y, 3);
|
||||
Assert.Equal(ship.RideHeight(drawnShip.xz, time), drawnShip.y, 4);
|
||||
Assert.Equal(w.Zone.GetHeight(ship.Position.xz) + GridOffset, ship.RideHeight(ship.Position.xz), 4);
|
||||
Assert.Equal(w.Zone.GetHeight(ship.Position.xz) + ShipLayerHeight, ship.RideHeight(ship.Position.xz), 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -292,7 +294,7 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
AssertNear(StationAt(w.Zone.ExactTime + lead), drawn.xz, 2e-3f, "the station on its rail at the drawn time");
|
||||
AssertNear(DrawAhead.Orbit(w.Zone, w.StationOrbit, lead), drawn.xz, 1e-6f, "the same point DrawAhead.Orbit gives");
|
||||
Assert.True(length(station.Position.xz + station.Velocity * lead - drawn.xz) > .01f, "a tight rail curves away from its straight extrapolation");
|
||||
Assert.Equal(w.Zone.GetHeight(drawn.xz, w.Zone.ExactTime + lead) + GridOffset, drawn.y, 4);
|
||||
Assert.Equal(w.Zone.GetHeight(drawn.xz, w.Zone.ExactTime + lead) + OrbitalLayerHeight, drawn.y, 4);
|
||||
}
|
||||
|
||||
// The generators make orbital entities with no orbit set (Loadout, LoadoutGenerator): a rail-less one is an ordinary
|
||||
|
|
|
|||
|
|
@ -35,8 +35,9 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings TestSettings(float minHit) => new GameplaySettings
|
||||
private static GameplaySettings TestSettings(float minHit, float orbitalLayerHeight) => new GameplaySettings
|
||||
{
|
||||
OrbitalLayerHeight = orbitalLayerHeight,
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
|
|
@ -71,7 +72,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
{
|
||||
public Zone Zone;
|
||||
public Ship Shooter;
|
||||
public Ship Target;
|
||||
public Entity Target;
|
||||
public List<Gun> Guns = new List<Gun>();
|
||||
public Gun Gun => Guns[0];
|
||||
public Gun Decoy;
|
||||
|
|
@ -84,12 +85,12 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
(ItemRotation Rotation, float Arc)[] mounts = null, bool beam = false, WeaponFuse? fuse = null, float blast = 0f,
|
||||
float range = 100f, float targetRange = 60f, float accuracy = 1f, float minHit = .2f, bool lockWeapon = false,
|
||||
bool turret = false, float energy = 0f, float heat = 0f, float visibility = 0f, int magazine = 0, bool singleAmmoBurst = false, bool decoy = false, bool charged = false, bool auto = false,
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetGridOffset = 0f)
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetLayerHeight = 0f)
|
||||
{
|
||||
mounts ??= new[] { (ItemRotation.None, 0f) };
|
||||
var hullData = new HullData
|
||||
{
|
||||
Name = "Hull", HullType = HullType.Ship, Shape = SolidShape(5, 4), Durability = 1000000, Mass = 1000, Armor = 0, GridOffset = targetGridOffset,
|
||||
Name = "Hull", HullType = HullType.Ship, Shape = SolidShape(5, 4), Durability = 1000000, Mass = 1000, Armor = 0,
|
||||
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
|
||||
};
|
||||
var shooterHullData = new HullData
|
||||
|
|
@ -192,7 +193,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
|
||||
var ledger = new ProvenanceLedger();
|
||||
for (var i = 1; i <= 100; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
|
||||
var items = new ItemManager(cache, ledger, TestSettings(minHit), _ => { });
|
||||
var items = new ItemManager(cache, ledger, TestSettings(minHit, targetLayerHeight), _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "PerWeapon", Owner = null }, null);
|
||||
|
||||
EquippableItem Make(string name, int lot, float durability) => new EquippableItem
|
||||
|
|
@ -235,7 +236,12 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Assert.True(shooter.TryEquip(t, upos));
|
||||
}
|
||||
|
||||
var target = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = lot++ }, new EntitySettings());
|
||||
// A target layer height rides the target on the orbital layer at that height (the shooter is a ship, on the ship layer),
|
||||
// so the two sit at different heights.
|
||||
var targetItem = new EquippableItem { Data = hullRef, Durability = 1000000, Lot = lot++ };
|
||||
Entity target = targetLayerHeight != 0f
|
||||
? new OrbitalEntity(items, zone, targetItem, default, new EntitySettings())
|
||||
: new Ship(items, zone, targetItem, new EntitySettings());
|
||||
Assert.True(target.TryEquip(Make("Gun", lot++, 1), new int2(0, 0)));
|
||||
|
||||
zone.Admit(shooter, piloted: false);
|
||||
|
|
@ -308,13 +314,14 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Assert.True(r.Gun.Weapon.Firing, "the turret must fire a fused weapon at a designated target out of arc");
|
||||
}
|
||||
|
||||
// The AI aims at the height a target rides at (Entity.RideHeight: field plus the hull's GridOffset), not at the
|
||||
// plane. A target hull raised 40 u, a shot fast enough to be predicted: the turret's and Combat's aim point at the
|
||||
// target's stepped pose. Kills: either aim site spelling its own offset (dropped, added to, or scaled).
|
||||
// The AI aims at the height a target rides at (Entity.RideHeight: field plus the layer the target's kind rides on),
|
||||
// not at the plane and not at the shooter's own layer. A target on a layer 40 u up against a shooter on the ship
|
||||
// layer, a shot fast enough to be predicted: the turret's and Combat's aim point at the target's stepped pose.
|
||||
// Kills: either aim site spelling its own offset (dropped, added to, scaled, or the shooter's layer).
|
||||
[Fact]
|
||||
public void ATurretAimsAtTheHeightATargetRidesAt()
|
||||
{
|
||||
var r = Build(turret: true, velocity: 500f, targetGridOffset: 40f);
|
||||
var r = Build(turret: true, velocity: 500f, targetLayerHeight: 40f);
|
||||
r.Zone.Update(1f);
|
||||
|
||||
AssertAimsAtTarget(r);
|
||||
|
|
@ -323,7 +330,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
[Fact]
|
||||
public void CombatAimsAtTheHeightATargetRidesAt()
|
||||
{
|
||||
var r = Build(velocity: 500f, targetGridOffset: 40f);
|
||||
var r = Build(velocity: 500f, targetLayerHeight: 40f);
|
||||
r.Zone.Agents.Add(new Minion(r.Shooter));
|
||||
r.Zone.Update(1f);
|
||||
r.Zone.Update(1f);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ public sealed class FireControlTests : IDisposable
|
|||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings TestSettings() => new GameplaySettings
|
||||
private static GameplaySettings TestSettings(float shipLayerHeight = 3f) => new GameplaySettings
|
||||
{
|
||||
ShipLayerHeight = shipLayerHeight,
|
||||
DefaultEntitySettings = new EntitySettings(),
|
||||
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
|
||||
QualityPriceModifier = new ExponentialLerp(),
|
||||
|
|
@ -45,13 +46,13 @@ public sealed class FireControlTests : IDisposable
|
|||
|
||||
// `well` digs the zone one well at the origin (WellFixture), so heights differ across the zone.
|
||||
private (ItemManager items, Ship ship, EquippedItem[] weapons) BuildShipWithHardpoints(
|
||||
bool well, (ItemRotation rotation, float firingArc)[] hardpoints, float gridOffset = 0f)
|
||||
bool well, (ItemRotation rotation, float firingArc)[] hardpoints, float shipLayerHeight = 0f)
|
||||
{
|
||||
var hullShape = new Shape(Math.Max(hardpoints.Length, 3), 3);
|
||||
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
||||
// Mass defaults to 0 (ItemData.cs:285), which zeroes ThermalMass (Entity.cs MapEntity) and NaNs
|
||||
// UpdateTemperature -- author a real mass so the entity's own temperature stays a real number.
|
||||
var hullData = new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000, GridOffset = gridOffset };
|
||||
var hullData = new HullData { Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000 };
|
||||
for (var i = 0; i < hardpoints.Length; i++)
|
||||
hullData.Hardpoints.Add(new HardpointData
|
||||
{
|
||||
|
|
@ -87,7 +88,7 @@ public sealed class FireControlTests : IDisposable
|
|||
ledger.Lots[1] = new Lot { Origin = new Attributed(), Quality = 1 }; // hull
|
||||
for (var i = 0; i < hardpoints.Length; i++)
|
||||
ledger.Lots[2 + i] = new Lot { Origin = new Attributed(), Quality = 1 }; // guns
|
||||
var items = new ItemManager(_openCache, ledger, TestSettings(), _ => { });
|
||||
var items = new ItemManager(_openCache, ledger, TestSettings(shipLayerHeight), _ => { });
|
||||
var planets = new PlanetSettings();
|
||||
var pack = new ZonePack();
|
||||
if (well) WellFixture.Dig(_openCache, pack, planets, 300f, 100f);
|
||||
|
|
@ -386,7 +387,7 @@ public sealed class FireControlTests : IDisposable
|
|||
[Fact]
|
||||
public void ADrawnShipRidesTheFieldAtItsDrawnPosition()
|
||||
{
|
||||
var (items, ship, _) = BuildShipWithHardpoints(true, new[] { (ItemRotation.None, 0f) }, gridOffset: 2.5f);
|
||||
var (items, ship, _) = BuildShipWithHardpoints(true, new[] { (ItemRotation.None, 0f) }, shipLayerHeight: 2.5f);
|
||||
var zone = ship.Zone;
|
||||
ship.Position = float3(200, 0, 0);
|
||||
zone.Update(1f);
|
||||
|
|
|
|||
|
|
@ -354,8 +354,9 @@ public sealed partial class FireControlCut124Tests
|
|||
{
|
||||
foreach (var shield in new[] { false, true })
|
||||
{
|
||||
var e = Build(TestSettings(), SolidShape(5, 4), equipShield: shield, shieldCapacity: 100000f);
|
||||
e.HullData.GridOffset = 5f;
|
||||
var raised = TestSettings();
|
||||
raised.ShipLayerHeight = 5f;
|
||||
var e = Build(raised, SolidShape(5, 4), equipShield: shield, shieldCapacity: 100000f);
|
||||
for (var i = 0; i < 30; i++) e.Zone.Update(.1f);
|
||||
Assert.Equal(5f, e.Target.Position.y, 1e-4);
|
||||
var subs = new List<IDisposable>();
|
||||
|
|
@ -434,9 +435,10 @@ public sealed partial class FireControlCut124Tests
|
|||
[Fact]
|
||||
public void TheImpactHeightIsTheTargetsHeightAndCarriedKeepsRelativeHeight()
|
||||
{
|
||||
// Production y is the zone height plus the hull's GridOffset, so a raised hull is reachable headless.
|
||||
var e = Build(TestSettings(), SolidShape(21, 3), velocity: 0, damage: 100f, custom: new[] { ("Marker", new Shape(), new int2(2, 1), 1f) });
|
||||
e.HullData.GridOffset = 5f;
|
||||
// Production y is the zone height plus the ship layer, so a raised ship plane is reachable headless.
|
||||
var raised = TestSettings();
|
||||
raised.ShipLayerHeight = 5f;
|
||||
var e = Build(raised, SolidShape(21, 3), velocity: 0, damage: 100f, custom: new[] { ("Marker", new Shape(), new int2(2, 1), 1f) });
|
||||
e.Zone.Update(.01f);
|
||||
Assert.Equal(5f, e.Target.Position.y, 1e-4);
|
||||
var subs = new List<IDisposable>();
|
||||
|
|
|
|||
126
tests/Aetheria.Shared.Tests/LayerTests.cs
Normal file
126
tests/Aetheria.Shared.Tests/LayerTests.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using Xunit;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
using int2 = CultMath.int2;
|
||||
|
||||
// Layers (aetheria-gravity cut orbital-layering; ruling orbitals-above-floaters-below): GameplaySettings' three
|
||||
// heights decide where each kind of thing sits above the field. A ship rides the ship plane, every orbital entity
|
||||
// (station, turret) the orbital layer above it, a floater (loot, mine) the floater layer below. No hull carries a
|
||||
// height of its own.
|
||||
public sealed class LayerTests : IDisposable
|
||||
{
|
||||
private const float ShipHeight = 2.5f, OrbitalHeight = 9f, FloaterHeight = -1.25f;
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-layers-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly CultCache _cache;
|
||||
|
||||
public LayerTests()
|
||||
{
|
||||
Directory.CreateDirectory(_root);
|
||||
_cache = AetheriaStores.Open(Path.Combine(_root, "layers.cc"), catalogWritable: true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
private static GameplaySettings 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(),
|
||||
ShipLayerHeight = ShipHeight, OrbitalLayerHeight = OrbitalHeight, FloaterLayerHeight = FloaterHeight
|
||||
};
|
||||
|
||||
private (ItemManager Items, Zone Zone) Build()
|
||||
{
|
||||
// Hull sizes and masses are spread so no kind coincides with a size or a mass: a layer chosen by either fails.
|
||||
Shape Solid(int side)
|
||||
{
|
||||
var shape = new Shape(side, side);
|
||||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
return shape;
|
||||
}
|
||||
_cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
_cache.Upsert(new HullData { Name = "Ship", HullType = HullType.Ship, Shape = Solid(5), Durability = 1, Mass = 1000 });
|
||||
_cache.Upsert(new HullData { Name = "Station", HullType = HullType.Station, Shape = Solid(3), Durability = 1, Mass = 1000000 });
|
||||
_cache.Upsert(new HullData { Name = "Turret", HullType = HullType.Turret, Shape = Solid(7), Durability = 1, Mass = 50 });
|
||||
_cache.FlushAsync().Wait();
|
||||
var items = new ItemManager(_cache, new ProvenanceLedger(), Settings(), _ => { });
|
||||
var zone = new Zone(items, new PlanetSettings(), new ZonePack { Radius = 0 }, new GalaxyZone { Name = "Layers", Owner = null }, null);
|
||||
return (items, zone);
|
||||
}
|
||||
|
||||
private static EquippableItem Hull(ItemManager items, string name, int lot) => new EquippableItem
|
||||
{
|
||||
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>(name)), Durability = 1, Lot = lot
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void EachKindRidesItsLayer()
|
||||
{
|
||||
var (items, zone) = Build();
|
||||
var ship = new Ship(items, zone, Hull(items, "Ship", 1), new EntitySettings());
|
||||
var station = new OrbitalEntity(items, zone, Hull(items, "Station", 2), default, new EntitySettings());
|
||||
var turret = new OrbitalEntity(items, zone, Hull(items, "Turret", 3), default, new EntitySettings());
|
||||
foreach (var e in new Entity[] { ship, station, turret }) zone.Admit(e, piloted: false);
|
||||
ship.Position = float3(40, 0, 10);
|
||||
station.Position = float3(-30, 0, 20);
|
||||
turret.Position = float3(5, 0, -60);
|
||||
|
||||
zone.Update(.1f);
|
||||
|
||||
Assert.Equal(zone.GetHeight(ship.Position.xz) + ShipHeight, ship.Position.y);
|
||||
Assert.Equal(zone.GetHeight(station.Position.xz) + OrbitalHeight, station.Position.y);
|
||||
Assert.Equal(zone.GetHeight(turret.Position.xz) + OrbitalHeight, turret.Position.y);
|
||||
const float lead = .05f;
|
||||
foreach (var (e, layer) in new (Entity, float)[] { (ship, ShipHeight), (station, OrbitalHeight), (turret, OrbitalHeight) })
|
||||
{
|
||||
var drawn = DrawAhead.Position(e, lead);
|
||||
Assert.Equal(zone.GetHeight(drawn.xz, DrawAhead.Time(zone, lead)) + layer, drawn.y);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrbitalsAboveShipsAboveFloaters()
|
||||
{
|
||||
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot()).Read<GameplaySettings>("GameplaySettings");
|
||||
Assert.True(authored.OrbitalLayerHeight > authored.ShipLayerHeight);
|
||||
Assert.True(authored.ShipLayerHeight > authored.FloaterLayerHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AFloaterSitsOnTheFloaterLayer()
|
||||
{
|
||||
var (items, zone) = Build();
|
||||
var settings = items.GameplaySettings;
|
||||
foreach (var at in new[] { float2(0, 0), float2(37, -12) })
|
||||
foreach (var lead in new[] { 0f, .05f })
|
||||
Assert.Equal(DrawAhead.Height(zone, at, lead) + FloaterHeight, DrawAhead.FloaterHeight(zone, settings, at, lead));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HullKey26IsRetired()
|
||||
{
|
||||
var keys = new System.Collections.Generic.HashSet<int>();
|
||||
for (var t = typeof(HullData); t != null; t = t.BaseType)
|
||||
foreach (var m in t.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
foreach (var k in m.GetCustomAttributes(typeof(MessagePack.KeyAttribute), false).Cast<MessagePack.KeyAttribute>())
|
||||
if (k.IntKey.HasValue) keys.Add(k.IntKey.Value);
|
||||
Assert.NotEmpty(keys);
|
||||
Assert.DoesNotContain(26, keys);
|
||||
|
||||
using var shipped = new CultCache(TestCatalog.Registry());
|
||||
shipped.AddBackingStore(new SingleFileMessagePackBackingStore(TestCatalog.Repo, true), AetheriaStores.CatalogTypes);
|
||||
Assert.NotEmpty(shipped.GetAll<HullData>());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue