Sync daemon authority migration
This commit is contained in:
parent
030b4b5d74
commit
39a0dc8737
295 changed files with 25121 additions and 7360 deletions
61
Assets/Scripts/Tests/CultMathPrimitiveTests.cs
Normal file
61
Assets/Scripts/Tests/CultMathPrimitiveTests.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using CultMath;
|
||||
using NUnit.Framework;
|
||||
using static CultMath.math;
|
||||
|
||||
public sealed class CultMathPrimitiveTests
|
||||
{
|
||||
[Test]
|
||||
public void Int2SupportsAetheriaGridArithmetic()
|
||||
{
|
||||
var cell = int2(2, 3);
|
||||
|
||||
Assert.AreEqual(new int2(3, 5), cell + int2(1, 2));
|
||||
Assert.AreEqual(new float2(2.0f, 3.0f), (float2)cell);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AetheriaMathRotatesItemDirectionsOnShipPlane()
|
||||
{
|
||||
var forward = float2(0, 1);
|
||||
|
||||
Assert.AreEqual(float2(-1, 0), AetheriaMath.Rotate(forward, ItemRotation.CounterClockwise));
|
||||
Assert.AreEqual(float2(0, -1), AetheriaMath.Rotate(forward, ItemRotation.Reversed));
|
||||
Assert.AreEqual(float2(1, 0), AetheriaMath.Rotate(forward, ItemRotation.Clockwise));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ItemRotationDirectionsAreCultVectors()
|
||||
{
|
||||
Assert.AreEqual(float2(0, 1), ItemRotation.None.Direction());
|
||||
Assert.AreEqual(float2(1, 0), ItemRotation.Clockwise.Direction());
|
||||
Assert.AreEqual(float2(0, -1), ItemRotation.Reversed.Direction());
|
||||
Assert.AreEqual(float2(-1, 0), ItemRotation.CounterClockwise.Direction());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AetheriaMathProjectsUnityVectorsOntoShipPlane()
|
||||
{
|
||||
var world = new Unity.Mathematics.float3(2, 99, 3);
|
||||
|
||||
Assert.AreEqual(float2(2, 3), AetheriaMath.ToCultXZ(world));
|
||||
Assert.AreEqual(new Unity.Mathematics.float3(2, 0, 3), AetheriaMath.ToUnityXZ(float2(2, 3)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AetheriaMathCalculatesStableAngles()
|
||||
{
|
||||
Assert.That(AetheriaMath.AngleDegrees(float2(1, 0), float2(0, 1)), Is.EqualTo(90).Within(0.0001f));
|
||||
Assert.That(AetheriaMath.AngleRadians(float2(1, 0), float2(0, 1)), Is.EqualTo(PI / 2).Within(0.0001f));
|
||||
Assert.That(AetheriaMath.AngleDegrees(float3(1, 0, 0), float3(1, 0, 0)), Is.EqualTo(0).Within(0.0001f));
|
||||
Assert.That(AetheriaMath.AngleDegrees(float3(0, 0, 0), float3(1, 0, 0)), Is.EqualTo(0).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ColorMathUsesCultVectorsAndSharedUnityBridge()
|
||||
{
|
||||
var rgb = ColorMath.HsvToRgb(float3(0, 1, 1));
|
||||
|
||||
Assert.AreEqual(float3(1, 0, 0), rgb);
|
||||
Assert.AreEqual(new Unity.Mathematics.float3(1, 0, 0), AetheriaMath.ToUnity(rgb));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Tests/CultMathPrimitiveTests.cs.meta
Normal file
2
Assets/Scripts/Tests/CultMathPrimitiveTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d4fc4667d3155a843b610a24d40fa6f0
|
||||
|
|
@ -196,6 +196,8 @@ public class DaemonRuntimeDocumentTests
|
|||
CollectionAssert.Contains(frame.Capabilities, AetheriaRuntimeDaemonSchemas.GameSurface);
|
||||
CollectionAssert.Contains(frame.Capabilities, AetheriaRuntimeDaemonSchemas.EditorSurface);
|
||||
CollectionAssert.AreEquivalent(result.OperationResult.AppliedCommandIds, frame.AppliedCommandIds);
|
||||
CollectionAssert.Contains(frame.AccountedCommandIds, targetCommand.CommandId);
|
||||
CollectionAssert.Contains(frame.AccountedCommandIds, movementCommand.CommandId);
|
||||
Assert.AreEqual(1, frame.Run.Zones[0].Entities[0].TargetEntityIndex);
|
||||
Assert.AreEqual(
|
||||
AetheriaRuntimeDaemonPublicationStore.GetProviderAdvertisementPath(statePath),
|
||||
|
|
@ -336,6 +338,47 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(AetheriaRuntimeDaemonEditorSurfaceBuilder.TuiSurfaceId, genericEditorTuiSurface.Surface.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TickRunnerSkipsPreviouslyAccountedObservedCommands()
|
||||
{
|
||||
var statePath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"aetheria-daemon-tick-runner-tests",
|
||||
Path.GetRandomFileName(),
|
||||
"state.cc");
|
||||
var run = RunWithTwoEntities();
|
||||
var targetCommand = AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.SetTarget,
|
||||
"codex",
|
||||
"session-tick",
|
||||
40,
|
||||
"zone.0.entity.0");
|
||||
targetCommand.TargetEntityKey = "zone.0.entity.1";
|
||||
|
||||
var result = AetheriaRuntimeDaemonTickRunner.Tick(
|
||||
statePath,
|
||||
run,
|
||||
new AetheriaRuntimeDaemonTickOptions
|
||||
{
|
||||
DaemonId = "test-daemon",
|
||||
SessionId = "session-tick",
|
||||
FrameId = 43,
|
||||
SimulationTimeSeconds = 12.52,
|
||||
FixedDeltaSeconds = 0.02,
|
||||
ObservedCommands = new[] { targetCommand },
|
||||
AccountedCommandIds = new[] { targetCommand.CommandId }
|
||||
});
|
||||
|
||||
Assert.AreEqual(0, result.OperationResult.AppliedCommandIds.Count);
|
||||
Assert.AreEqual(0, result.OperationResult.RejectedCommandIds.Count);
|
||||
Assert.AreEqual(-1, run.Zones[0].Entities[0].TargetEntityIndex);
|
||||
CollectionAssert.Contains(result.Frame.AccountedCommandIds, targetCommand.CommandId);
|
||||
Assert.IsTrue(AetheriaRuntimeDaemonPublicationStore.TryReadHealth(statePath, out var health));
|
||||
Assert.AreEqual(1, health.ObservedCommandCount);
|
||||
Assert.AreEqual(0, health.AppliedCommandCount);
|
||||
Assert.AreEqual(0, health.RejectedCommandCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SoaViewFactoryDescribesDaemonOwnedObserverReadOnlySlabs()
|
||||
{
|
||||
|
|
|
|||
2
Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs.meta
Normal file
2
Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: da50d4a5bedc04148a6912cff6d6f02d
|
||||
108
Assets/Scripts/Tests/StatRecipeSurfaceCommandTests.cs
Normal file
108
Assets/Scripts/Tests/StatRecipeSurfaceCommandTests.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Unity;
|
||||
using NUnit.Framework;
|
||||
|
||||
public class StatRecipeSurfaceCommandTests
|
||||
{
|
||||
private const string TestUpdatedAtUtc = "2026-06-18T00:00:00.0000000Z";
|
||||
|
||||
[Test]
|
||||
public void ToggleConditionCreatesEnabledInfluenceForSelectedRecipe()
|
||||
{
|
||||
var state = StateWithRecipe();
|
||||
|
||||
var next = AetheriaRuntimeStatRecipes.ToggleCondition(
|
||||
state,
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Range,
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
var influence = next.SelectedRecipe.Influences.Single();
|
||||
Assert.AreEqual(AetheriaRuntimeStatRecipeConditions.Range, influence.Condition);
|
||||
Assert.AreEqual(AetheriaRuntimeStatRecipeOperations.Add, influence.Operation);
|
||||
Assert.IsTrue(influence.Enabled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CycleInfluenceOperationAdvancesAddMultiplyOverride()
|
||||
{
|
||||
var state = AetheriaRuntimeStatRecipes.ToggleCondition(
|
||||
StateWithRecipe(),
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Quality,
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
state = AetheriaRuntimeStatRecipes.CycleInfluenceOperation(
|
||||
state,
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Quality,
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
Assert.AreEqual(AetheriaRuntimeStatRecipeOperations.Multiply, state.SelectedRecipe.Influences[0].Operation);
|
||||
|
||||
state = AetheriaRuntimeStatRecipes.CycleInfluenceOperation(
|
||||
state,
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Quality,
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
Assert.AreEqual(AetheriaRuntimeStatRecipeOperations.Override, state.SelectedRecipe.Influences[0].Operation);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetInfluenceAmountAndCurveUpdateOnlyTargetInfluence()
|
||||
{
|
||||
var state = AetheriaRuntimeStatRecipes.ToggleCondition(
|
||||
StateWithRecipe(),
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Heat,
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
state = AetheriaRuntimeStatRecipes.SetInfluenceAmount(
|
||||
state,
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Heat,
|
||||
3.5,
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
state = AetheriaRuntimeStatRecipes.SetInfluenceCurve(
|
||||
state,
|
||||
"weapon.damage",
|
||||
AetheriaRuntimeStatRecipeConditions.Heat,
|
||||
"easeOut",
|
||||
updatedAtUtc: TestUpdatedAtUtc);
|
||||
|
||||
var influence = state.SelectedRecipe.Influences.Single();
|
||||
Assert.AreEqual(3.5, influence.Amount, 0.0001);
|
||||
Assert.AreEqual("easeOut", influence.CurveLabel);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetPreviewConditionUpdatesRequestedPreviewAxis()
|
||||
{
|
||||
var next = AetheriaRuntimeStatRecipes.SetPreviewCondition(
|
||||
StateWithRecipe(),
|
||||
AetheriaRuntimeStatRecipeConditions.Charge,
|
||||
0.25,
|
||||
TestUpdatedAtUtc);
|
||||
|
||||
Assert.AreEqual(0.25, next.Preview.Charge, 0.0001);
|
||||
Assert.AreEqual(AetheriaRuntimeStatRecipePreviewState.Default.Quality, next.Preview.Quality, 0.0001);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipeSurfaceState StateWithRecipe()
|
||||
{
|
||||
return new AetheriaRuntimeStatRecipeSurfaceState(
|
||||
new[]
|
||||
{
|
||||
new AetheriaRuntimeStatRecipeState(
|
||||
"weapon.damage",
|
||||
"Weapon Damage",
|
||||
10,
|
||||
new AetheriaRuntimeStatInfluenceState[0])
|
||||
},
|
||||
"Weapon Damage",
|
||||
AetheriaRuntimeStatRecipePreviewState.Default,
|
||||
"");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 45bf0c07c6a63554a949f3605ff81d13
|
||||
254
Assets/Scripts/Tests/StatRecipeTests.cs
Normal file
254
Assets/Scripts/Tests/StatRecipeTests.cs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
using System;
|
||||
using GameCult.Aetheria.State.Unity;
|
||||
using NUnit.Framework;
|
||||
|
||||
public class StatRecipeTests
|
||||
{
|
||||
[Test]
|
||||
public void AddModifierScalesByConditionSample()
|
||||
{
|
||||
var recipe = new StatRecipe
|
||||
{
|
||||
BaseValue = 10,
|
||||
Modifiers = new[]
|
||||
{
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Quality,
|
||||
Operation = StatModifierOperation.Add,
|
||||
Amount = 6
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var value = recipe.Evaluate(new StatEvaluationContext(quality: 0.5f, durability: 1, heat: 1));
|
||||
|
||||
Assert.AreEqual(13, value, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MultiplyModifierInterpolatesFromIdentity()
|
||||
{
|
||||
var recipe = new StatRecipe
|
||||
{
|
||||
BaseValue = 10,
|
||||
Modifiers = new[]
|
||||
{
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Durability,
|
||||
Operation = StatModifierOperation.Multiply,
|
||||
Amount = 2
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var value = recipe.Evaluate(new StatEvaluationContext(quality: 1, durability: 0.25f, heat: 1));
|
||||
|
||||
Assert.AreEqual(12.5f, value, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverrideModifierInterpolatesTowardTargetValue()
|
||||
{
|
||||
var recipe = new StatRecipe
|
||||
{
|
||||
BaseValue = 10,
|
||||
Modifiers = new[]
|
||||
{
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Heat,
|
||||
Operation = StatModifierOperation.Override,
|
||||
Amount = 20
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var value = recipe.Evaluate(new StatEvaluationContext(quality: 1, durability: 1, heat: 0.25f));
|
||||
|
||||
Assert.AreEqual(12.5f, value, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DependencyMaskOnlyIncludesEnabledConditions()
|
||||
{
|
||||
var recipe = new StatRecipe
|
||||
{
|
||||
Modifiers = new[]
|
||||
{
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Quality,
|
||||
Enabled = false
|
||||
},
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Range,
|
||||
Enabled = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.AreEqual(StatConditionMask.Range, recipe.DependencyMask);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisabledAndConditionlessModifiersDoNotAffectEvaluation()
|
||||
{
|
||||
var recipe = new StatRecipe
|
||||
{
|
||||
BaseValue = 10,
|
||||
Modifiers = new[]
|
||||
{
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Quality,
|
||||
Operation = StatModifierOperation.Add,
|
||||
Amount = 100,
|
||||
Enabled = false
|
||||
},
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.None,
|
||||
Operation = StatModifierOperation.Add,
|
||||
Amount = 100,
|
||||
Enabled = true
|
||||
},
|
||||
new StatRecipeModifier
|
||||
{
|
||||
Condition = StatConditionMask.Heat,
|
||||
Operation = StatModifierOperation.Add,
|
||||
Amount = 4,
|
||||
Enabled = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var value = recipe.Evaluate(new StatEvaluationContext(quality: 1, durability: 1, heat: 0.25f));
|
||||
|
||||
Assert.AreEqual(11, value, 0.0001f);
|
||||
Assert.AreEqual(StatConditionMask.Heat, recipe.DependencyMask);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContextOverrideReplacesSingleCondition()
|
||||
{
|
||||
var context = new StatEvaluationContext(quality: 0.2f, durability: 0.3f, heat: 0.4f, range: 0.5f);
|
||||
|
||||
var next = context.WithCondition(StatConditionMask.Range, 0.75f);
|
||||
|
||||
Assert.AreEqual(0.2f, next.Quality, 0.0001f);
|
||||
Assert.AreEqual(0.3f, next.Durability, 0.0001f);
|
||||
Assert.AreEqual(0.4f, next.Heat, 0.0001f);
|
||||
Assert.AreEqual(0.75f, next.Range, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RecipeTokensMapCatalogStringsToRuntimeEnums()
|
||||
{
|
||||
Assert.AreEqual(StatConditionMask.Quality, StatRecipeTokens.ToConditionMask("quality"));
|
||||
Assert.AreEqual(StatConditionMask.PilotSkill, StatRecipeTokens.ToConditionMask("pilotSkill"));
|
||||
Assert.AreEqual(StatConditionMask.None, StatRecipeTokens.ToConditionMask("unknown"));
|
||||
Assert.AreEqual(StatModifierOperation.Multiply, StatRecipeTokens.ToModifierOperation("multiply"));
|
||||
Assert.AreEqual(StatModifierOperation.Override, StatRecipeTokens.ToModifierOperation("override"));
|
||||
Assert.AreEqual(StatModifierOperation.Add, StatRecipeTokens.ToModifierOperation("unknown"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LegacyStatDependencyMaskIncludesOnlyActiveLegacyInputs()
|
||||
{
|
||||
var stat = new PerformanceStat
|
||||
{
|
||||
QualityExponent = 2,
|
||||
DurabilityExponentMultiplier = 0,
|
||||
HeatExponentMultiplier = 3
|
||||
};
|
||||
|
||||
Assert.AreEqual(StatConditionMask.Quality, stat.GetDependencyMask(includeHeat: false));
|
||||
Assert.AreEqual(StatConditionMask.Quality | StatConditionMask.Heat, stat.GetDependencyMask(includeHeat: true));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LegacyStatEvaluationUsesExplicitContextExponents()
|
||||
{
|
||||
var stat = new PerformanceStat
|
||||
{
|
||||
Min = 0,
|
||||
Max = 100,
|
||||
QualityExponent = 2,
|
||||
DurabilityExponentMultiplier = 1,
|
||||
HeatExponentMultiplier = 1
|
||||
};
|
||||
|
||||
var value = stat.EvaluateRecipeOrLegacy(
|
||||
new StatEvaluationContext(quality: 0.5f, durability: 0.5f, heat: 0.5f),
|
||||
durabilityExponent: 2,
|
||||
thermalExponent: 2,
|
||||
includeHeat: true);
|
||||
|
||||
Assert.AreEqual(3.125f, value, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConsumableBaselinePreservesLinearEffectivenessCompatibility()
|
||||
{
|
||||
var stat = new PerformanceStat
|
||||
{
|
||||
Min = 10,
|
||||
Max = 30,
|
||||
QualityExponent = 2,
|
||||
DurabilityExponentMultiplier = 10
|
||||
};
|
||||
|
||||
var value = stat.EvaluateConsumableBaseline(
|
||||
new StatEvaluationContext(quality: 0.5f, durability: 0.5f, heat: 1));
|
||||
|
||||
Assert.AreEqual(12.5f, value, 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EntityModifierApplicationLeavesBaseValueWhenNoEntityModifiersExist()
|
||||
{
|
||||
var stat = new PerformanceStat();
|
||||
|
||||
Assert.AreEqual(42f, stat.ApplyEntityModifiers(null, 42f), 0.0001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BehaviorValueReaderReadsEnumFromString()
|
||||
{
|
||||
var value = BehaviorValue("multiply", 0);
|
||||
|
||||
var operation = AetheriaRuntimeBehaviorValueReader.ReadEnum(value, StatModifierOperation.Add);
|
||||
|
||||
Assert.AreEqual(StatModifierOperation.Multiply, operation);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BehaviorValueReaderReadsEnumFromNumberOrFallback()
|
||||
{
|
||||
var value = BehaviorValue("", (int)StatModifierOperation.Override);
|
||||
var unknown = BehaviorValue("", 999);
|
||||
|
||||
Assert.AreEqual(
|
||||
StatModifierOperation.Override,
|
||||
AetheriaRuntimeBehaviorValueReader.ReadEnum(value, StatModifierOperation.Add));
|
||||
Assert.AreEqual(
|
||||
StatModifierOperation.Add,
|
||||
AetheriaRuntimeBehaviorValueReader.ReadEnum(unknown, StatModifierOperation.Add));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue BehaviorValue(string stringValue, double numberValue)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"",
|
||||
stringValue,
|
||||
numberValue,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Tests/StatRecipeTests.cs.meta
Normal file
2
Assets/Scripts/Tests/StatRecipeTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 53a35efb3a0d3d646a1ea27845e4391f
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner",
|
||||
"Unity.Mathematics",
|
||||
"Aetheria.Shared.Unity"
|
||||
"CultMath",
|
||||
"Aetheria.Shared.Unity",
|
||||
"GameCult.Aetheria.State.Unity"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
|
|
@ -22,4 +24,4 @@
|
|||
],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
482
Assets/Scripts/Tests/YmirPhysicsQueryTests.cs
Normal file
482
Assets/Scripts/Tests/YmirPhysicsQueryTests.cs
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
|
||||
public sealed class YmirPhysicsQueryTests
|
||||
{
|
||||
[Test]
|
||||
public void StepIntegratesDynamicBodiesWithoutMutatingInputWorld()
|
||||
{
|
||||
var body = PhysicsBody("ship", 1, 2, 1);
|
||||
body.velocity = Vec2(3, -4);
|
||||
var world = World(body);
|
||||
|
||||
var result = YmirPhysicsQueries.Step(world, 0.5f);
|
||||
|
||||
Assert.That(result.world.time, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(result.world.bodies[0].position.x, Is.EqualTo(2.5f).Within(0.0001f));
|
||||
Assert.That(result.world.bodies[0].position.y, Is.EqualTo(0.0f).Within(0.0001f));
|
||||
Assert.That(world.bodies[0].position.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
Assert.That(world.bodies[0].position.y, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StepAppliesRadialFieldsWithLinearFalloff()
|
||||
{
|
||||
var body = PhysicsBody("ship", 5, 0, 1);
|
||||
var world = World(body);
|
||||
world.fields = new[]
|
||||
{
|
||||
new YmirRadialField
|
||||
{
|
||||
id = "push",
|
||||
position = Vec2(0, 0),
|
||||
strength = 10,
|
||||
radius = 10
|
||||
}
|
||||
};
|
||||
|
||||
var result = YmirPhysicsQueries.Step(world, 1.0f);
|
||||
|
||||
Assert.That(result.world.bodies[0].velocity.x, Is.EqualTo(5.0f).Within(0.0001f));
|
||||
Assert.That(result.world.bodies[0].position.x, Is.EqualTo(10.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StepReportsContactsAndSeparatesDynamicBodyFromStaticBody()
|
||||
{
|
||||
var mover = PhysicsBody("mover", 0, 0, 1);
|
||||
mover.velocity = Vec2(2, 0);
|
||||
mover.mass = 1;
|
||||
var wall = PhysicsBody("wall", 1.5f, 0, 1);
|
||||
wall.isStatic = true;
|
||||
var world = World(mover, wall);
|
||||
|
||||
var result = YmirPhysicsQueries.Step(world, 0.1f);
|
||||
|
||||
Assert.That(result.contacts, Has.Length.EqualTo(1));
|
||||
Assert.AreEqual("mover", result.contacts[0].bodyA);
|
||||
Assert.AreEqual("wall", result.contacts[0].bodyB);
|
||||
Assert.That(result.contacts[0].normal.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
Assert.That(result.contacts[0].penetration, Is.GreaterThan(0));
|
||||
Assert.That(result.world.bodies[0].position.x, Is.EqualTo(-0.5f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StepSkipsInvalidInput()
|
||||
{
|
||||
var result = YmirPhysicsQueries.Step((YmirStepRequest)null);
|
||||
|
||||
Assert.That(result.world.bodies, Is.Empty);
|
||||
Assert.That(result.contacts, Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.Step(World(PhysicsBody("ship", 1, 0, 1)), -1).contacts, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapSphereReturnsHitsSortedBySurfaceDistance()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.OverlapSphere(
|
||||
new[]
|
||||
{
|
||||
Body("far", 4, 0, 0, 2),
|
||||
Body("near", 1, 0, 0, 0.25f),
|
||||
Body("miss", 10, 0, 0, 1)
|
||||
},
|
||||
Vec3(0, 0, 0),
|
||||
3);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("near", hits[0].bodyId);
|
||||
Assert.AreEqual("far", hits[1].bodyId);
|
||||
Assert.That(hits[0].distance, Is.EqualTo(0.75f).Within(0.0001f));
|
||||
Assert.That(hits[1].distance, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapSphereBreaksEqualDistanceTiesByBodyId()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.OverlapSphere(
|
||||
new[]
|
||||
{
|
||||
Body("b", -2, 0, 0, 1),
|
||||
Body("a", 2, 0, 0, 1)
|
||||
},
|
||||
Vec3(0, 0, 0),
|
||||
3);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("a", hits[0].bodyId);
|
||||
Assert.AreEqual("b", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapSphereReportsPenetrationAndContactPoint()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.OverlapSphere(
|
||||
new[] { Body("target", 2, 0, 0, 1) },
|
||||
Vec3(0, 0, 0),
|
||||
3);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(1));
|
||||
Assert.That(hits[0].penetration, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].normal.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].point.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapSphereCanReuseCallerOwnedHitBuffer()
|
||||
{
|
||||
var hits = new List<YmirSphereOverlapHit> { new YmirSphereOverlapHit { bodyId = "stale" } };
|
||||
|
||||
var count = YmirPhysicsQueries.OverlapSphere(
|
||||
new[]
|
||||
{
|
||||
Body("b", -2, 0, 0, 1),
|
||||
Body("a", 2, 0, 0, 1)
|
||||
},
|
||||
Vec3(0, 0, 0),
|
||||
3,
|
||||
hits);
|
||||
|
||||
Assert.That(count, Is.EqualTo(2));
|
||||
Assert.That(hits, Has.Count.EqualTo(2));
|
||||
Assert.AreEqual("a", hits[0].bodyId);
|
||||
Assert.AreEqual("b", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapSphereSkipsInvalidInput()
|
||||
{
|
||||
Assert.That(YmirPhysicsQueries.OverlapSphere(null, Vec3(0, 0, 0), 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.OverlapSphere(new[] { Body("bad", 0, 0, 0, -1) }, Vec3(0, 0, 0), 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.OverlapSphere(new[] { Body("target", 0, 0, 0, 1) }, Vec3(0, 0, 0), -1), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapCircleReturnsHitsSortedBySurfaceDistance()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.OverlapCircle(
|
||||
new[]
|
||||
{
|
||||
PhysicsBody("far", 4, 0, 2),
|
||||
PhysicsBody("near", 1, 0, 0.25f),
|
||||
PhysicsBody("miss", 10, 0, 1)
|
||||
},
|
||||
Vec2(0, 0),
|
||||
3);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("near", hits[0].bodyId);
|
||||
Assert.AreEqual("far", hits[1].bodyId);
|
||||
Assert.That(hits[0].distance, Is.EqualTo(0.75f).Within(0.0001f));
|
||||
Assert.That(hits[1].distance, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapCircleBreaksEqualDistanceTiesByBodyId()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.OverlapCircle(
|
||||
new[]
|
||||
{
|
||||
PhysicsBody("b", -2, 0, 1),
|
||||
PhysicsBody("a", 2, 0, 1)
|
||||
},
|
||||
Vec2(0, 0),
|
||||
3);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("a", hits[0].bodyId);
|
||||
Assert.AreEqual("b", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapCircleReportsPenetrationAndContactPoint()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.OverlapCircle(
|
||||
new[] { PhysicsBody("target", 2, 0, 1) },
|
||||
Vec2(0, 0),
|
||||
3);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(1));
|
||||
Assert.That(hits[0].penetration, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].normal.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].point.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapCircleCanReuseCallerOwnedHitBuffer()
|
||||
{
|
||||
var hits = new List<YmirCircleOverlapHit> { new YmirCircleOverlapHit { bodyId = "stale" } };
|
||||
|
||||
var count = YmirPhysicsQueries.OverlapCircle(
|
||||
new[]
|
||||
{
|
||||
PhysicsBody("b", -2, 0, 1),
|
||||
PhysicsBody("a", 2, 0, 1)
|
||||
},
|
||||
Vec2(0, 0),
|
||||
3,
|
||||
hits);
|
||||
|
||||
Assert.That(count, Is.EqualTo(2));
|
||||
Assert.That(hits, Has.Count.EqualTo(2));
|
||||
Assert.AreEqual("a", hits[0].bodyId);
|
||||
Assert.AreEqual("b", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OverlapCircleSkipsInvalidInput()
|
||||
{
|
||||
Assert.That(YmirPhysicsQueries.OverlapCircle(null, Vec2(0, 0), 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.OverlapCircle(new[] { PhysicsBody("bad", 0, 0, -1) }, Vec2(0, 0), 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.OverlapCircle(new[] { PhysicsBody("target", 0, 0, 1) }, Vec2(0, 0), -1), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastSphereReturnsHitsSortedByImpactDistance()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastSphere(
|
||||
new[]
|
||||
{
|
||||
Body("far", 8, 0, 0, 1),
|
||||
Body("near", 4, 0, 0, 1),
|
||||
Body("miss", 4, 4, 0, 1)
|
||||
},
|
||||
Vec3(0, 0, 0),
|
||||
Vec3(1, 0, 0),
|
||||
10,
|
||||
1);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("near", hits[0].bodyId);
|
||||
Assert.AreEqual("far", hits[1].bodyId);
|
||||
Assert.That(hits[0].distance, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
Assert.That(hits[1].distance, Is.EqualTo(6.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastSphereBreaksEqualDistanceTiesByBodyId()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastSphere(
|
||||
new[]
|
||||
{
|
||||
Body("b", 4, 1, 0, 1),
|
||||
Body("a", 4, -1, 0, 1)
|
||||
},
|
||||
Vec3(0, 0, 0),
|
||||
Vec3(1, 0, 0),
|
||||
10,
|
||||
0);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("a", hits[0].bodyId);
|
||||
Assert.AreEqual("b", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastSphereReportsImpactPointAndNormal()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastSphere(
|
||||
new[] { Body("target", 4, 0, 0, 1) },
|
||||
Vec3(0, 0, 0),
|
||||
Vec3(2, 0, 0),
|
||||
10,
|
||||
1);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(1));
|
||||
Assert.That(hits[0].distance, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].normal.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].point.x, Is.EqualTo(3.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastSphereReportsInitialOverlapAtZeroDistance()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastSphere(
|
||||
new[] { Body("inside", 0.5f, 0, 0, 1) },
|
||||
Vec3(0, 0, 0),
|
||||
Vec3(1, 0, 0),
|
||||
10,
|
||||
1);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(1));
|
||||
Assert.That(hits[0].distance, Is.EqualTo(0.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastSphereCanReuseCallerOwnedHitBuffer()
|
||||
{
|
||||
var hits = new List<YmirSphereCastHit> { new YmirSphereCastHit { bodyId = "stale" } };
|
||||
|
||||
var count = YmirPhysicsQueries.CastSphere(
|
||||
new[]
|
||||
{
|
||||
Body("far", 8, 0, 0, 1),
|
||||
Body("near", 4, 0, 0, 1)
|
||||
},
|
||||
Vec3(0, 0, 0),
|
||||
Vec3(1, 0, 0),
|
||||
10,
|
||||
1,
|
||||
hits);
|
||||
|
||||
Assert.That(count, Is.EqualTo(2));
|
||||
Assert.That(hits, Has.Count.EqualTo(2));
|
||||
Assert.AreEqual("near", hits[0].bodyId);
|
||||
Assert.AreEqual("far", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastSphereSkipsInvalidInput()
|
||||
{
|
||||
Assert.That(YmirPhysicsQueries.CastSphere(null, Vec3(0, 0, 0), Vec3(1, 0, 0), 1, 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.CastSphere(new[] { Body("target", 0, 0, 0, 1) }, Vec3(0, 0, 0), Vec3(0, 0, 0), 1, 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.CastSphere(new[] { Body("target", 0, 0, 0, 1) }, Vec3(0, 0, 0), Vec3(1, 0, 0), -1, 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.CastSphere(new[] { Body("bad", 0, 0, 0, -1) }, Vec3(0, 0, 0), Vec3(1, 0, 0), 1, 1), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastCircleReturnsHitsSortedByImpactDistance()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastCircle(
|
||||
new[]
|
||||
{
|
||||
PhysicsBody("far", 8, 0, 1),
|
||||
PhysicsBody("near", 4, 0, 1),
|
||||
PhysicsBody("miss", 4, 4, 1)
|
||||
},
|
||||
Vec2(0, 0),
|
||||
Vec2(1, 0),
|
||||
10,
|
||||
1);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("near", hits[0].bodyId);
|
||||
Assert.AreEqual("far", hits[1].bodyId);
|
||||
Assert.That(hits[0].distance, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
Assert.That(hits[1].distance, Is.EqualTo(6.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastCircleBreaksEqualDistanceTiesByBodyId()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastCircle(
|
||||
new[]
|
||||
{
|
||||
PhysicsBody("b", 4, 1, 1),
|
||||
PhysicsBody("a", 4, -1, 1)
|
||||
},
|
||||
Vec2(0, 0),
|
||||
Vec2(1, 0),
|
||||
10,
|
||||
0);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(2));
|
||||
Assert.AreEqual("a", hits[0].bodyId);
|
||||
Assert.AreEqual("b", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastCircleReportsImpactPointAndNormal()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastCircle(
|
||||
new[] { PhysicsBody("target", 4, 0, 1) },
|
||||
Vec2(0, 0),
|
||||
Vec2(2, 0),
|
||||
10,
|
||||
1);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(1));
|
||||
Assert.That(hits[0].distance, Is.EqualTo(2.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].normal.x, Is.EqualTo(1.0f).Within(0.0001f));
|
||||
Assert.That(hits[0].point.x, Is.EqualTo(3.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastCircleReportsInitialOverlapAtZeroDistance()
|
||||
{
|
||||
var hits = YmirPhysicsQueries.CastCircle(
|
||||
new[] { PhysicsBody("inside", 0.5f, 0, 1) },
|
||||
Vec2(0, 0),
|
||||
Vec2(1, 0),
|
||||
10,
|
||||
1);
|
||||
|
||||
Assert.That(hits, Has.Length.EqualTo(1));
|
||||
Assert.That(hits[0].distance, Is.EqualTo(0.0f).Within(0.0001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastCircleCanReuseCallerOwnedHitBuffer()
|
||||
{
|
||||
var hits = new List<YmirCircleCastHit> { new YmirCircleCastHit { bodyId = "stale" } };
|
||||
|
||||
var count = YmirPhysicsQueries.CastCircle(
|
||||
new[]
|
||||
{
|
||||
PhysicsBody("far", 8, 0, 1),
|
||||
PhysicsBody("near", 4, 0, 1)
|
||||
},
|
||||
Vec2(0, 0),
|
||||
Vec2(1, 0),
|
||||
10,
|
||||
1,
|
||||
hits);
|
||||
|
||||
Assert.That(count, Is.EqualTo(2));
|
||||
Assert.That(hits, Has.Count.EqualTo(2));
|
||||
Assert.AreEqual("near", hits[0].bodyId);
|
||||
Assert.AreEqual("far", hits[1].bodyId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CastCircleSkipsInvalidInput()
|
||||
{
|
||||
Assert.That(YmirPhysicsQueries.CastCircle(null, Vec2(0, 0), Vec2(1, 0), 1, 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.CastCircle(new[] { PhysicsBody("target", 0, 0, 1) }, Vec2(0, 0), Vec2(0, 0), 1, 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.CastCircle(new[] { PhysicsBody("target", 0, 0, 1) }, Vec2(0, 0), Vec2(1, 0), -1, 1), Is.Empty);
|
||||
Assert.That(YmirPhysicsQueries.CastCircle(new[] { PhysicsBody("bad", 0, 0, -1) }, Vec2(0, 0), Vec2(1, 0), 1, 1), Is.Empty);
|
||||
}
|
||||
|
||||
private static YmirSphereQueryBody Body(string id, float x, float y, float z, float radius)
|
||||
{
|
||||
return new YmirSphereQueryBody
|
||||
{
|
||||
id = id,
|
||||
position = Vec3(x, y, z),
|
||||
radius = radius
|
||||
};
|
||||
}
|
||||
|
||||
private static YmirPhysicsBody PhysicsBody(string id, float x, float y, float radius)
|
||||
{
|
||||
return new YmirPhysicsBody
|
||||
{
|
||||
id = id,
|
||||
position = Vec2(x, y),
|
||||
direction = Vec2(1, 0),
|
||||
radius = radius,
|
||||
mass = 1,
|
||||
momentOfInertia = 1
|
||||
};
|
||||
}
|
||||
|
||||
private static YmirWorld World(params YmirPhysicsBody[] bodies)
|
||||
{
|
||||
return new YmirWorld
|
||||
{
|
||||
time = 0,
|
||||
bodies = bodies,
|
||||
fields = new YmirRadialField[0]
|
||||
};
|
||||
}
|
||||
|
||||
private static YmirVec3 Vec3(float x, float y, float z)
|
||||
{
|
||||
return new YmirVec3 { x = x, y = y, z = z };
|
||||
}
|
||||
|
||||
private static YmirVec2 Vec2(float x, float y)
|
||||
{
|
||||
return new YmirVec2 { x = x, y = y };
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Tests/YmirPhysicsQueryTests.cs.meta
Normal file
2
Assets/Scripts/Tests/YmirPhysicsQueryTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 97ea5fc21683ebf409292b94fcb4cf6a
|
||||
Loading…
Reference in a new issue