Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
9fd703c
Plan: run the in-engine suites on CI without the extraction
claude Oct 1, 2026
2d9b35a
Content gate skips obj, bin and .godot on Linux and macOS too
claude Oct 1, 2026
0c02a41
A2: nine unguarded suites skip without an install, eight menu suites …
claude Oct 1, 2026
ab0cc54
A1: --run-tests=tier:ci runs a checked-in list where a skip fails
claude Oct 1, 2026
df51565
tier:ci takes the seven menu suites that run without an install
claude Oct 1, 2026
518cb21
A4: forward-rotation and launch-direction-cache run without a chapter…
claude Oct 1, 2026
c4220b7
A3: CI runs the ci engine tier headless on Linux and fails on a failu…
claude Oct 1, 2026
86aefc7
B11: --synthetic-data reads an invented extraction tree written into …
claude Oct 1, 2026
acf55aa
Split three suites: their install-free legs run on tier:ci as -core s…
claude Oct 1, 2026
57d3c0b
B12, B13: an invented, armed stand-in plane flies the empty stage on …
claude Oct 1, 2026
334a5a2
C23: the synthetic tree opens the Original shell, and 17 Original-she…
claude Oct 1, 2026
075c1a3
B14: the synthetic tree carries a generated sound archive, and nine s…
claude Oct 1, 2026
b087642
C21: a Dogfight on the empty stage opens on a code-built spawn ring, …
claude Oct 1, 2026
286cd32
B15a: a suite missing an input the synthetic tree lacks SKIPs naming it
claude Oct 1, 2026
1118622
C22: a team Dogfight on the empty stage walks its own team blocks bes…
claude Oct 1, 2026
3bdae64
player_pfighter stand-in: the synthetic tree flies a second invented …
claude Oct 1, 2026
e00668a
B15b-1: the ordnance suites fly invented weapons, and fourteen join t…
claude Oct 1, 2026
fdd0a0c
B15b-2: twelve flight, AI, combat, sound and HUD suites read their ex…
claude Oct 1, 2026
1a7205c
B15b-3: six network suites read their lines off the table and fly the…
claude Oct 1, 2026
d64074b
Plan: hand-off note for the work left after the cloud orchestration
claude Oct 1, 2026
44e87ff
PROTOTYPE: Enhanced wave ocean on C1B (throwaway branch, not for main)
Laeresh Oct 5, 2026
38c9709
PROTOTYPE: break the ocean foam's lattice with a drifting patch field
Laeresh Oct 5, 2026
83b3d2e
Merge branch 'main' into enhanced-ocean-proto
Laeresh Oct 5, 2026
8210c04
PLAN-enhanced-ocean: the plan, from the C1B wave ocean prototype
Laeresh Oct 5, 2026
862f2d5
PLAN-enhanced-ocean A1: the wave ocean as a production module on C1B
Laeresh Oct 5, 2026
59f8526
PLAN-enhanced-ocean: C21's evidence from the per-chapter sea-level su…
Laeresh Oct 5, 2026
e50cf74
PLAN-enhanced-ocean: Decision 12, C21 covers every chapter with a sea
Laeresh Oct 5, 2026
70f538c
PLAN-enhanced-ocean A2: the Water Quality setting, flat or waves, app…
Laeresh Oct 5, 2026
9459e03
PLAN-enhanced-ocean B13: the ocean's mask bake in about 90 ms instead…
Laeresh Oct 5, 2026
5908975
PLAN-enhanced-ocean A3 (scripted half): graphics-ocean-switch on C1B
Laeresh Oct 5, 2026
f7c1e8b
PLAN-enhanced-ocean C21: the ocean on every chapter with a sea (C1, C…
Laeresh Oct 6, 2026
bd8affd
PLAN-enhanced-ocean C26: the hidden-node census and the mask-gated hide
Laeresh Oct 6, 2026
fa32a20
PLAN-enhanced-ocean A3: the leak check's C2B session runs at flat wat…
Laeresh Oct 6, 2026
7007e0a
PLAN-enhanced-ocean C25: the ocean matches the flat sheet at the shor…
Laeresh Oct 6, 2026
893dd30
PLAN-enhanced-ocean C22: ship calm zones shaped to the hull and its w…
Laeresh Oct 6, 2026
17c9b9d
Merge branch 'main' into enhanced-ocean-proto
Laeresh Oct 6, 2026
07cb0e8
PLAN-enhanced-ocean C26: the sea sheet hides only where the ocean draws
Laeresh Oct 6, 2026
962e350
PLAN-enhanced-ocean C23: the swell shows no lattice from altitude
Laeresh Oct 6, 2026
3f6a1f4
PLAN-enhanced-ocean C27: calm zones for the hulls a mission animation…
Laeresh Oct 6, 2026
9077d2c
PLAN-enhanced-ocean C23 follow-up: no grain from the short swell and …
Laeresh Oct 6, 2026
addaf4c
PLAN-enhanced-ocean B12: the ocean on the Steam Deck costs +6.6 to +7…
Laeresh Oct 6, 2026
d72a8bf
PLAN-enhanced-ocean: Decision 13, the spyglass disc shows the flat sea
Laeresh Oct 6, 2026
a6a2d44
PLAN-enhanced-ocean B11: the ocean's low-pass cost is +1.1 ms, inside…
Laeresh Oct 6, 2026
e66d2d3
PLAN-enhanced-ocean B14: the spyglass disc shows the flat sea, not th…
Laeresh Oct 6, 2026
140a58d
PLAN-enhanced-ocean C24: the ocean goldens, re-pinned and one new
Laeresh Oct 7, 2026
63c4bc0
PLAN-enhanced-ocean: Decision 14, a sea state per region is a follow-…
Laeresh Oct 7, 2026
135cb10
PLAN-enhanced-ocean review: a stale comment, a private XML summary, t…
Laeresh Oct 7, 2026
28291f6
PLAN-enhanced-ocean A3: the user's verdict, and Wave D for its look f…
Laeresh Oct 7, 2026
95599a4
PLAN-enhanced-ocean review: entries trimmed, one ocean count, the thr…
Laeresh Oct 7, 2026
b6e8b5c
PLAN-enhanced-ocean review: the ocean asks the animation runtime whic…
Laeresh Oct 7, 2026
f38802c
PLAN-enhanced-ocean D33: the coast fades gradually; the look ramp is …
Laeresh Oct 7, 2026
88448bd
PLAN-enhanced-ocean: D33's frame cost measured, and D32's foam refere…
Laeresh Oct 7, 2026
cd247c4
PLAN-enhanced-ocean D31: no sea texture, bent crests, and noise detai…
Laeresh Oct 7, 2026
7820c05
PLAN-enhanced-ocean D32: foam from the swell's Jacobian, on a hash th…
Laeresh Oct 8, 2026
aaa6ff5
Merge main into enhanced-ocean-proto
Laeresh Oct 8, 2026
a4ebec5
PLAN-enhanced-ocean Wave D: the four ocean goldens re-pinned
Laeresh Oct 8, 2026
88bdbdf
PLAN-enhanced-ocean: Wave E, an ocean lab and a shipped sea state per…
Laeresh Oct 8, 2026
ca5a95e
PLAN-enhanced-ocean E41: an ocean lab in --freecam and a shipped sea …
Laeresh Oct 8, 2026
3568715
PLAN-enhanced-ocean review 2: Save keeps the file, TargetsOf claims n…
Laeresh Oct 8, 2026
9a9d828
PLAN-enhanced-ocean review 2: the judgement calls the user ruled on
Laeresh Oct 8, 2026
df86a27
PLAN-enhanced-ocean: E42, a shader rewritten for Enhanced with no wea…
Laeresh Oct 8, 2026
5bdfc5d
E42: a shader rewritten for Enhanced is pinned until Godot has recomp…
Laeresh Oct 8, 2026
a44b9b6
E42: graphics-retext-compiles draws nothing before the rewrite
Laeresh Oct 8, 2026
50f9b04
PLAN-enhanced-ocean: E42 verified
Laeresh Oct 8, 2026
44d7e78
PLAN-enhanced-ocean: E42 points at #164 for the rewrite race
Laeresh Oct 8, 2026
f1e1dc0
PLAN-enhanced-ocean: E43, a raised water triangle shows and shadows o…
Laeresh Oct 8, 2026
9b89a82
ocean_seas.json: the user's sea per chapter, tuned in the ocean lab
Laeresh Oct 8, 2026
1db604e
Merge main into the CI engine suites plan
Laeresh Oct 8, 2026
34cd1d0
Headless limits: the dummy renderer's RID race until Godot 4.8
Laeresh Oct 8, 2026
36d7f6d
B15: player_fbrand and player_avenger fly the synthetic tree, and thr…
Laeresh Oct 8, 2026
6b2a351
C24: an invented mission scope and voice family, and nine weather, vo…
Laeresh Oct 8, 2026
c1d5a1e
C24: an invented effects family, and fourteen anim and effect suites …
Laeresh Oct 8, 2026
6c62100
B15: stand-ins under six more shipped airframe names and a turret tab…
Laeresh Oct 8, 2026
2e90545
E43: a harbour's raised water triangles are open sea the hide clears,…
Laeresh Oct 8, 2026
81fc9ec
Ocean seas: tests hold any shipped sea, and the three goldens the use…
Laeresh Oct 8, 2026
b98b019
PLAN-enhanced-ocean: E41 and E43 verified
Laeresh Oct 8, 2026
1d59b70
Merge main into enhanced-ocean-proto
Laeresh Oct 8, 2026
0d3ec93
Close PLAN-enhanced-ocean: a wave-displaced ocean in Enhanced mode on…
Laeresh Oct 8, 2026
3a43b10
Test harness: suites that build a GameSession share the run's decode …
Laeresh Oct 8, 2026
a52e000
Merge branch 'main' into enhanced-ocean-proto
Laeresh Oct 8, 2026
5c66e37
Close PLAN-ci-engine-suites: 196 in-engine suites run on CI with no g…
Laeresh Oct 8, 2026
7c1b3cd
Merge branch 'main' into claude/engine-test-assets-vgbml9
Laeresh Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 98 additions & 4 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# The checks a contributor can pass without the original game: the repo's content gate, dotnet
# format, StyleCop, and the unit tests (tests needing extracted game data skip themselves). The
# engine suites and goldens need Godot and an install, so they stay with RunTests.ps1 locally.
# Three OSes, because the scripts must work wherever a contributor runs them.
# The checks a contributor can pass without the original game. The checks job runs the repo's
# content gate, dotnet format, StyleCop, and the unit tests (tests needing extracted game data skip
# themselves) on three OSes, because the scripts must work wherever a contributor runs them. The
# engine job runs the in-engine suites on the ci tier headless on Linux with no extraction, through
# RunCiSuites.ps1; a listed suite that fails or skips turns it red. The rest of the engine catalog,
# the goldens and the hitch check need an install or a renderer, so they stay with RunTests.ps1.
name: Checks

on:
Expand Down Expand Up @@ -75,3 +77,95 @@ jobs:

- name: Unit tests
run: dotnet test CSVM/CSVM.sln --nologo

engine:
name: engine suites (tier:ci, headless)
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
shell: pwsh
env:
GODOT_RELEASE: 4.7-stable
GODOT_BUILD: Godot_v4.7-stable_mono_linux_x86_64
GODOT_EXE: Godot_v4.7-stable_mono_linux.x86_64
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

# Keyed on the release, so a version bump downloads and verifies afresh. A cached copy was
# verified against the release's SHA512-SUMS.txt before it was stored.
- name: Cache Godot
id: godot-cache
uses: actions/cache@v4
with:
path: ~/godot
key: godot-${{ env.GODOT_BUILD }}

- name: Download and verify Godot
if: steps.godot-cache.outputs.cache-hit != 'true'
run: |
$base = "https://github.com/godotengine/godot/releases/download/$env:GODOT_RELEASE"
$zip = "$env:GODOT_BUILD.zip"
$dir = Join-Path $HOME 'godot'
New-Item -ItemType Directory -Force $dir | Out-Null
Set-Location $dir
curl -fsSL --retry 3 -o $zip "$base/$zip"
if ($LASTEXITCODE) { exit $LASTEXITCODE }
curl -fsSL --retry 3 -o SHA512-SUMS.txt "$base/SHA512-SUMS.txt"
if ($LASTEXITCODE) { exit $LASTEXITCODE }
$rows = @(Get-Content SHA512-SUMS.txt | Where-Object { ($_ -split '\s+')[-1].TrimStart('*') -eq $zip })
if ($rows.Count -ne 1) { throw "SHA512-SUMS.txt lists $zip $($rows.Count) times, not once" }
$want = ($rows[0] -split '\s+')[0].ToLowerInvariant()
$got = (Get-FileHash -Algorithm SHA512 $zip).Hash.ToLowerInvariant()
if ($got -ne $want) { throw "$zip has SHA-512 $got; the release publishes $want" }
Write-Host "$zip matches the SHA-512 the release publishes"
# unzip, not Expand-Archive, which drops the executable bit.
unzip -q $zip
if ($LASTEXITCODE) { exit $LASTEXITCODE }
chmod +x "$env:GODOT_BUILD/$env:GODOT_EXE"
Remove-Item $zip, SHA512-SUMS.txt

- name: Locate Godot
run: |
$godot = Join-Path $HOME "godot/$env:GODOT_BUILD/$env:GODOT_EXE"
if (-not (Test-Path $godot)) { throw "No Godot executable at $godot" }
"GODOT=$godot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

# Without fontconfig Godot's Linux build logs an engine error on every font lookup, and
# RunCiSuites.ps1 refuses to start. Installed only when the runner image lacks it.
- name: fontconfig
run: |
if (-not (@(/sbin/ldconfig -p) | Where-Object { $_ -match 'libfontconfig\.so\.1' })) {
sudo apt-get update
sudo apt-get install -y libfontconfig1
}

- name: Build
run: dotnet build CSVM/CSVM.sln --nologo

- name: Import
run: |
& $env:GODOT --headless --path CSVM --import
if ($LASTEXITCODE) { exit $LASTEXITCODE }

- name: Engine suites
run: ./RunCiSuites.ps1 -Godot $env:GODOT

- name: Upload report and logs
if: always()
uses: actions/upload-artifact@v4
with:
name: engine-suites
# .scratch is a hidden folder, which upload-artifact skips unless told otherwise.
include-hidden-files: true
if-no-files-found: warn
path: |
.scratch/test-report.json
.scratch/ci-suites/godot.out
.scratch/ci-suites/godot.err
.scratch/ci-suites/godot.log
.scratch/logs/
43 changes: 43 additions & 0 deletions CSVM.Tests/LoadoutTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using CSVM.Flight.Weapons;
using Xunit;

Expand All @@ -13,6 +14,11 @@ namespace CSVM.Tests;
[Trait("Tier", "Quick")]
public class LoadoutTests
{
// Every firepoint and pylon the rig can carry, the shape the lab loadout covers.
private static readonly string[] FullRig = Enumerable.Range(1, 8)
.SelectMany(i => new[] { $"firepoint{i}", $"pylon{i}" })
.ToArray();

private static string ConfigPath =>
Path.Combine(TestData.RepoRoot, "CSVM", "data", "stock_loadouts.json");

Expand Down Expand Up @@ -205,6 +211,43 @@ public void ArmedIsTheOneSpellingAndHonoursInfiniteAmmo()
Assert.True(pylon.Armed(infinite: false));
}

[Fact]
public void TheRigLoadoutKeepsAWeaponTheStockFitNames()
{
// A named weapon sits outside the caliber matrix; rebuilding the slot from Caliber and Ammo
// alone binds a matrix weapon in its place. A slot stock leaves out takes the first gun's.
var stock = new LoadoutDef { Def = "rig_probe" };
stock.Guns.Add(new GunSpec { Slot = 1, Caliber = 30, Ammo = "slug", WeaponId = "wep_named" });
stock.Guns.Add(new GunSpec { Slot = 2, Caliber = 50, Ammo = "ap" });

var rig = Loadout.RigDef(FullRig, stock);

Assert.Equal(new[] { 1, 2, 3, 4 }, rig.Guns.Select(g => g.Slot));
Assert.Equal("wep_named", rig.Guns[0].WeaponId);
Assert.Null(rig.Guns[1].WeaponId);
Assert.Equal((50, "ap"), (rig.Guns[1].Caliber, rig.Guns[1].Ammo));
Assert.Equal("wep_named", rig.Guns[2].WeaponId);
Assert.Equal(new[] { "firepoint7", "firepoint8" }, rig.Guns[0].Markers);
}

[Fact]
public void EveryShippedFitsRigLoadoutStillComposesItsWeaponFromCaliberAndAmmo()
{
// No shipped fit names a weapon. Every rig slot therefore carries none, and Bind composes
// the matrix id from Caliber and Ammo.
foreach (var (def, loadout) in Load().All)
{
var rig = Loadout.RigDef(FullRig, loadout);
Assert.Equal(4, rig.Guns.Count);
foreach (var gun in rig.Guns)
{
var stockGun = loadout.Guns.FirstOrDefault(g => g.Slot == gun.Slot) ?? loadout.Guns[0];
Assert.True(gun.WeaponId == null, $"{def} slot {gun.Slot}: {gun.WeaponId}");
Assert.Equal((stockGun.Caliber, stockGun.Ammo), (gun.Caliber, gun.Ammo));
}
}
}

private static StockLoadouts Load() => StockLoadouts.Load(ConfigPath);

// A fit of N pylons, every one carrying the stock high explosive the hangar writes.
Expand Down
50 changes: 50 additions & 0 deletions CSVM.Tests/NameResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,56 @@ public void AWildcardInstancedDefsMissedNameReachesNoOtherNode()
Assert.Equal(1, resolver.GlobalTierRefused);
}

