@plasius/gpu-shared 0.1.4 → 0.1.7

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";
@@ -2022,9 +2022,12 @@ var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __re
2022
2022
  if (typeof __require !== "undefined") return __require.apply(this, arguments);
2023
2023
  throw Error('Dynamic require of "' + x + '" is not supported');
2024
2024
  });
2025
+ function createModuleBaseUrl(metaUrl) {
2026
+ return Reflect.construct(URL, [String(metaUrl)]);
2027
+ }
2025
2028
  var baseUrl = (() => {
2026
2029
  if (typeof import.meta.url !== "undefined") {
2027
- return new URL("./index.js", import.meta.url);
2030
+ return createModuleBaseUrl(import.meta.url);
2028
2031
  }
2029
2032
  if (typeof __filename !== "undefined" && typeof __require2 !== "undefined") {
2030
2033
  const { pathToFileURL } = __require2("url");
@@ -4935,7 +4938,7 @@ function createGpuDebugSession(options = {}) {
4935
4938
  };
4936
4939
  }
4937
4940
 
4938
- // node_modules/@plasius/gpu-physics/dist/chunk-CNTXT5QJ.js
4941
+ // node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js
4939
4942
  var physicsDebugOwner = "physics";
4940
4943
  var physicsWorkerQueueClasses = Object.freeze({
4941
4944
  simulation: "simulation",
@@ -5763,7 +5766,7 @@ var physicsWorkerDagSpecs = {
5763
5766
  contactVisuals: { priority: 1, dependencies: ["worldSnapshot"] }
5764
5767
  }
5765
5768
  };
5766
- var physicsSimulationPlanSpecs = Object.freeze({
5769
+ var physicsSimulationPlans = Object.freeze({
5767
5770
  gameplay: Object.freeze({
5768
5771
  description: "Gameplay simulation plan that hands off a stable post-commit world snapshot to visual preparation.",
5769
5772
  snapshotStageId: "worldSnapshot",
@@ -6039,7 +6042,7 @@ function getPhysicsWorkerManifest(profile = defaultPhysicsWorkerProfile) {
6039
6042
  return manifest;
6040
6043
  }
6041
6044
  function createPhysicsSimulationPlan(profile = defaultPhysicsWorkerProfile) {
6042
- const plan = physicsSimulationPlanSpecs[profile];
6045
+ const plan = physicsSimulationPlans[profile];
6043
6046
  if (!plan) {
6044
6047
  const available = physicsWorkerProfileNames.join(", ");
6045
6048
  throw new Error(
@@ -6106,8 +6109,16 @@ function createPhysicsWorldSnapshot(input) {
6106
6109
 
6107
6110
  // src/showcase-runtime.js
6108
6111
  var STYLE_ID = "plasius-shared-3d-showcase-style";
6112
+ var ROOT_CLASS = "plasius-showcase-root";
6109
6113
  var DEFAULT_TITLE = "Flag by the Sea";
6110
6114
  var DEFAULT_SUBTITLE = "Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
6115
+ var SHIP_SCALE = 1.1;
6116
+ var HARBOR_BOUNDS = Object.freeze({
6117
+ minX: -11.2,
6118
+ maxX: 11.2,
6119
+ minZ: 1.8,
6120
+ maxZ: 17.2
6121
+ });
6111
6122
  var CAMERA_PRESETS = Object.freeze({
6112
6123
  integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
6113
6124
  lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
@@ -6119,10 +6130,10 @@ var CAMERA_PRESETS = Object.freeze({
6119
6130
  });
6120
6131
  var showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
6121
6132
  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."
6133
+ "Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
6134
+ "Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
6135
+ "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
6136
+ "Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched."
6126
6137
  ]);
6127
6138
  var UNIT_BOX_MESH = Object.freeze({
6128
6139
  positions: Object.freeze([
@@ -6190,6 +6201,17 @@ var UNIT_BOX_MESH = Object.freeze({
6190
6201
  0
6191
6202
  ])
6192
6203
  });
6204
+ var SHIP_LANTERNS = Object.freeze([
6205
+ Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
6206
+ Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
6207
+ Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
6208
+ Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
6209
+ ]);
6210
+ var HARBOR_TORCHES = Object.freeze([
6211
+ Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
6212
+ Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
6213
+ Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
6214
+ ]);
6193
6215
  function injectStyles() {
6194
6216
  if (document.getElementById(STYLE_ID)) {
6195
6217
  return;
@@ -6197,27 +6219,27 @@ function injectStyles() {
6197
6219
  const style = document.createElement("style");
6198
6220
  style.id = STYLE_ID;
6199
6221
  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 {
6222
+ .${ROOT_CLASS} {
6223
+ color-scheme: dark;
6224
+ --plasius-paper: #081321;
6225
+ --plasius-ink: #edf4ff;
6226
+ --plasius-muted: #b6c5dd;
6227
+ --plasius-accent: #f3b16a;
6228
+ --plasius-panel: rgba(8, 19, 33, 0.72);
6229
+ --plasius-border: rgba(159, 185, 223, 0.18);
6230
+ --plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
6214
6231
  margin: 0;
6215
- min-height: 100vh;
6232
+ min-height: 100%;
6216
6233
  font-family: "Fraunces", "Iowan Old Style", serif;
6217
6234
  color: var(--plasius-ink);
6218
6235
  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%);
6236
+ radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
6237
+ radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
6238
+ linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
6239
+ }
6240
+ .${ROOT_CLASS},
6241
+ .${ROOT_CLASS} * {
6242
+ box-sizing: border-box;
6221
6243
  }
6222
6244
  .plasius-demo {
6223
6245
  width: min(1560px, calc(100vw - 32px));
@@ -6251,7 +6273,7 @@ function injectStyles() {
6251
6273
  text-transform: uppercase;
6252
6274
  letter-spacing: 0.18em;
6253
6275
  font-size: 12px;
6254
- color: rgba(21, 32, 40, 0.56);
6276
+ color: rgba(226, 236, 255, 0.58);
6255
6277
  }
6256
6278
  .plasius-demo h1,
6257
6279
  .plasius-demo h2,
@@ -6269,7 +6291,7 @@ function injectStyles() {
6269
6291
  margin: 0;
6270
6292
  padding: 8px 12px;
6271
6293
  border-radius: 999px;
6272
- background: rgba(143, 86, 52, 0.12);
6294
+ background: rgba(243, 177, 106, 0.14);
6273
6295
  color: var(--plasius-accent);
6274
6296
  font-weight: 700;
6275
6297
  }
@@ -6291,8 +6313,8 @@ function injectStyles() {
6291
6313
  aspect-ratio: 16 / 9;
6292
6314
  display: block;
6293
6315
  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%);
6316
+ border: 1px solid rgba(159, 185, 223, 0.12);
6317
+ background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
6296
6318
  }
6297
6319
  .plasius-demo__toolbar {
6298
6320
  position: absolute;
@@ -6312,9 +6334,9 @@ function injectStyles() {
6312
6334
  .plasius-demo button,
6313
6335
  .plasius-demo .plasius-toggle,
6314
6336
  .plasius-demo select {
6315
- border: 1px solid rgba(21, 32, 40, 0.12);
6337
+ border: 1px solid rgba(159, 185, 223, 0.18);
6316
6338
  border-radius: 999px;
6317
- background: rgba(255, 255, 255, 0.84);
6339
+ background: rgba(9, 20, 34, 0.84);
6318
6340
  color: var(--plasius-ink);
6319
6341
  padding: 10px 14px;
6320
6342
  }
@@ -6353,8 +6375,8 @@ function injectStyles() {
6353
6375
  bottom: 24px;
6354
6376
  padding: 10px 14px;
6355
6377
  border-radius: 16px;
6356
- background: rgba(255, 255, 255, 0.84);
6357
- border: 1px solid rgba(21, 32, 40, 0.1);
6378
+ background: rgba(9, 20, 34, 0.82);
6379
+ border: 1px solid rgba(159, 185, 223, 0.16);
6358
6380
  color: var(--plasius-muted);
6359
6381
  font-size: 12px;
6360
6382
  line-height: 1.45;
@@ -6366,7 +6388,7 @@ function injectStyles() {
6366
6388
  }
6367
6389
  .plasius-demo__footer {
6368
6390
  margin-top: 4px;
6369
- color: rgba(21, 32, 40, 0.66);
6391
+ color: rgba(226, 236, 255, 0.68);
6370
6392
  font-size: 13px;
6371
6393
  line-height: 1.6;
6372
6394
  }
@@ -6385,6 +6407,10 @@ function clamp(value, min, max) {
6385
6407
  function mix(a, b, t) {
6386
6408
  return a + (b - a) * t;
6387
6409
  }
6410
+ function pseudoRandom(seed) {
6411
+ const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
6412
+ return value - Math.floor(value);
6413
+ }
6388
6414
  function vec3(x = 0, y = 0, z = 0) {
6389
6415
  return { x, y, z };
6390
6416
  }
@@ -6418,6 +6444,12 @@ function reflectVec3(vector, normal) {
6418
6444
  const unitNormal = normalizeVec3(normal);
6419
6445
  return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
6420
6446
  }
6447
+ function directionFromYaw(yaw) {
6448
+ return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
6449
+ }
6450
+ function perpendicularOnWater(direction) {
6451
+ return vec3(-direction.z, 0, direction.x);
6452
+ }
6421
6453
  function rotateY(point, angle) {
6422
6454
  const cosine = Math.cos(angle);
6423
6455
  const sine = Math.sin(angle);
@@ -6600,7 +6632,7 @@ function buildDemoDom(root, options) {
6600
6632
  <section class="plasius-panel plasius-demo__status">
6601
6633
  <p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene\u2026</p>
6602
6634
  <p id="demoDetails" class="plasius-demo__status-text">
6603
- Preparing GLTF assets, cloth and fluid continuity plans, and adaptive quality metadata.
6635
+ Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
6604
6636
  </p>
6605
6637
  </section>
6606
6638
  </section>
@@ -6628,9 +6660,9 @@ function buildDemoDom(root, options) {
6628
6660
  </div>
6629
6661
  <div class="plasius-demo__legend">
6630
6662
  <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.
6663
+ GLTF ships carry hull mass and damping metadata.<br />
6664
+ Lanterns and torches warm the moonlit harbor.<br />
6665
+ Mass-aware collisions stay authoritative near the camera.
6634
6666
  </div>
6635
6667
  </section>
6636
6668
  <aside class="plasius-demo__sidebar">
@@ -6711,6 +6743,7 @@ function buildSceneSnapshot(state, shipModel) {
6711
6743
  })
6712
6744
  )
6713
6745
  ),
6746
+ shipPhysics: shipModel?.physics ?? null,
6714
6747
  physics: Object.freeze({
6715
6748
  profile: state.physics.profile,
6716
6749
  plan: state.physics.plan,
@@ -6754,28 +6787,39 @@ function readVisualNumber(value, fallback) {
6754
6787
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6755
6788
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6756
6789
  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)",
6790
+ skyTop: premiumShadows ? "#040c1a" : "#06101f",
6791
+ skyMid: premiumShadows ? "#11203b" : "#152643",
6792
+ skyBottom: premiumShadows ? "#2f4468" : "#364d73",
6793
+ duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
6794
+ seaTop: premiumShadows ? "#102946" : "#153050",
6795
+ seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
6796
+ seaBottom: "#04101d",
6797
+ moonCore: "rgba(241, 246, 255, 0.98)",
6798
+ moonHalo: "rgba(167, 191, 255, 0.24)",
6799
+ moonReflection: "rgba(192, 214, 255, 0.22)",
6800
+ starColor: "rgba(232, 239, 255, 0.82)",
6801
+ ambientMist: "rgba(41, 63, 97, 0.16)",
6764
6802
  reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
6765
6803
  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
6804
+ waveAmplitude: 0.94,
6805
+ waveDirection: { x: 0.88, z: 0.28 },
6806
+ wavePhaseSpeed: 0.88,
6807
+ wakeStrength: 0.31,
6808
+ wakeLength: 18,
6809
+ collisionRippleStrength: 0.42,
6810
+ waterNear: { r: 0.08, g: 0.23, b: 0.33 },
6811
+ waterFar: { r: 0.18, g: 0.35, b: 0.49 },
6812
+ harborWall: { r: 0.26, g: 0.24, b: 0.28 },
6813
+ harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
6814
+ harborTower: { r: 0.23, g: 0.24, b: 0.29 },
6815
+ flagColor: { r: 0.66, g: 0.16, b: 0.13 },
6816
+ flagMotion: 0.92,
6817
+ lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
6818
+ lanternGlow: { r: 1, g: 0.56, b: 0.2 },
6819
+ lanternReflectionStrength: 0.42,
6820
+ torchCore: { r: 0.99, g: 0.72, b: 0.36 },
6821
+ torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
6822
+ collisionFlash: "rgba(255, 212, 168, 0.16)"
6779
6823
  };
6780
6824
  return {
6781
6825
  skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
@@ -6784,7 +6828,12 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6784
6828
  seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
6785
6829
  seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
6786
6830
  seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
6787
- sunCore: typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
6831
+ duskGlow: typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
6832
+ moonCore: typeof customVisuals.moonCore === "string" ? customVisuals.moonCore : typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.moonCore,
6833
+ moonHalo: typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
6834
+ moonReflection: typeof customVisuals.moonReflection === "string" ? customVisuals.moonReflection : defaults.moonReflection,
6835
+ starColor: typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
6836
+ ambientMist: typeof customVisuals.ambientMist === "string" ? customVisuals.ambientMist : defaults.ambientMist,
6788
6837
  reflectionStrength: readVisualNumber(
6789
6838
  customVisuals.reflectionStrength,
6790
6839
  defaults.reflectionStrength
@@ -6805,7 +6854,16 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6805
6854
  harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
6806
6855
  harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
6807
6856
  flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
6808
- flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
6857
+ flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
6858
+ lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
6859
+ lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
6860
+ lanternReflectionStrength: readVisualNumber(
6861
+ customVisuals.lanternReflectionStrength,
6862
+ defaults.lanternReflectionStrength
6863
+ ),
6864
+ torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
6865
+ torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
6866
+ collisionFlash: typeof customVisuals.collisionFlash === "string" ? customVisuals.collisionFlash : defaults.collisionFlash
6809
6867
  };
6810
6868
  }
6811
6869
  function buildClothSurface(model, state, meshDetail, visuals) {
@@ -7040,18 +7098,34 @@ function createSceneState(options) {
7040
7098
  {
7041
7099
  id: "northwind",
7042
7100
  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 }
7101
+ velocity: vec3(2.35, 0, -1.08),
7102
+ rotationY: 0.58,
7103
+ angularVelocity: 0.09,
7104
+ tint: { r: 0.62, g: 0.39, b: 0.23 },
7105
+ massScale: 1.42,
7106
+ cruiseSpeed: 2.25,
7107
+ throttleResponse: 0.46,
7108
+ rudderResponse: 0.54,
7109
+ wanderPhase: 0.35,
7110
+ lanterns: SHIP_LANTERNS,
7111
+ lanternStrength: 1.06,
7112
+ collisionRadiusScale: 1.04
7047
7113
  },
7048
7114
  {
7049
7115
  id: "tidecaller",
7050
7116
  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 }
7117
+ velocity: vec3(-2.15, 0, 1.74),
7118
+ rotationY: -2.48,
7119
+ angularVelocity: -0.2,
7120
+ tint: { r: 0.48, g: 0.28, b: 0.19 },
7121
+ massScale: 0.84,
7122
+ cruiseSpeed: 2.68,
7123
+ throttleResponse: 0.7,
7124
+ rudderResponse: 0.78,
7125
+ wanderPhase: 1.6,
7126
+ lanterns: SHIP_LANTERNS,
7127
+ lanternStrength: 1.18,
7128
+ collisionRadiusScale: 0.94
7055
7129
  }
7056
7130
  ],
7057
7131
  sprays: [],
@@ -7073,41 +7147,71 @@ function setListContent(element, values) {
7073
7147
  element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
7074
7148
  }
7075
7149
  function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
7076
- const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
7077
7150
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
7078
7151
  sky.addColorStop(0, visuals.skyTop);
7079
- sky.addColorStop(0.6, visuals.skyMid);
7152
+ sky.addColorStop(0.54, visuals.skyMid);
7080
7153
  sky.addColorStop(1, visuals.skyBottom);
7081
7154
  ctx.fillStyle = sky;
7082
7155
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7156
+ for (let index = 0; index < 70; index += 1) {
7157
+ const x = pseudoRandom(index + 13) * canvas.width;
7158
+ const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
7159
+ const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
7160
+ const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
7161
+ ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
7162
+ ctx.beginPath();
7163
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
7164
+ ctx.fill();
7165
+ }
7166
+ const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
7167
+ horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
7168
+ horizonGlow.addColorStop(1, visuals.duskGlow);
7169
+ ctx.fillStyle = horizonGlow;
7170
+ ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
7083
7171
  const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
7084
7172
  shoreline.addColorStop(0, visuals.seaTop);
7085
7173
  shoreline.addColorStop(0.52, visuals.seaMid);
7086
7174
  shoreline.addColorStop(1, visuals.seaBottom);
7087
7175
  ctx.fillStyle = shoreline;
7088
7176
  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;
7177
+ const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
7178
+ const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
7179
+ const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
7180
+ moon.addColorStop(0, visuals.moonCore);
7181
+ moon.addColorStop(0.46, visuals.moonHalo);
7182
+ moon.addColorStop(1, "rgba(167, 191, 255, 0)");
7183
+ ctx.fillStyle = moon;
7095
7184
  ctx.beginPath();
7096
- ctx.arc(sunX, sunY, 90, 0, Math.PI * 2);
7185
+ ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
7097
7186
  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)");
7187
+ const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
7188
+ moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
7189
+ moonCore.addColorStop(1, visuals.moonCore);
7190
+ ctx.fillStyle = moonCore;
7191
+ ctx.beginPath();
7192
+ ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
7193
+ ctx.fill();
7194
+ const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
7195
+ track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
7196
+ track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
7197
+ track.addColorStop(1, "rgba(192, 214, 255, 0)");
7102
7198
  ctx.save();
7103
7199
  ctx.globalCompositeOperation = "screen";
7104
7200
  ctx.fillStyle = track;
7105
7201
  ctx.beginPath();
7106
- ctx.ellipse(sunX, canvas.height * 0.72, 46 + shadowStrength * 60, canvas.height * 0.26, 0, 0, Math.PI * 2);
7202
+ ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
7107
7203
  ctx.fill();
7108
7204
  ctx.restore();
7205
+ const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
7206
+ mist.addColorStop(0, "rgba(0, 0, 0, 0)");
7207
+ mist.addColorStop(1, visuals.ambientMist);
7208
+ ctx.fillStyle = mist;
7209
+ ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7109
7210
  if (state.collisionFlash > 0.01) {
7110
- ctx.fillStyle = `rgba(255, 243, 228, ${state.collisionFlash * 0.14})`;
7211
+ ctx.fillStyle = visuals.collisionFlash.replace(
7212
+ /[\d.]+\)$/u,
7213
+ `${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
7214
+ );
7111
7215
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7112
7216
  }
7113
7217
  }
@@ -7206,7 +7310,7 @@ function pushHarborGeometry(camera, viewport, triangles, visuals) {
7206
7310
  }
7207
7311
  }
7208
7312
  function renderShipRigging(ctx, ship, camera, viewport) {
7209
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7313
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7210
7314
  const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
7211
7315
  const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
7212
7316
  const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
@@ -7310,6 +7414,35 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
7310
7414
  }
7311
7415
  }
7312
7416
  }
7417
+ function readPhysicsNumber(physics, key, fallback) {
7418
+ const value = physics?.[key];
7419
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
7420
+ }
7421
+ function getShipMass(ship, shipModel) {
7422
+ const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
7423
+ return baseMass * readVisualNumber(ship.massScale, 1);
7424
+ }
7425
+ function getShipHalfExtents(ship, shipModel) {
7426
+ const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents) ? shipModel.physics.halfExtents : [1.35, 0.95, 3.9];
7427
+ const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
7428
+ return {
7429
+ x: physicsHalfExtents[0] * scale,
7430
+ y: physicsHalfExtents[1] * scale,
7431
+ z: physicsHalfExtents[2] * scale
7432
+ };
7433
+ }
7434
+ function getShipCollisionRadius(ship, shipModel) {
7435
+ const halfExtents = getShipHalfExtents(ship, shipModel);
7436
+ return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
7437
+ }
7438
+ function getShipInverseMass(ship, shipModel) {
7439
+ return 1 / Math.max(1, getShipMass(ship, shipModel));
7440
+ }
7441
+ function getShipInverseInertia(ship, shipModel) {
7442
+ const radius = getShipCollisionRadius(ship, shipModel);
7443
+ const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
7444
+ return 1 / Math.max(1, inertia);
7445
+ }
7313
7446
  function spawnSpray(state, point, intensity) {
7314
7447
  const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
7315
7448
  for (let index = 0; index < count; index += 1) {
@@ -7322,55 +7455,182 @@ function spawnSpray(state, point, intensity) {
7322
7455
  });
7323
7456
  }
7324
7457
  }
7325
- function updateShips(state, dt, shipModel) {
7458
+ function resolveShipRoute(ship, state, radius) {
7459
+ if (typeof ship.routeDirection !== "number") {
7460
+ ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
7461
+ }
7462
+ if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
7463
+ ship.routeDirection = -1;
7464
+ } else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
7465
+ ship.routeDirection = 1;
7466
+ }
7467
+ const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
7468
+ const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
7469
+ const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
7470
+ const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
7471
+ return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
7472
+ }
7473
+ function updateShipMotion(state, ship, dt, shipModel) {
7326
7474
  const physics = shipModel.physics;
7327
- const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
7475
+ const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
7476
+ const radius = getShipCollisionRadius(ship, shipModel);
7477
+ const waterline = readPhysicsNumber(physics, "waterline", 0.42);
7478
+ const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
7479
+ const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
7480
+ const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
7481
+ const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
7482
+ const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
7483
+ ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
7484
+ const forward = directionFromYaw(ship.rotationY);
7485
+ const lateral = perpendicularOnWater(forward);
7486
+ const routeTarget = resolveShipRoute(ship, state, radius);
7487
+ const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
7488
+ const headingError = Math.atan2(
7489
+ Math.sin(desiredHeading - ship.rotationY),
7490
+ Math.cos(desiredHeading - ship.rotationY)
7491
+ );
7492
+ ship.angularVelocity += headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) + Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
7493
+ const waveDirection = resolveWaveDirection(state);
7494
+ const forwardSpeed = dotVec3(ship.velocity, forward);
7495
+ const lateralSpeed = dotVec3(ship.velocity, lateral);
7496
+ const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
7497
+ const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
7498
+ const acceleration = addVec3(
7499
+ scaleVec3(forward, thrust),
7500
+ addVec3(
7501
+ scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
7502
+ scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
7503
+ )
7504
+ );
7505
+ ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
7506
+ ship.velocity = scaleVec3(
7507
+ ship.velocity,
7508
+ Math.max(0, 1 - linearDamping / Math.pow(massScale, 0.22) * dt)
7509
+ );
7510
+ ship.angularVelocity *= Math.max(
7511
+ 0,
7512
+ 1 - angularDamping / Math.pow(massScale, 0.15) * dt
7513
+ );
7514
+ ship.rotationY += ship.angularVelocity * dt;
7515
+ ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7516
+ ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
7517
+ }
7518
+ function resolveBoundaryCollision(ship, state, shipModel) {
7519
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
7520
+ const radius = getShipCollisionRadius(ship, shipModel);
7521
+ const boundaries = [
7522
+ { axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
7523
+ { axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) }
7524
+ ];
7525
+ for (const boundary of boundaries) {
7526
+ if (ship.position[boundary.axis] < boundary.min) {
7527
+ ship.position[boundary.axis] = boundary.min;
7528
+ const normal = boundary.normalMin;
7529
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7530
+ if (speedIntoWall < 0) {
7531
+ ship.velocity = subVec3(
7532
+ ship.velocity,
7533
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7534
+ );
7535
+ const tangent = vec3(-normal.z, 0, normal.x);
7536
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7537
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7538
+ ship.angularVelocity += tangentSpeed * 4e-3;
7539
+ }
7540
+ } else if (ship.position[boundary.axis] > boundary.max) {
7541
+ ship.position[boundary.axis] = boundary.max;
7542
+ const normal = boundary.normalMax;
7543
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7544
+ if (speedIntoWall < 0) {
7545
+ ship.velocity = subVec3(
7546
+ ship.velocity,
7547
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7548
+ );
7549
+ const tangent = vec3(-normal.z, 0, normal.x);
7550
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7551
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7552
+ ship.angularVelocity += tangentSpeed * 4e-3;
7553
+ }
7554
+ }
7555
+ }
7556
+ }
7557
+ function resolveShipCollision(state, a, b, shipModel) {
7558
+ const delta = subVec3(b.position, a.position);
7559
+ const radiusA = getShipCollisionRadius(a, shipModel);
7560
+ const radiusB = getShipCollisionRadius(b, shipModel);
7561
+ const distance = Math.hypot(delta.x, delta.z);
7562
+ const minDistance = radiusA + radiusB;
7563
+ if (distance >= minDistance) {
7564
+ return false;
7565
+ }
7566
+ 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)));
7567
+ const tangent = vec3(-normal.z, 0, normal.x);
7568
+ const penetration = minDistance - distance;
7569
+ const invMassA = getShipInverseMass(a, shipModel);
7570
+ const invMassB = getShipInverseMass(b, shipModel);
7571
+ const invMassSum = invMassA + invMassB;
7572
+ const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
7573
+ a.position = subVec3(a.position, scaleVec3(correction, invMassA));
7574
+ b.position = addVec3(b.position, scaleVec3(correction, invMassB));
7575
+ const relativeVelocity = subVec3(b.velocity, a.velocity);
7576
+ const velocityAlongNormal = dotVec3(relativeVelocity, normal);
7577
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
7578
+ if (velocityAlongNormal < 0) {
7579
+ const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
7580
+ const impulse = scaleVec3(normal, impulseMagnitude);
7581
+ a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
7582
+ b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
7583
+ const tangentSpeed = dotVec3(relativeVelocity, tangent);
7584
+ const frictionMagnitude = clamp(
7585
+ -tangentSpeed / Math.max(1e-4, invMassSum),
7586
+ -impulseMagnitude * 0.16,
7587
+ impulseMagnitude * 0.16
7588
+ );
7589
+ const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
7590
+ a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
7591
+ b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
7592
+ a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7593
+ b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7594
+ const impactSpeed = Math.abs(velocityAlongNormal);
7595
+ if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
7596
+ const contactPoint = vec3(
7597
+ (a.position.x + b.position.x) * 0.5,
7598
+ (a.position.y + b.position.y) * 0.5 + 0.14,
7599
+ (a.position.z + b.position.z) * 0.5
7600
+ );
7601
+ spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
7602
+ state.waveImpulses.push({
7603
+ x: contactPoint.x,
7604
+ z: contactPoint.z,
7605
+ strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
7606
+ radius: 0.9 + penetration * 1.4,
7607
+ life: 1
7608
+ });
7609
+ state.collisionCount += 1;
7610
+ state.collisionFlash = Math.max(
7611
+ state.collisionFlash,
7612
+ clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
7613
+ );
7614
+ a.collisionCooldown = 0.2;
7615
+ b.collisionCooldown = 0.2;
7616
+ }
7617
+ }
7618
+ state.contactCount += 1;
7619
+ return true;
7620
+ }
7621
+ function updateShips(state, dt, shipModel) {
7328
7622
  let collided = false;
7329
7623
  state.contactCount = 0;
7330
7624
  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
- }
7625
+ updateShipMotion(state, ship, dt, shipModel);
7626
+ resolveBoundaryCollision(ship, state, shipModel);
7344
7627
  }
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;
7628
+ for (let index = 0; index < state.ships.length; index += 1) {
7629
+ for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
7630
+ collided = resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) || collided;
7631
+ }
7372
7632
  }
7373
- state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
7633
+ state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
7374
7634
  }
7375
7635
  function updateWaveImpulses(state, dt) {
7376
7636
  state.waveImpulses = state.waveImpulses.map((impulse) => ({
@@ -7483,7 +7743,7 @@ function renderFlagPole(ctx, camera, viewport) {
7483
7743
  function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
7484
7744
  const bounds = shipModel.bounds;
7485
7745
  const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
7486
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7746
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7487
7747
  const hullCorners = [
7488
7748
  vec3(bounds.min[0], keelY, bounds.min[2]),
7489
7749
  vec3(bounds.max[0], keelY, bounds.min[2]),
@@ -7509,6 +7769,97 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7509
7769
  blur: 12 + shadowStrength * 20
7510
7770
  });
7511
7771
  }
7772
+ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7773
+ const projected = projectPoint(point, camera, viewport);
7774
+ if (!projected) {
7775
+ return;
7776
+ }
7777
+ const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
7778
+ const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7779
+ halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7780
+ halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7781
+ halo.addColorStop(1, colorToRgba(glowColor, 0));
7782
+ ctx.save();
7783
+ ctx.globalCompositeOperation = "screen";
7784
+ ctx.fillStyle = halo;
7785
+ ctx.beginPath();
7786
+ ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7787
+ ctx.fill();
7788
+ ctx.restore();
7789
+ ctx.fillStyle = colorToRgba(coreColor, 0.98);
7790
+ ctx.beginPath();
7791
+ ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7792
+ ctx.fill();
7793
+ if (state && reflectionStrength > 0) {
7794
+ const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7795
+ const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7796
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
7797
+ if (reflected) {
7798
+ const reflectionRadius = radius * 0.72;
7799
+ const glow = ctx.createRadialGradient(
7800
+ reflected.x,
7801
+ reflected.y,
7802
+ reflectionRadius * 0.1,
7803
+ reflected.x,
7804
+ reflected.y,
7805
+ reflectionRadius
7806
+ );
7807
+ glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7808
+ glow.addColorStop(1, colorToRgba(glowColor, 0));
7809
+ ctx.save();
7810
+ ctx.globalCompositeOperation = "screen";
7811
+ ctx.fillStyle = glow;
7812
+ ctx.beginPath();
7813
+ ctx.ellipse(
7814
+ reflected.x,
7815
+ reflected.y,
7816
+ reflectionRadius * 0.34,
7817
+ reflectionRadius,
7818
+ 0,
7819
+ 0,
7820
+ Math.PI * 2
7821
+ );
7822
+ ctx.fill();
7823
+ ctx.restore();
7824
+ }
7825
+ }
7826
+ }
7827
+ function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7828
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7829
+ const strength = readVisualNumber(ship.lanternStrength, 1);
7830
+ for (const lantern of lanterns) {
7831
+ const position = transformPoint(
7832
+ vec3(lantern.x, lantern.y, lantern.z),
7833
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7834
+ );
7835
+ renderGlowLight(
7836
+ ctx,
7837
+ position,
7838
+ camera,
7839
+ viewport,
7840
+ visuals.lanternCore,
7841
+ visuals.lanternGlow,
7842
+ lantern.glow * strength,
7843
+ visuals.lanternReflectionStrength,
7844
+ state
7845
+ );
7846
+ }
7847
+ }
7848
+ function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7849
+ for (const torch of HARBOR_TORCHES) {
7850
+ renderGlowLight(
7851
+ ctx,
7852
+ vec3(torch.x, torch.y, torch.z),
7853
+ camera,
7854
+ viewport,
7855
+ visuals.torchCore,
7856
+ visuals.torchGlow,
7857
+ torch.glow,
7858
+ visuals.lanternReflectionStrength * 0.55,
7859
+ state
7860
+ );
7861
+ }
7862
+ }
7512
7863
  function renderScene(ctx, canvas, state, shipModel, dom) {
7513
7864
  const viewport = { width: canvas.width, height: canvas.height };
7514
7865
  const camera = buildCamera(state, canvas);
@@ -7518,7 +7869,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7518
7869
  importance: state.focus === "lighting" ? "critical" : "high"
7519
7870
  });
7520
7871
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
7521
- const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
7872
+ const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
7522
7873
  const lightingSnapshot = state.lightingDetail.getSnapshot();
7523
7874
  const visuals = resolveVisualConfig(
7524
7875
  nearLighting,
@@ -7592,7 +7943,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7592
7943
  for (const ship of state.ships) {
7593
7944
  buildTrianglesFromMesh(
7594
7945
  shipModel,
7595
- { position: ship.position, rotationY: ship.rotationY, scale: 1.1 },
7946
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
7596
7947
  ship.tint,
7597
7948
  camera,
7598
7949
  viewport,
@@ -7606,10 +7957,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7606
7957
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
7607
7958
  drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
7608
7959
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
7960
+ renderHarborTorches(ctx, state, camera, viewport, visuals);
7609
7961
  renderFlagPole(ctx, camera, viewport);
7610
7962
  renderClothAccent(ctx, cloth, camera, viewport);
7611
7963
  for (const ship of state.ships) {
7612
7964
  renderShipRigging(ctx, ship, camera, viewport);
7965
+ renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
7613
7966
  }
7614
7967
  renderSprays(ctx, state.sprays, camera, viewport);
7615
7968
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -7621,8 +7974,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7621
7974
  const sceneMetrics = [
7622
7975
  `focus: ${state.focus}`,
7623
7976
  `ships: ${state.ships.length} active GLTF hulls`,
7977
+ `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
7624
7978
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
7625
- `physics contacts: ${state.physics.snapshot.contactCount ?? 0}`,
7979
+ `physics contacts: ${state.contactCount}`,
7980
+ `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
7626
7981
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
7627
7982
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
7628
7983
  `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
@@ -7645,8 +8000,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7645
8000
  ];
7646
8001
  const sceneNotes = state.focus === "physics" ? [
7647
8002
  "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."
8003
+ "The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
8004
+ "Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
7650
8005
  ] : SCENE_NOTES;
7651
8006
  const custom = state.demoDescription ?? null;
7652
8007
  setListContent(
@@ -7663,7 +8018,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7663
8018
  );
7664
8019
  setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
7665
8020
  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}.`;
8021
+ 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
8022
  }
7668
8023
  function updateSceneState(state, dt, shipModel) {
7669
8024
  updateShips(state, dt, shipModel);
@@ -7682,7 +8037,9 @@ function syncTextState(state, shipModel) {
7682
8037
  y: Number(ship.position.y.toFixed(2)),
7683
8038
  z: Number(ship.position.z.toFixed(2)),
7684
8039
  vx: Number(ship.velocity.x.toFixed(2)),
7685
- vz: Number(ship.velocity.z.toFixed(2))
8040
+ vz: Number(ship.velocity.z.toFixed(2)),
8041
+ massKg: Math.round(getShipMass(ship, shipModel)),
8042
+ lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
7686
8043
  })),
7687
8044
  shipPhysics: shipModel.physics,
7688
8045
  sprays: state.sprays.length,
@@ -7710,6 +8067,7 @@ function syncTextState(state, shipModel) {
7710
8067
  async function mountGpuShowcase(options = {}) {
7711
8068
  injectStyles();
7712
8069
  const root = options.root ?? document.body;
8070
+ root.classList?.add?.(ROOT_CLASS);
7713
8071
  const previousMarkup = root.innerHTML;
7714
8072
  const previousRenderGameToText = window.render_game_to_text;
7715
8073
  const previousAdvanceTime = window.advanceTime;
@@ -7729,8 +8087,11 @@ async function mountGpuShowcase(options = {}) {
7729
8087
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7730
8088
  syncTextState(state, shipModel);
7731
8089
  const ctx = dom.canvas.getContext("2d");
8090
+ if (!ctx) {
8091
+ throw new Error("2D canvas context is required for the shared showcase.");
8092
+ }
8093
+ let animationFrameId = 0;
7732
8094
  let destroyed = false;
7733
- let frameHandle = null;
7734
8095
  const renderFrame = (nowMs) => {
7735
8096
  if (destroyed) {
7736
8097
  return;
@@ -7751,7 +8112,7 @@ async function mountGpuShowcase(options = {}) {
7751
8112
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7752
8113
  renderScene(ctx, dom.canvas, state, shipModel, dom);
7753
8114
  syncTextState(state, shipModel);
7754
- frameHandle = requestAnimationFrame(renderFrame);
8115
+ animationFrameId = requestAnimationFrame(renderFrame);
7755
8116
  };
7756
8117
  const handlePauseClick = () => {
7757
8118
  state.paused = !state.paused;
@@ -7770,34 +8131,43 @@ async function mountGpuShowcase(options = {}) {
7770
8131
  dom.pauseButton.addEventListener("click", handlePauseClick);
7771
8132
  dom.stressToggle.addEventListener("change", handleStressChange);
7772
8133
  dom.focusMode.addEventListener("change", handleFocusChange);
7773
- frameHandle = requestAnimationFrame(renderFrame);
8134
+ animationFrameId = requestAnimationFrame(renderFrame);
8135
+ const destroy = () => {
8136
+ if (destroyed) {
8137
+ return;
8138
+ }
8139
+ destroyed = true;
8140
+ if (animationFrameId) {
8141
+ cancelAnimationFrame(animationFrameId);
8142
+ }
8143
+ dom.pauseButton.removeEventListener("click", handlePauseClick);
8144
+ dom.stressToggle.removeEventListener("change", handleStressChange);
8145
+ dom.focusMode.removeEventListener("change", handleFocusChange);
8146
+ try {
8147
+ if (typeof options.destroyState === "function") {
8148
+ options.destroyState(state.packageState);
8149
+ }
8150
+ } finally {
8151
+ state.packageState = void 0;
8152
+ }
8153
+ root.classList?.remove?.(ROOT_CLASS);
8154
+ root.innerHTML = previousMarkup;
8155
+ if (typeof previousRenderGameToText === "function") {
8156
+ window.render_game_to_text = previousRenderGameToText;
8157
+ } else {
8158
+ delete window.render_game_to_text;
8159
+ }
8160
+ if (typeof previousAdvanceTime === "function") {
8161
+ window.advanceTime = previousAdvanceTime;
8162
+ } else {
8163
+ delete window.advanceTime;
8164
+ }
8165
+ };
7774
8166
  return {
7775
8167
  state,
7776
8168
  shipModel,
7777
8169
  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
- }
8170
+ destroy
7801
8171
  };
7802
8172
  }
7803
8173
  function updatePhysicsSnapshot(state, shipModel) {
@@ -7811,7 +8181,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7811
8181
  animationInputRevision: state.frame,
7812
8182
  bodyCount: state.ships.length + 2,
7813
8183
  dynamicBodyCount: state.ships.length,
7814
- contactCount: state.collisionFlash > 0.02 ? 1 : 0,
8184
+ contactCount: state.contactCount,
7815
8185
  metadata: {
7816
8186
  collisionCount: state.collisionCount,
7817
8187
  contactCount: state.contactCount,
@@ -7824,4 +8194,4 @@ export {
7824
8194
  mountGpuShowcase,
7825
8195
  showcaseFocusModes
7826
8196
  };
7827
- //# sourceMappingURL=showcase-runtime-4BS7TWHS.js.map
8197
+ //# sourceMappingURL=showcase-runtime-AIPDRK7G.js.map