@plasius/gpu-shared 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  resolveShowcaseAssetUrl
3
- } from "./chunk-S5NCFNKJ.js";
3
+ } from "./chunk-OTCJ3VOK.js";
4
4
  import {
5
5
  loadGltfModel
6
- } from "./chunk-UUJLYYQS.js";
6
+ } from "./chunk-QBMXJ3V2.js";
7
7
  import {
8
8
  __require
9
9
  } from "./chunk-DGUM43GV.js";
@@ -4935,7 +4935,7 @@ function createGpuDebugSession(options = {}) {
4935
4935
  };
4936
4936
  }
4937
4937
 
4938
- // node_modules/@plasius/gpu-physics/dist/chunk-CNTXT5QJ.js
4938
+ // node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js
4939
4939
  var physicsDebugOwner = "physics";
4940
4940
  var physicsWorkerQueueClasses = Object.freeze({
4941
4941
  simulation: "simulation",
@@ -5763,7 +5763,7 @@ var physicsWorkerDagSpecs = {
5763
5763
  contactVisuals: { priority: 1, dependencies: ["worldSnapshot"] }
5764
5764
  }
5765
5765
  };
5766
- var physicsSimulationPlanSpecs = Object.freeze({
5766
+ var physicsSimulationPlans = Object.freeze({
5767
5767
  gameplay: Object.freeze({
5768
5768
  description: "Gameplay simulation plan that hands off a stable post-commit world snapshot to visual preparation.",
5769
5769
  snapshotStageId: "worldSnapshot",
@@ -6039,7 +6039,7 @@ function getPhysicsWorkerManifest(profile = defaultPhysicsWorkerProfile) {
6039
6039
  return manifest;
6040
6040
  }
6041
6041
  function createPhysicsSimulationPlan(profile = defaultPhysicsWorkerProfile) {
6042
- const plan = physicsSimulationPlanSpecs[profile];
6042
+ const plan = physicsSimulationPlans[profile];
6043
6043
  if (!plan) {
6044
6044
  const available = physicsWorkerProfileNames.join(", ");
6045
6045
  throw new Error(
@@ -6106,8 +6106,16 @@ function createPhysicsWorldSnapshot(input) {
6106
6106
 
6107
6107
  // src/showcase-runtime.js
6108
6108
  var STYLE_ID = "plasius-shared-3d-showcase-style";
6109
+ var ROOT_CLASS = "plasius-showcase-root";
6109
6110
  var DEFAULT_TITLE = "Flag by the Sea";
6110
6111
  var DEFAULT_SUBTITLE = "Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
6112
+ var SHIP_SCALE = 1.1;
6113
+ var HARBOR_BOUNDS = Object.freeze({
6114
+ minX: -11.2,
6115
+ maxX: 11.2,
6116
+ minZ: 1.8,
6117
+ maxZ: 17.2
6118
+ });
6111
6119
  var CAMERA_PRESETS = Object.freeze({
6112
6120
  integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
6113
6121
  lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
@@ -6119,10 +6127,10 @@ var CAMERA_PRESETS = Object.freeze({
6119
6127
  });
6120
6128
  var showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
6121
6129
  var SCENE_NOTES = Object.freeze([
6122
- "Ships are loaded from a GLTF asset and carry physics metadata from node extras.",
6123
- "Near-field lighting uses the ray-traced-primary shadow and reflection path before stepping down by distance band.",
6124
- "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands.",
6125
- "Performance pressure reduces visual detail before authoritative collision motion is touched."
6130
+ "Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
6131
+ "Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
6132
+ "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
6133
+ "Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched."
6126
6134
  ]);
6127
6135
  var UNIT_BOX_MESH = Object.freeze({
6128
6136
  positions: Object.freeze([
@@ -6190,6 +6198,17 @@ var UNIT_BOX_MESH = Object.freeze({
6190
6198
  0
6191
6199
  ])
6192
6200
  });
6201
+ var SHIP_LANTERNS = Object.freeze([
6202
+ Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
6203
+ Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
6204
+ Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
6205
+ Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
6206
+ ]);
6207
+ var HARBOR_TORCHES = Object.freeze([
6208
+ Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
6209
+ Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
6210
+ Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
6211
+ ]);
6193
6212
  function injectStyles() {
6194
6213
  if (document.getElementById(STYLE_ID)) {
6195
6214
  return;
@@ -6197,27 +6216,27 @@ function injectStyles() {
6197
6216
  const style = document.createElement("style");
6198
6217
  style.id = STYLE_ID;
6199
6218
  style.textContent = `
6200
- :root {
6201
- color-scheme: light;
6202
- --plasius-paper: #f4f7f8;
6203
- --plasius-ink: #152028;
6204
- --plasius-muted: #5c6f7b;
6205
- --plasius-accent: #8f5634;
6206
- --plasius-panel: rgba(255, 255, 255, 0.82);
6207
- --plasius-border: rgba(21, 32, 40, 0.12);
6208
- --plasius-shadow: 0 20px 48px rgba(15, 24, 31, 0.16);
6209
- }
6210
- * {
6211
- box-sizing: border-box;
6212
- }
6213
- body {
6219
+ .${ROOT_CLASS} {
6220
+ color-scheme: dark;
6221
+ --plasius-paper: #081321;
6222
+ --plasius-ink: #edf4ff;
6223
+ --plasius-muted: #b6c5dd;
6224
+ --plasius-accent: #f3b16a;
6225
+ --plasius-panel: rgba(8, 19, 33, 0.72);
6226
+ --plasius-border: rgba(159, 185, 223, 0.18);
6227
+ --plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
6214
6228
  margin: 0;
6215
- min-height: 100vh;
6229
+ min-height: 100%;
6216
6230
  font-family: "Fraunces", "Iowan Old Style", serif;
6217
6231
  color: var(--plasius-ink);
6218
6232
  background:
6219
- radial-gradient(circle at top left, rgba(255, 247, 238, 0.92), transparent 34%),
6220
- linear-gradient(180deg, #f6f8fb 0%, #d2dee6 48%, #b6c4ce 100%);
6233
+ radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
6234
+ radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
6235
+ linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
6236
+ }
6237
+ .${ROOT_CLASS},
6238
+ .${ROOT_CLASS} * {
6239
+ box-sizing: border-box;
6221
6240
  }
6222
6241
  .plasius-demo {
6223
6242
  width: min(1560px, calc(100vw - 32px));
@@ -6251,7 +6270,7 @@ function injectStyles() {
6251
6270
  text-transform: uppercase;
6252
6271
  letter-spacing: 0.18em;
6253
6272
  font-size: 12px;
6254
- color: rgba(21, 32, 40, 0.56);
6273
+ color: rgba(226, 236, 255, 0.58);
6255
6274
  }
6256
6275
  .plasius-demo h1,
6257
6276
  .plasius-demo h2,
@@ -6269,7 +6288,7 @@ function injectStyles() {
6269
6288
  margin: 0;
6270
6289
  padding: 8px 12px;
6271
6290
  border-radius: 999px;
6272
- background: rgba(143, 86, 52, 0.12);
6291
+ background: rgba(243, 177, 106, 0.14);
6273
6292
  color: var(--plasius-accent);
6274
6293
  font-weight: 700;
6275
6294
  }
@@ -6291,8 +6310,8 @@ function injectStyles() {
6291
6310
  aspect-ratio: 16 / 9;
6292
6311
  display: block;
6293
6312
  border-radius: 20px;
6294
- border: 1px solid rgba(21, 32, 40, 0.08);
6295
- background: linear-gradient(180deg, #dce8ef 0%, #a9bfd0 42%, #0f5168 42%, #092433 100%);
6313
+ border: 1px solid rgba(159, 185, 223, 0.12);
6314
+ background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
6296
6315
  }
6297
6316
  .plasius-demo__toolbar {
6298
6317
  position: absolute;
@@ -6312,9 +6331,9 @@ function injectStyles() {
6312
6331
  .plasius-demo button,
6313
6332
  .plasius-demo .plasius-toggle,
6314
6333
  .plasius-demo select {
6315
- border: 1px solid rgba(21, 32, 40, 0.12);
6334
+ border: 1px solid rgba(159, 185, 223, 0.18);
6316
6335
  border-radius: 999px;
6317
- background: rgba(255, 255, 255, 0.84);
6336
+ background: rgba(9, 20, 34, 0.84);
6318
6337
  color: var(--plasius-ink);
6319
6338
  padding: 10px 14px;
6320
6339
  }
@@ -6353,8 +6372,8 @@ function injectStyles() {
6353
6372
  bottom: 24px;
6354
6373
  padding: 10px 14px;
6355
6374
  border-radius: 16px;
6356
- background: rgba(255, 255, 255, 0.84);
6357
- border: 1px solid rgba(21, 32, 40, 0.1);
6375
+ background: rgba(9, 20, 34, 0.82);
6376
+ border: 1px solid rgba(159, 185, 223, 0.16);
6358
6377
  color: var(--plasius-muted);
6359
6378
  font-size: 12px;
6360
6379
  line-height: 1.45;
@@ -6366,7 +6385,7 @@ function injectStyles() {
6366
6385
  }
6367
6386
  .plasius-demo__footer {
6368
6387
  margin-top: 4px;
6369
- color: rgba(21, 32, 40, 0.66);
6388
+ color: rgba(226, 236, 255, 0.68);
6370
6389
  font-size: 13px;
6371
6390
  line-height: 1.6;
6372
6391
  }
@@ -6385,6 +6404,10 @@ function clamp(value, min, max) {
6385
6404
  function mix(a, b, t) {
6386
6405
  return a + (b - a) * t;
6387
6406
  }
6407
+ function pseudoRandom(seed) {
6408
+ const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
6409
+ return value - Math.floor(value);
6410
+ }
6388
6411
  function vec3(x = 0, y = 0, z = 0) {
6389
6412
  return { x, y, z };
6390
6413
  }
@@ -6418,6 +6441,12 @@ function reflectVec3(vector, normal) {
6418
6441
  const unitNormal = normalizeVec3(normal);
6419
6442
  return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
6420
6443
  }
6444
+ function directionFromYaw(yaw) {
6445
+ return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
6446
+ }
6447
+ function perpendicularOnWater(direction) {
6448
+ return vec3(-direction.z, 0, direction.x);
6449
+ }
6421
6450
  function rotateY(point, angle) {
6422
6451
  const cosine = Math.cos(angle);
6423
6452
  const sine = Math.sin(angle);
@@ -6600,7 +6629,7 @@ function buildDemoDom(root, options) {
6600
6629
  <section class="plasius-panel plasius-demo__status">
6601
6630
  <p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene\u2026</p>
6602
6631
  <p id="demoDetails" class="plasius-demo__status-text">
6603
- Preparing GLTF assets, cloth and fluid continuity plans, and adaptive quality metadata.
6632
+ Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
6604
6633
  </p>
6605
6634
  </section>
6606
6635
  </section>
@@ -6628,9 +6657,9 @@ function buildDemoDom(root, options) {
6628
6657
  </div>
6629
6658
  <div class="plasius-demo__legend">
6630
6659
  <strong>Scene</strong>
6631
- GLTF ships carry collision metadata.<br />
6632
- Flag cloth and ocean waves scale by distance band.<br />
6633
- Ray-traced shadow and reflection style is preserved near the camera.
6660
+ GLTF ships carry hull mass and damping metadata.<br />
6661
+ Lanterns and torches warm the moonlit harbor.<br />
6662
+ Mass-aware collisions stay authoritative near the camera.
6634
6663
  </div>
6635
6664
  </section>
6636
6665
  <aside class="plasius-demo__sidebar">
@@ -6711,6 +6740,7 @@ function buildSceneSnapshot(state, shipModel) {
6711
6740
  })
6712
6741
  )
6713
6742
  ),
6743
+ shipPhysics: shipModel?.physics ?? null,
6714
6744
  physics: Object.freeze({
6715
6745
  profile: state.physics.profile,
6716
6746
  plan: state.physics.plan,
@@ -6754,28 +6784,39 @@ function readVisualNumber(value, fallback) {
6754
6784
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6755
6785
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6756
6786
  const defaults = {
6757
- skyTop: premiumShadows ? "#f0f7fb" : "#e8f1f7",
6758
- skyMid: premiumShadows ? "#c7d9e5" : "#b9ceda",
6759
- skyBottom: premiumShadows ? "#84a7bd" : "#7b9bb0",
6760
- seaTop: premiumShadows ? "#235064" : "#264c5f",
6761
- seaMid: premiumShadows ? "#153e53" : "#173d4f",
6762
- seaBottom: "#0b2433",
6763
- sunCore: "rgba(255, 244, 210, 0.9)",
6787
+ skyTop: premiumShadows ? "#040c1a" : "#06101f",
6788
+ skyMid: premiumShadows ? "#11203b" : "#152643",
6789
+ skyBottom: premiumShadows ? "#2f4468" : "#364d73",
6790
+ duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
6791
+ seaTop: premiumShadows ? "#102946" : "#153050",
6792
+ seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
6793
+ seaBottom: "#04101d",
6794
+ moonCore: "rgba(241, 246, 255, 0.98)",
6795
+ moonHalo: "rgba(167, 191, 255, 0.24)",
6796
+ moonReflection: "rgba(192, 214, 255, 0.22)",
6797
+ starColor: "rgba(232, 239, 255, 0.82)",
6798
+ ambientMist: "rgba(41, 63, 97, 0.16)",
6764
6799
  reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
6765
6800
  shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
6766
- waveAmplitude: 1,
6767
- waveDirection: { x: 0.86, z: 0.34 },
6768
- wavePhaseSpeed: 1,
6769
- wakeStrength: 0.24,
6770
- wakeLength: 15,
6771
- collisionRippleStrength: 0.34,
6772
- waterNear: { r: 0.12, g: 0.36, b: 0.46 },
6773
- waterFar: { r: 0.28, g: 0.56, b: 0.68 },
6774
- harborWall: { r: 0.48, g: 0.4, b: 0.32 },
6775
- harborDeck: { r: 0.5, g: 0.34, b: 0.22 },
6776
- harborTower: { r: 0.34, g: 0.32, b: 0.36 },
6777
- flagColor: { r: 0.76, g: 0.24, b: 0.18 },
6778
- flagMotion: 1
6801
+ waveAmplitude: 0.94,
6802
+ waveDirection: { x: 0.88, z: 0.28 },
6803
+ wavePhaseSpeed: 0.88,
6804
+ wakeStrength: 0.31,
6805
+ wakeLength: 18,
6806
+ collisionRippleStrength: 0.42,
6807
+ waterNear: { r: 0.08, g: 0.23, b: 0.33 },
6808
+ waterFar: { r: 0.18, g: 0.35, b: 0.49 },
6809
+ harborWall: { r: 0.26, g: 0.24, b: 0.28 },
6810
+ harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
6811
+ harborTower: { r: 0.23, g: 0.24, b: 0.29 },
6812
+ flagColor: { r: 0.66, g: 0.16, b: 0.13 },
6813
+ flagMotion: 0.92,
6814
+ lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
6815
+ lanternGlow: { r: 1, g: 0.56, b: 0.2 },
6816
+ lanternReflectionStrength: 0.42,
6817
+ torchCore: { r: 0.99, g: 0.72, b: 0.36 },
6818
+ torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
6819
+ collisionFlash: "rgba(255, 212, 168, 0.16)"
6779
6820
  };
6780
6821
  return {
6781
6822
  skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
@@ -6784,7 +6825,12 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6784
6825
  seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
6785
6826
  seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
6786
6827
  seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
6787
- sunCore: typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
6828
+ duskGlow: typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
6829
+ moonCore: typeof customVisuals.moonCore === "string" ? customVisuals.moonCore : typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.moonCore,
6830
+ moonHalo: typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
6831
+ moonReflection: typeof customVisuals.moonReflection === "string" ? customVisuals.moonReflection : defaults.moonReflection,
6832
+ starColor: typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
6833
+ ambientMist: typeof customVisuals.ambientMist === "string" ? customVisuals.ambientMist : defaults.ambientMist,
6788
6834
  reflectionStrength: readVisualNumber(
6789
6835
  customVisuals.reflectionStrength,
6790
6836
  defaults.reflectionStrength
@@ -6805,7 +6851,16 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6805
6851
  harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
6806
6852
  harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
6807
6853
  flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
6808
- flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
6854
+ flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
6855
+ lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
6856
+ lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
6857
+ lanternReflectionStrength: readVisualNumber(
6858
+ customVisuals.lanternReflectionStrength,
6859
+ defaults.lanternReflectionStrength
6860
+ ),
6861
+ torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
6862
+ torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
6863
+ collisionFlash: typeof customVisuals.collisionFlash === "string" ? customVisuals.collisionFlash : defaults.collisionFlash
6809
6864
  };
6810
6865
  }
6811
6866
  function buildClothSurface(model, state, meshDetail, visuals) {
@@ -7040,18 +7095,34 @@ function createSceneState(options) {
7040
7095
  {
7041
7096
  id: "northwind",
7042
7097
  position: vec3(-5.2, 0, 7.2),
7043
- velocity: vec3(2.1, 0, -1.6),
7044
- rotationY: 0.42,
7045
- angularVelocity: 0.18,
7046
- tint: { r: 0.62, g: 0.39, b: 0.23 }
7098
+ velocity: vec3(2.35, 0, -1.08),
7099
+ rotationY: 0.58,
7100
+ angularVelocity: 0.09,
7101
+ tint: { r: 0.62, g: 0.39, b: 0.23 },
7102
+ massScale: 1.42,
7103
+ cruiseSpeed: 2.25,
7104
+ throttleResponse: 0.46,
7105
+ rudderResponse: 0.54,
7106
+ wanderPhase: 0.35,
7107
+ lanterns: SHIP_LANTERNS,
7108
+ lanternStrength: 1.06,
7109
+ collisionRadiusScale: 1.04
7047
7110
  },
7048
7111
  {
7049
7112
  id: "tidecaller",
7050
7113
  position: vec3(4.8, 0, 4.4),
7051
- velocity: vec3(-1.85, 0, 1.25),
7052
- rotationY: -2.62,
7053
- angularVelocity: -0.14,
7054
- tint: { r: 0.48, g: 0.28, b: 0.19 }
7114
+ velocity: vec3(-2.15, 0, 1.74),
7115
+ rotationY: -2.48,
7116
+ angularVelocity: -0.2,
7117
+ tint: { r: 0.48, g: 0.28, b: 0.19 },
7118
+ massScale: 0.84,
7119
+ cruiseSpeed: 2.68,
7120
+ throttleResponse: 0.7,
7121
+ rudderResponse: 0.78,
7122
+ wanderPhase: 1.6,
7123
+ lanterns: SHIP_LANTERNS,
7124
+ lanternStrength: 1.18,
7125
+ collisionRadiusScale: 0.94
7055
7126
  }
7056
7127
  ],
7057
7128
  sprays: [],
@@ -7073,41 +7144,71 @@ function setListContent(element, values) {
7073
7144
  element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
7074
7145
  }
7075
7146
  function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
7076
- const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
7077
7147
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
7078
7148
  sky.addColorStop(0, visuals.skyTop);
7079
- sky.addColorStop(0.6, visuals.skyMid);
7149
+ sky.addColorStop(0.54, visuals.skyMid);
7080
7150
  sky.addColorStop(1, visuals.skyBottom);
7081
7151
  ctx.fillStyle = sky;
7082
7152
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7153
+ for (let index = 0; index < 70; index += 1) {
7154
+ const x = pseudoRandom(index + 13) * canvas.width;
7155
+ const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
7156
+ const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
7157
+ const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
7158
+ ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
7159
+ ctx.beginPath();
7160
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
7161
+ ctx.fill();
7162
+ }
7163
+ const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
7164
+ horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
7165
+ horizonGlow.addColorStop(1, visuals.duskGlow);
7166
+ ctx.fillStyle = horizonGlow;
7167
+ ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
7083
7168
  const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
7084
7169
  shoreline.addColorStop(0, visuals.seaTop);
7085
7170
  shoreline.addColorStop(0.52, visuals.seaMid);
7086
7171
  shoreline.addColorStop(1, visuals.seaBottom);
7087
7172
  ctx.fillStyle = shoreline;
7088
7173
  ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7089
- const sunX = mix(canvas.width * 0.16, canvas.width * 0.84, (Math.sin(state.time * 0.12) + 1) * 0.5);
7090
- const sunY = canvas.height * 0.14 + Math.cos(state.time * 0.12) * 22;
7091
- const sun = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 90);
7092
- sun.addColorStop(0, visuals.sunCore);
7093
- sun.addColorStop(1, "rgba(255, 244, 210, 0)");
7094
- ctx.fillStyle = sun;
7174
+ const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
7175
+ const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
7176
+ const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
7177
+ moon.addColorStop(0, visuals.moonCore);
7178
+ moon.addColorStop(0.46, visuals.moonHalo);
7179
+ moon.addColorStop(1, "rgba(167, 191, 255, 0)");
7180
+ ctx.fillStyle = moon;
7095
7181
  ctx.beginPath();
7096
- ctx.arc(sunX, sunY, 90, 0, Math.PI * 2);
7182
+ ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
7097
7183
  ctx.fill();
7098
- const track = ctx.createLinearGradient(sunX, canvas.height * 0.46, sunX, canvas.height * 0.96);
7099
- track.addColorStop(0, `rgba(255, 243, 214, ${0.08 + reflectionStrength * 0.18})`);
7100
- track.addColorStop(0.42, `rgba(224, 242, 255, ${0.04 + reflectionStrength * 0.2})`);
7101
- track.addColorStop(1, "rgba(224, 242, 255, 0)");
7184
+ const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
7185
+ moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
7186
+ moonCore.addColorStop(1, visuals.moonCore);
7187
+ ctx.fillStyle = moonCore;
7188
+ ctx.beginPath();
7189
+ ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
7190
+ ctx.fill();
7191
+ const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
7192
+ track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
7193
+ track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
7194
+ track.addColorStop(1, "rgba(192, 214, 255, 0)");
7102
7195
  ctx.save();
7103
7196
  ctx.globalCompositeOperation = "screen";
7104
7197
  ctx.fillStyle = track;
7105
7198
  ctx.beginPath();
7106
- ctx.ellipse(sunX, canvas.height * 0.72, 46 + shadowStrength * 60, canvas.height * 0.26, 0, 0, Math.PI * 2);
7199
+ ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
7107
7200
  ctx.fill();
7108
7201
  ctx.restore();
7202
+ const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
7203
+ mist.addColorStop(0, "rgba(0, 0, 0, 0)");
7204
+ mist.addColorStop(1, visuals.ambientMist);
7205
+ ctx.fillStyle = mist;
7206
+ ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7109
7207
  if (state.collisionFlash > 0.01) {
7110
- ctx.fillStyle = `rgba(255, 243, 228, ${state.collisionFlash * 0.14})`;
7208
+ ctx.fillStyle = visuals.collisionFlash.replace(
7209
+ /[\d.]+\)$/u,
7210
+ `${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
7211
+ );
7111
7212
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7112
7213
  }
7113
7214
  }
@@ -7206,7 +7307,7 @@ function pushHarborGeometry(camera, viewport, triangles, visuals) {
7206
7307
  }
7207
7308
  }
7208
7309
  function renderShipRigging(ctx, ship, camera, viewport) {
7209
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7310
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7210
7311
  const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
7211
7312
  const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
7212
7313
  const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
@@ -7310,6 +7411,35 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
7310
7411
  }
7311
7412
  }
7312
7413
  }
7414
+ function readPhysicsNumber(physics, key, fallback) {
7415
+ const value = physics?.[key];
7416
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
7417
+ }
7418
+ function getShipMass(ship, shipModel) {
7419
+ const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
7420
+ return baseMass * readVisualNumber(ship.massScale, 1);
7421
+ }
7422
+ function getShipHalfExtents(ship, shipModel) {
7423
+ const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents) ? shipModel.physics.halfExtents : [1.35, 0.95, 3.9];
7424
+ const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
7425
+ return {
7426
+ x: physicsHalfExtents[0] * scale,
7427
+ y: physicsHalfExtents[1] * scale,
7428
+ z: physicsHalfExtents[2] * scale
7429
+ };
7430
+ }
7431
+ function getShipCollisionRadius(ship, shipModel) {
7432
+ const halfExtents = getShipHalfExtents(ship, shipModel);
7433
+ return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
7434
+ }
7435
+ function getShipInverseMass(ship, shipModel) {
7436
+ return 1 / Math.max(1, getShipMass(ship, shipModel));
7437
+ }
7438
+ function getShipInverseInertia(ship, shipModel) {
7439
+ const radius = getShipCollisionRadius(ship, shipModel);
7440
+ const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
7441
+ return 1 / Math.max(1, inertia);
7442
+ }
7313
7443
  function spawnSpray(state, point, intensity) {
7314
7444
  const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
7315
7445
  for (let index = 0; index < count; index += 1) {
@@ -7322,55 +7452,182 @@ function spawnSpray(state, point, intensity) {
7322
7452
  });
7323
7453
  }
7324
7454
  }
7325
- function updateShips(state, dt, shipModel) {
7455
+ function resolveShipRoute(ship, state, radius) {
7456
+ if (typeof ship.routeDirection !== "number") {
7457
+ ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
7458
+ }
7459
+ if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
7460
+ ship.routeDirection = -1;
7461
+ } else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
7462
+ ship.routeDirection = 1;
7463
+ }
7464
+ const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
7465
+ const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
7466
+ const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
7467
+ const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
7468
+ return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
7469
+ }
7470
+ function updateShipMotion(state, ship, dt, shipModel) {
7326
7471
  const physics = shipModel.physics;
7327
- const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
7472
+ const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
7473
+ const radius = getShipCollisionRadius(ship, shipModel);
7474
+ const waterline = readPhysicsNumber(physics, "waterline", 0.42);
7475
+ const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
7476
+ const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
7477
+ const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
7478
+ const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
7479
+ const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
7480
+ ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
7481
+ const forward = directionFromYaw(ship.rotationY);
7482
+ const lateral = perpendicularOnWater(forward);
7483
+ const routeTarget = resolveShipRoute(ship, state, radius);
7484
+ const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
7485
+ const headingError = Math.atan2(
7486
+ Math.sin(desiredHeading - ship.rotationY),
7487
+ Math.cos(desiredHeading - ship.rotationY)
7488
+ );
7489
+ ship.angularVelocity += headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) + Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
7490
+ const waveDirection = resolveWaveDirection(state);
7491
+ const forwardSpeed = dotVec3(ship.velocity, forward);
7492
+ const lateralSpeed = dotVec3(ship.velocity, lateral);
7493
+ const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
7494
+ const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
7495
+ const acceleration = addVec3(
7496
+ scaleVec3(forward, thrust),
7497
+ addVec3(
7498
+ scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
7499
+ scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
7500
+ )
7501
+ );
7502
+ ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
7503
+ ship.velocity = scaleVec3(
7504
+ ship.velocity,
7505
+ Math.max(0, 1 - linearDamping / Math.pow(massScale, 0.22) * dt)
7506
+ );
7507
+ ship.angularVelocity *= Math.max(
7508
+ 0,
7509
+ 1 - angularDamping / Math.pow(massScale, 0.15) * dt
7510
+ );
7511
+ ship.rotationY += ship.angularVelocity * dt;
7512
+ ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7513
+ ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
7514
+ }
7515
+ function resolveBoundaryCollision(ship, state, shipModel) {
7516
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
7517
+ const radius = getShipCollisionRadius(ship, shipModel);
7518
+ const boundaries = [
7519
+ { axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
7520
+ { axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) }
7521
+ ];
7522
+ for (const boundary of boundaries) {
7523
+ if (ship.position[boundary.axis] < boundary.min) {
7524
+ ship.position[boundary.axis] = boundary.min;
7525
+ const normal = boundary.normalMin;
7526
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7527
+ if (speedIntoWall < 0) {
7528
+ ship.velocity = subVec3(
7529
+ ship.velocity,
7530
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7531
+ );
7532
+ const tangent = vec3(-normal.z, 0, normal.x);
7533
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7534
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7535
+ ship.angularVelocity += tangentSpeed * 4e-3;
7536
+ }
7537
+ } else if (ship.position[boundary.axis] > boundary.max) {
7538
+ ship.position[boundary.axis] = boundary.max;
7539
+ const normal = boundary.normalMax;
7540
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7541
+ if (speedIntoWall < 0) {
7542
+ ship.velocity = subVec3(
7543
+ ship.velocity,
7544
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7545
+ );
7546
+ const tangent = vec3(-normal.z, 0, normal.x);
7547
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7548
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7549
+ ship.angularVelocity += tangentSpeed * 4e-3;
7550
+ }
7551
+ }
7552
+ }
7553
+ }
7554
+ function resolveShipCollision(state, a, b, shipModel) {
7555
+ const delta = subVec3(b.position, a.position);
7556
+ const radiusA = getShipCollisionRadius(a, shipModel);
7557
+ const radiusB = getShipCollisionRadius(b, shipModel);
7558
+ const distance = Math.hypot(delta.x, delta.z);
7559
+ const minDistance = radiusA + radiusB;
7560
+ if (distance >= minDistance) {
7561
+ return false;
7562
+ }
7563
+ const normal = distance > 1e-4 ? normalizeVec3(vec3(delta.x / distance, 0, delta.z / distance)) : normalizeVec3(vec3(Math.cos(state.time * 5.2), 0, Math.sin(state.time * 4.8)));
7564
+ const tangent = vec3(-normal.z, 0, normal.x);
7565
+ const penetration = minDistance - distance;
7566
+ const invMassA = getShipInverseMass(a, shipModel);
7567
+ const invMassB = getShipInverseMass(b, shipModel);
7568
+ const invMassSum = invMassA + invMassB;
7569
+ const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
7570
+ a.position = subVec3(a.position, scaleVec3(correction, invMassA));
7571
+ b.position = addVec3(b.position, scaleVec3(correction, invMassB));
7572
+ const relativeVelocity = subVec3(b.velocity, a.velocity);
7573
+ const velocityAlongNormal = dotVec3(relativeVelocity, normal);
7574
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
7575
+ if (velocityAlongNormal < 0) {
7576
+ const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
7577
+ const impulse = scaleVec3(normal, impulseMagnitude);
7578
+ a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
7579
+ b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
7580
+ const tangentSpeed = dotVec3(relativeVelocity, tangent);
7581
+ const frictionMagnitude = clamp(
7582
+ -tangentSpeed / Math.max(1e-4, invMassSum),
7583
+ -impulseMagnitude * 0.16,
7584
+ impulseMagnitude * 0.16
7585
+ );
7586
+ const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
7587
+ a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
7588
+ b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
7589
+ a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7590
+ b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7591
+ const impactSpeed = Math.abs(velocityAlongNormal);
7592
+ if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
7593
+ const contactPoint = vec3(
7594
+ (a.position.x + b.position.x) * 0.5,
7595
+ (a.position.y + b.position.y) * 0.5 + 0.14,
7596
+ (a.position.z + b.position.z) * 0.5
7597
+ );
7598
+ spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
7599
+ state.waveImpulses.push({
7600
+ x: contactPoint.x,
7601
+ z: contactPoint.z,
7602
+ strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
7603
+ radius: 0.9 + penetration * 1.4,
7604
+ life: 1
7605
+ });
7606
+ state.collisionCount += 1;
7607
+ state.collisionFlash = Math.max(
7608
+ state.collisionFlash,
7609
+ clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
7610
+ );
7611
+ a.collisionCooldown = 0.2;
7612
+ b.collisionCooldown = 0.2;
7613
+ }
7614
+ }
7615
+ state.contactCount += 1;
7616
+ return true;
7617
+ }
7618
+ function updateShips(state, dt, shipModel) {
7328
7619
  let collided = false;
7329
7620
  state.contactCount = 0;
7330
7621
  for (const ship of state.ships) {
7331
- ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7332
- ship.rotationY += ship.angularVelocity * dt;
7333
- ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
7334
- ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
7335
- ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7336
- if (Math.abs(ship.position.x) > 10) {
7337
- ship.velocity.x *= -1;
7338
- ship.angularVelocity *= -1;
7339
- }
7340
- if (ship.position.z < 2 || ship.position.z > 16) {
7341
- ship.velocity.z *= -1;
7342
- ship.angularVelocity *= -1;
7343
- }
7622
+ updateShipMotion(state, ship, dt, shipModel);
7623
+ resolveBoundaryCollision(ship, state, shipModel);
7344
7624
  }
7345
- const [a, b] = state.ships;
7346
- const dx = b.position.x - a.position.x;
7347
- const dz = b.position.z - a.position.z;
7348
- const overlapX = Math.abs(dx) < halfExtents[0] * 1.7;
7349
- const overlapZ = Math.abs(dz) < halfExtents[2] * 0.8;
7350
- if (overlapX && overlapZ) {
7351
- const restitution = physics.restitution ?? 0.22;
7352
- const swapX = a.velocity.x;
7353
- const swapZ = a.velocity.z;
7354
- a.velocity.x = -b.velocity.x * (0.86 + restitution);
7355
- a.velocity.z = -b.velocity.z * (0.82 + restitution);
7356
- b.velocity.x = -swapX * (0.86 + restitution);
7357
- b.velocity.z = -swapZ * (0.82 + restitution);
7358
- a.angularVelocity += 0.55;
7359
- b.angularVelocity -= 0.55;
7360
- const contactPoint = vec3((a.position.x + b.position.x) * 0.5, (a.position.y + b.position.y) * 0.5 + 0.1, (a.position.z + b.position.z) * 0.5);
7361
- spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
7362
- state.waveImpulses.push({
7363
- x: contactPoint.x,
7364
- z: contactPoint.z,
7365
- strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
7366
- radius: 0.8,
7367
- life: 1
7368
- });
7369
- state.collisionCount += 1;
7370
- state.contactCount = 1;
7371
- collided = true;
7625
+ for (let index = 0; index < state.ships.length; index += 1) {
7626
+ for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
7627
+ collided = resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) || collided;
7628
+ }
7372
7629
  }
7373
- state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
7630
+ state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
7374
7631
  }
7375
7632
  function updateWaveImpulses(state, dt) {
7376
7633
  state.waveImpulses = state.waveImpulses.map((impulse) => ({
@@ -7483,7 +7740,7 @@ function renderFlagPole(ctx, camera, viewport) {
7483
7740
  function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
7484
7741
  const bounds = shipModel.bounds;
7485
7742
  const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
7486
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7743
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7487
7744
  const hullCorners = [
7488
7745
  vec3(bounds.min[0], keelY, bounds.min[2]),
7489
7746
  vec3(bounds.max[0], keelY, bounds.min[2]),
@@ -7509,6 +7766,97 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7509
7766
  blur: 12 + shadowStrength * 20
7510
7767
  });
7511
7768
  }
7769
+ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7770
+ const projected = projectPoint(point, camera, viewport);
7771
+ if (!projected) {
7772
+ return;
7773
+ }
7774
+ const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
7775
+ const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7776
+ halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7777
+ halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7778
+ halo.addColorStop(1, colorToRgba(glowColor, 0));
7779
+ ctx.save();
7780
+ ctx.globalCompositeOperation = "screen";
7781
+ ctx.fillStyle = halo;
7782
+ ctx.beginPath();
7783
+ ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7784
+ ctx.fill();
7785
+ ctx.restore();
7786
+ ctx.fillStyle = colorToRgba(coreColor, 0.98);
7787
+ ctx.beginPath();
7788
+ ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7789
+ ctx.fill();
7790
+ if (state && reflectionStrength > 0) {
7791
+ const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7792
+ const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7793
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
7794
+ if (reflected) {
7795
+ const reflectionRadius = radius * 0.72;
7796
+ const glow = ctx.createRadialGradient(
7797
+ reflected.x,
7798
+ reflected.y,
7799
+ reflectionRadius * 0.1,
7800
+ reflected.x,
7801
+ reflected.y,
7802
+ reflectionRadius
7803
+ );
7804
+ glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7805
+ glow.addColorStop(1, colorToRgba(glowColor, 0));
7806
+ ctx.save();
7807
+ ctx.globalCompositeOperation = "screen";
7808
+ ctx.fillStyle = glow;
7809
+ ctx.beginPath();
7810
+ ctx.ellipse(
7811
+ reflected.x,
7812
+ reflected.y,
7813
+ reflectionRadius * 0.34,
7814
+ reflectionRadius,
7815
+ 0,
7816
+ 0,
7817
+ Math.PI * 2
7818
+ );
7819
+ ctx.fill();
7820
+ ctx.restore();
7821
+ }
7822
+ }
7823
+ }
7824
+ function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7825
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7826
+ const strength = readVisualNumber(ship.lanternStrength, 1);
7827
+ for (const lantern of lanterns) {
7828
+ const position = transformPoint(
7829
+ vec3(lantern.x, lantern.y, lantern.z),
7830
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7831
+ );
7832
+ renderGlowLight(
7833
+ ctx,
7834
+ position,
7835
+ camera,
7836
+ viewport,
7837
+ visuals.lanternCore,
7838
+ visuals.lanternGlow,
7839
+ lantern.glow * strength,
7840
+ visuals.lanternReflectionStrength,
7841
+ state
7842
+ );
7843
+ }
7844
+ }
7845
+ function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7846
+ for (const torch of HARBOR_TORCHES) {
7847
+ renderGlowLight(
7848
+ ctx,
7849
+ vec3(torch.x, torch.y, torch.z),
7850
+ camera,
7851
+ viewport,
7852
+ visuals.torchCore,
7853
+ visuals.torchGlow,
7854
+ torch.glow,
7855
+ visuals.lanternReflectionStrength * 0.55,
7856
+ state
7857
+ );
7858
+ }
7859
+ }
7512
7860
  function renderScene(ctx, canvas, state, shipModel, dom) {
7513
7861
  const viewport = { width: canvas.width, height: canvas.height };
7514
7862
  const camera = buildCamera(state, canvas);
@@ -7518,7 +7866,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7518
7866
  importance: state.focus === "lighting" ? "critical" : "high"
7519
7867
  });
7520
7868
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
7521
- const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
7869
+ const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
7522
7870
  const lightingSnapshot = state.lightingDetail.getSnapshot();
7523
7871
  const visuals = resolveVisualConfig(
7524
7872
  nearLighting,
@@ -7592,7 +7940,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7592
7940
  for (const ship of state.ships) {
7593
7941
  buildTrianglesFromMesh(
7594
7942
  shipModel,
7595
- { position: ship.position, rotationY: ship.rotationY, scale: 1.1 },
7943
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
7596
7944
  ship.tint,
7597
7945
  camera,
7598
7946
  viewport,
@@ -7606,10 +7954,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7606
7954
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
7607
7955
  drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
7608
7956
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
7957
+ renderHarborTorches(ctx, state, camera, viewport, visuals);
7609
7958
  renderFlagPole(ctx, camera, viewport);
7610
7959
  renderClothAccent(ctx, cloth, camera, viewport);
7611
7960
  for (const ship of state.ships) {
7612
7961
  renderShipRigging(ctx, ship, camera, viewport);
7962
+ renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
7613
7963
  }
7614
7964
  renderSprays(ctx, state.sprays, camera, viewport);
7615
7965
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -7621,8 +7971,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7621
7971
  const sceneMetrics = [
7622
7972
  `focus: ${state.focus}`,
7623
7973
  `ships: ${state.ships.length} active GLTF hulls`,
7974
+ `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
7624
7975
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
7625
- `physics contacts: ${state.physics.snapshot.contactCount ?? 0}`,
7976
+ `physics contacts: ${state.contactCount}`,
7977
+ `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
7626
7978
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
7627
7979
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
7628
7980
  `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
@@ -7645,8 +7997,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7645
7997
  ];
7646
7998
  const sceneNotes = state.focus === "physics" ? [
7647
7999
  "Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
7648
- "The ships collide on GLTF-derived hull volumes while cloth and fluid remain downstream visual consumers.",
7649
- "Near-field lighting keeps the ray-traced-primary shadow impression so the collision read stays crisp."
8000
+ "The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
8001
+ "Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
7650
8002
  ] : SCENE_NOTES;
7651
8003
  const custom = state.demoDescription ?? null;
7652
8004
  setListContent(
@@ -7663,7 +8015,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7663
8015
  );
7664
8016
  setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
7665
8017
  dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7666
- dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; GLTF ships collide on ${shipModel.physics.shape ?? "box"} volumes while visual follow-up remains downstream.` : `GLTF ships are colliding with ${shipModel.physics.shape ?? "box"} physics volumes; cloth and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
8018
+ dom.details.textContent = typeof custom?.details === "string" ? custom.details : state.focus === "physics" ? `Stable world snapshots are emitted from ${state.physics.plan.snapshotStageId} after the authoritative solver; the heavier hull now carries momentum through mass-aware collision impulses while cloth and fluid remain downstream.` : `Moonlit GLTF ships collide on ${shipModel.physics.shape ?? "box"} physics volumes; lantern reflections, cloth, and fluid remain continuous while the governor pressure is ${state.lastDecision.pressureLevel}.`;
7667
8019
  }
7668
8020
  function updateSceneState(state, dt, shipModel) {
7669
8021
  updateShips(state, dt, shipModel);
@@ -7682,7 +8034,9 @@ function syncTextState(state, shipModel) {
7682
8034
  y: Number(ship.position.y.toFixed(2)),
7683
8035
  z: Number(ship.position.z.toFixed(2)),
7684
8036
  vx: Number(ship.velocity.x.toFixed(2)),
7685
- vz: Number(ship.velocity.z.toFixed(2))
8037
+ vz: Number(ship.velocity.z.toFixed(2)),
8038
+ massKg: Math.round(getShipMass(ship, shipModel)),
8039
+ lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
7686
8040
  })),
7687
8041
  shipPhysics: shipModel.physics,
7688
8042
  sprays: state.sprays.length,
@@ -7710,6 +8064,7 @@ function syncTextState(state, shipModel) {
7710
8064
  async function mountGpuShowcase(options = {}) {
7711
8065
  injectStyles();
7712
8066
  const root = options.root ?? document.body;
8067
+ root.classList?.add?.(ROOT_CLASS);
7713
8068
  const previousMarkup = root.innerHTML;
7714
8069
  const previousRenderGameToText = window.render_game_to_text;
7715
8070
  const previousAdvanceTime = window.advanceTime;
@@ -7729,8 +8084,11 @@ async function mountGpuShowcase(options = {}) {
7729
8084
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7730
8085
  syncTextState(state, shipModel);
7731
8086
  const ctx = dom.canvas.getContext("2d");
8087
+ if (!ctx) {
8088
+ throw new Error("2D canvas context is required for the shared showcase.");
8089
+ }
8090
+ let animationFrameId = 0;
7732
8091
  let destroyed = false;
7733
- let frameHandle = null;
7734
8092
  const renderFrame = (nowMs) => {
7735
8093
  if (destroyed) {
7736
8094
  return;
@@ -7751,7 +8109,7 @@ async function mountGpuShowcase(options = {}) {
7751
8109
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7752
8110
  renderScene(ctx, dom.canvas, state, shipModel, dom);
7753
8111
  syncTextState(state, shipModel);
7754
- frameHandle = requestAnimationFrame(renderFrame);
8112
+ animationFrameId = requestAnimationFrame(renderFrame);
7755
8113
  };
7756
8114
  const handlePauseClick = () => {
7757
8115
  state.paused = !state.paused;
@@ -7770,34 +8128,43 @@ async function mountGpuShowcase(options = {}) {
7770
8128
  dom.pauseButton.addEventListener("click", handlePauseClick);
7771
8129
  dom.stressToggle.addEventListener("change", handleStressChange);
7772
8130
  dom.focusMode.addEventListener("change", handleFocusChange);
7773
- frameHandle = requestAnimationFrame(renderFrame);
8131
+ animationFrameId = requestAnimationFrame(renderFrame);
8132
+ const destroy = () => {
8133
+ if (destroyed) {
8134
+ return;
8135
+ }
8136
+ destroyed = true;
8137
+ if (animationFrameId) {
8138
+ cancelAnimationFrame(animationFrameId);
8139
+ }
8140
+ dom.pauseButton.removeEventListener("click", handlePauseClick);
8141
+ dom.stressToggle.removeEventListener("change", handleStressChange);
8142
+ dom.focusMode.removeEventListener("change", handleFocusChange);
8143
+ try {
8144
+ if (typeof options.destroyState === "function") {
8145
+ options.destroyState(state.packageState);
8146
+ }
8147
+ } finally {
8148
+ state.packageState = void 0;
8149
+ }
8150
+ root.classList?.remove?.(ROOT_CLASS);
8151
+ root.innerHTML = previousMarkup;
8152
+ if (typeof previousRenderGameToText === "function") {
8153
+ window.render_game_to_text = previousRenderGameToText;
8154
+ } else {
8155
+ delete window.render_game_to_text;
8156
+ }
8157
+ if (typeof previousAdvanceTime === "function") {
8158
+ window.advanceTime = previousAdvanceTime;
8159
+ } else {
8160
+ delete window.advanceTime;
8161
+ }
8162
+ };
7774
8163
  return {
7775
8164
  state,
7776
8165
  shipModel,
7777
8166
  canvas: dom.canvas,
7778
- destroy() {
7779
- if (destroyed) {
7780
- return;
7781
- }
7782
- destroyed = true;
7783
- if (frameHandle != null) {
7784
- cancelAnimationFrame(frameHandle);
7785
- }
7786
- dom.pauseButton.removeEventListener("click", handlePauseClick);
7787
- dom.stressToggle.removeEventListener("change", handleStressChange);
7788
- dom.focusMode.removeEventListener("change", handleFocusChange);
7789
- root.innerHTML = previousMarkup;
7790
- if (typeof previousRenderGameToText === "function") {
7791
- window.render_game_to_text = previousRenderGameToText;
7792
- } else {
7793
- delete window.render_game_to_text;
7794
- }
7795
- if (typeof previousAdvanceTime === "function") {
7796
- window.advanceTime = previousAdvanceTime;
7797
- } else {
7798
- delete window.advanceTime;
7799
- }
7800
- }
8167
+ destroy
7801
8168
  };
7802
8169
  }
7803
8170
  function updatePhysicsSnapshot(state, shipModel) {
@@ -7811,7 +8178,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7811
8178
  animationInputRevision: state.frame,
7812
8179
  bodyCount: state.ships.length + 2,
7813
8180
  dynamicBodyCount: state.ships.length,
7814
- contactCount: state.collisionFlash > 0.02 ? 1 : 0,
8181
+ contactCount: state.contactCount,
7815
8182
  metadata: {
7816
8183
  collisionCount: state.collisionCount,
7817
8184
  contactCount: state.contactCount,
@@ -7824,4 +8191,4 @@ export {
7824
8191
  mountGpuShowcase,
7825
8192
  showcaseFocusModes
7826
8193
  };
7827
- //# sourceMappingURL=showcase-runtime-4BS7TWHS.js.map
8194
+ //# sourceMappingURL=showcase-runtime-JZIYGQAU.js.map