[Fact]
public void APeekResolvesLikeTheDispatchAndTalliesNothing()
{
// A read-only query asks the same tiers: the anchor's own node, the world answering an
// exact def, and the refusal a shared def's instance meets. Only the dispatch's form counts.
var crate01 = Node("crate01");
var crateHealthy = Node("healthy");
var crate02 = Node("crate02");
var stray = Node("door1");
var resolver = Build(
(crate01, "crate01", null),
(crateHealthy, "healthy", crate01),
(crate02, "crate02", null),
(stray, "door1", null));
var shared = Def("crate**", rootName: "healthy");
var healthy = new List<string> { "healthy" };
var door = new List<string> { "door1" };

Assert.Equal(new[] { crateHealthy }, resolver.PeekScoped(healthy, shared, crate01));
Assert.Empty(resolver.PeekScoped(healthy, shared, crate02));
Assert.Equal(new[] { stray }, resolver.PeekScoped(door, Def("crate01"), crate01));
Assert.Equal(0, resolver.GlobalTierRefused);
Assert.Equal(resolver.PeekScoped(healthy, shared, crate02), resolver.ResolveScoped(healthy, shared, crate02));
Assert.Equal(1, resolver.GlobalTierRefused);
}

[Fact]
public void APeekClaimsNothingAndLeavesTheLaterClaimOwed()
{
// A peek that memoized its answer unclaimed would hand the real query a memo hit, and the
// node would never be claimed. So the peek writes no memo, and the real query claims as usual.
var boat = Node("boat1");
var anchor = Node("harbour");
var resolver = Build((anchor, "harbour", null), (boat, "boat1", anchor));
var claimed = new List<TestNode>();
resolver.Claimed = claimed.Add;
var def = Def("harbour");
var path = new List<string> { "boat1" };

Assert.Equal(new TestNode?[] { anchor }, resolver.PeekAnchors(def));
Assert.Equal(new[] { boat }, resolver.PeekScoped(path, def, anchor));
Assert.Equal(new[] { boat }, resolver.Peek(() => resolver.FindAll("boat1", null)));
Assert.Empty(claimed);

Assert.Equal(new[] { boat }, resolver.ResolveScoped(path, def, anchor));
Assert.Equal(new[] { boat }, claimed);
Assert.Equal(new TestNode?[] { anchor }, resolver.Anchors(def));
Assert.Contains(anchor, claimed);
}

[Fact]
public void AnExactlyNamedDefStillFallsThroughToTheGlobalTier()
{
Expand Down
33 changes: 33 additions & 0 deletions CSVM.Tests/OceanBaseTextureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using CSVM.Mech3;
using Xunit;

namespace CSVM.Tests;

/// <summary>
/// Which water textures are the open-sea base sheet the Enhanced wave ocean replaces
/// (<see cref="SceneBuilder.IsOceanBaseTexture"/>). C1 and C1C paint their sea with
/// <c>water1</c>, and C1's opaque coast tiles <c>water1_trans1/2</c> sit beside it and must stay.
/// </summary>
public class OceanBaseTextureTests
{
[Theory]
[InlineData("wtr00000.tif")]
[InlineData("WTR00000.TIF")]
[InlineData("wtr00000")]
[InlineData("water1.tif")]
[InlineData("water1")]
[InlineData("Water1.tif")]
public void TheBaseSheetIsMatched(string texture) => Assert.True(SceneBuilder.IsOceanBaseTexture(texture));

[Theory]
[InlineData("water1_trans1.tif")]
[InlineData("water1_trans2.tif")]
[InlineData("water10.tif")]
[InlineData("water2.tif")]
[InlineData("srf0001.tif")]
[InlineData("wakefront1.tif")]
[InlineData("z3_foggrad.tif")]
[InlineData("beach1.tif")]
[InlineData("")]
public void CoastTilesSurfAndOverlaysAreNot(string texture) => Assert.False(SceneBuilder.IsOceanBaseTexture(texture));
}
91 changes: 91 additions & 0 deletions CSVM.Tests/OceanCalmZoneTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using CSVM.Effects;
using Godot;
using Xunit;

namespace CSVM.Tests;

/// <summary>
/// The ship calm zone the wave ocean flattens its waves in (<see cref="OceanCalmZone"/>). It is a
/// box along the hull's heading, grown over the waterline and the wake sheets' footprints. The
/// shader's smoothstep fades the waves back in past it.
/// </summary>
public class OceanCalmZoneTests
{
private const float Eps = 1e-3f;

[Fact]
public void AnEmptyZoneIsADiscAtItsOrigin()
{
var zone = new OceanCalmZone(new Vector2(100f, -40f), new Vector2(0f, 1f));
Assert.True(zone.IsEmpty);
Assert.Equal(Vector2.Zero, zone.HalfExtents);
Assert.Equal(5f, zone.Distance(new Vector2(103f, -36f)), Eps);
}

[Fact]
public void TheBoxRunsAlongTheHeading()
{
var zone = new OceanCalmZone(Vector2.Zero, new Vector2(0f, 3f));
zone.Add(new Vector2(-10f, -100f));
zone.Add(new Vector2(10f, 50f));
Assert.Equal(new Vector2(0f, 1f), zone.Axis);
Assert.Equal(75f, zone.HalfExtents.X, Eps);
Assert.Equal(10f, zone.HalfExtents.Y, Eps);
Assert.Equal(0f, zone.Center.X, Eps);
Assert.Equal(-25f, zone.Center.Y, Eps);
Assert.Equal(0f, zone.Distance(new Vector2(9f, -99f)), Eps);
Assert.Equal(5f, zone.Distance(new Vector2(0f, 55f)), Eps);
Assert.Equal(5f, zone.Distance(new Vector2(-15f, 0f)), Eps);
// Past a corner the distance is to the corner, which rounds the zone's ends.
Assert.Equal(5f, zone.Distance(new Vector2(13f, 54f)), Eps);
}

[Fact]
public void AMeshFootprintTurnsWithItsShip()
{
// A sheet 10 m wide and 100 m long, on a ship turned 30 degrees and moved off the origin.
var basis = new Basis(Vector3.Up, Mathf.DegToRad(30f));
var ship = new Transform3D(basis, new Vector3(-5000f, 0f, -10000f));
var sheet = new Aabb(new Vector3(-5f, 0.1f, -50f), new Vector3(10f, 9f, 100f));
var heading = new Vector2(basis.Z.X, basis.Z.Z);
var zone = new OceanCalmZone(new Vector2(ship.Origin.X, ship.Origin.Z), heading);
zone.Add(sheet, ship);
Assert.Equal(50f, zone.HalfExtents.X, Eps);
Assert.Equal(5f, zone.HalfExtents.Y, Eps);
Assert.Equal(-5000f, zone.Center.X, 0.01f);
Assert.Equal(-10000f, zone.Center.Y, 0.01f);
var bow = new Vector2(ship.Origin.X, ship.Origin.Z) + (heading.Normalized() * 60f);
Assert.Equal(10f, zone.Distance(bow), 0.01f);
}

[Fact]
public void AWakeBehindTheHullStretchesTheZoneOverBoth()
{
// The hull's waterline from -60 to 60 m along its heading, a wake sheet trailing it to -200.
var ship = new Transform3D(Basis.Identity, new Vector3(0f, 0f, 0f));
var zone = new OceanCalmZone(Vector2.Zero, new Vector2(0f, 1f));
zone.Add(new Aabb(new Vector3(-10f, -5f, -60f), new Vector3(20f, 10f, 120f)), ship);
zone.Add(new Aabb(new Vector3(-25f, 0.2f, -200f), new Vector3(50f, 0.1f, 140f)), ship);
Assert.Equal(130f, zone.HalfExtents.X, Eps);
Assert.Equal(25f, zone.HalfExtents.Y, Eps);
Assert.Equal(-70f, zone.Center.Y, Eps);
Assert.Equal(0f, zone.Distance(new Vector2(24f, -199f)), Eps);
}

[Fact]
public void AHeadingWithNoLengthFallsBackToX()
{
var zone = new OceanCalmZone(Vector2.Zero, Vector2.Zero);
Assert.Equal(new Vector2(1f, 0f), zone.Axis);
}

[Theory]
[InlineData(0f, 0f)]
[InlineData(6f, 0f)]
[InlineData(31f, 0.5f)]
[InlineData(56f, 1f)]
[InlineData(500f, 1f)]
[InlineData(18.5f, 0.15625f)]
public void TheFadeIsTheShadersSmoothstep(float distance, float expected) =>
Assert.Equal(expected, OceanCalmZone.Calm(distance, 6f, 50f), Eps);
}
Loading
Loading