@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.
package/dist/index.cjs CHANGED
@@ -20,11 +20,32 @@ var __copyProps = (to, from, except, desc) => {
20
20
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
21
 
22
22
  // src/asset-url.js
23
+ function createInlineShowcaseAssetUrl() {
24
+ return new URL(INLINE_BRIGANTINE_GLTF_URL);
25
+ }
26
+ function getBrowserBaseUrl() {
27
+ if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
28
+ return document.baseURI;
29
+ }
30
+ if (typeof window !== "undefined" && typeof window.location?.href === "string" && window.location.href.length > 0) {
31
+ return window.location.href;
32
+ }
33
+ return null;
34
+ }
23
35
  function resolveShowcaseAssetUrl(baseUrl2 = import_meta.url) {
24
36
  try {
25
37
  return new URL("../assets/brigantine.gltf", baseUrl2);
26
38
  } catch {
27
- return new URL(INLINE_BRIGANTINE_GLTF_URL);
39
+ const browserBaseUrl = getBrowserBaseUrl();
40
+ if (browserBaseUrl) {
41
+ try {
42
+ const normalizedBaseUrl = new URL(baseUrl2, browserBaseUrl);
43
+ return new URL("../assets/brigantine.gltf", normalizedBaseUrl);
44
+ } catch {
45
+ return createInlineShowcaseAssetUrl();
46
+ }
47
+ }
48
+ return createInlineShowcaseAssetUrl();
28
49
  }
29
50
  }
30
51
  var import_meta, INLINE_BRIGANTINE_GLTF_URL;
@@ -110,12 +131,39 @@ function computeBounds(positions) {
110
131
  }
111
132
  return { min, max };
112
133
  }
134
+ function resolveBrowserRequestBaseUrl() {
135
+ if (typeof document !== "undefined" && typeof document.baseURI === "string" && document.baseURI.length > 0) {
136
+ return document.baseURI;
137
+ }
138
+ if (typeof window !== "undefined" && typeof window.location?.href === "string" && window.location.href.length > 0) {
139
+ return window.location.href;
140
+ }
141
+ return null;
142
+ }
143
+ function resolveFetchBaseUrl(requestUrl, responseUrl) {
144
+ if (typeof responseUrl === "string" && responseUrl.length > 0) {
145
+ try {
146
+ return new URL(responseUrl);
147
+ } catch {
148
+ }
149
+ }
150
+ try {
151
+ return new URL(requestUrl);
152
+ } catch {
153
+ const browserBaseUrl = resolveBrowserRequestBaseUrl();
154
+ if (browserBaseUrl) {
155
+ return new URL(requestUrl, browserBaseUrl);
156
+ }
157
+ throw new Error(`Unable to resolve a stable base URL for glTF asset loading: ${String(requestUrl)}`);
158
+ }
159
+ }
113
160
  async function loadGltfModel(url) {
114
161
  const response = await fetch(url);
115
162
  if (!response.ok) {
116
163
  throw new Error(`Failed to load glTF asset: ${response.status} ${response.statusText}`);
117
164
  }
118
165
  const document2 = await response.json();
166
+ const baseUrl2 = resolveFetchBaseUrl(url, response.url);
119
167
  const buffers = await Promise.all(
120
168
  (document2.buffers ?? []).map(async (buffer) => {
121
169
  if (typeof buffer.uri !== "string") {
@@ -124,7 +172,7 @@ async function loadGltfModel(url) {
124
172
  if (buffer.uri.startsWith("data:")) {
125
173
  return decodeDataUri(buffer.uri);
126
174
  }
127
- const nested = await fetch(new URL(buffer.uri, url));
175
+ const nested = await fetch(new URL(buffer.uri, baseUrl2));
128
176
  if (!nested.ok) {
129
177
  throw new Error(`Failed to load glTF buffer: ${nested.status} ${nested.statusText}`);
130
178
  }
@@ -5107,7 +5155,7 @@ var init_dist5 = __esm({
5107
5155
  }
5108
5156
  });
5109
5157
 
5110
- // node_modules/@plasius/gpu-physics/dist/chunk-CNTXT5QJ.js
5158
+ // node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js
5111
5159
  function assertIdentifier5(name, value) {
5112
5160
  if (typeof value !== "string" || value.trim().length === 0) {
5113
5161
  throw new Error(`${name} must be a non-empty string.`);
@@ -5256,7 +5304,7 @@ function getPhysicsWorkerManifest(profile = defaultPhysicsWorkerProfile) {
5256
5304
  return manifest;
5257
5305
  }
5258
5306
  function createPhysicsSimulationPlan(profile = defaultPhysicsWorkerProfile) {
5259
- const plan = physicsSimulationPlanSpecs[profile];
5307
+ const plan = physicsSimulationPlans[profile];
5260
5308
  if (!plan) {
5261
5309
  const available = physicsWorkerProfileNames.join(", ");
5262
5310
  throw new Error(
@@ -5320,9 +5368,9 @@ function createPhysicsWorldSnapshot(input) {
5320
5368
  metadata: normalizeMetadata(input.metadata)
5321
5369
  });
5322
5370
  }
5323
- var physicsDebugOwner, physicsWorkerQueueClasses, defaultPhysicsWorkerProfile, physicsSimulationStageOrder, physicsWorkerProfileSpecs, physicsWorkerDagSpecs, physicsSimulationPlanSpecs, physicsWorkerManifests, physicsWorkerProfileNames;
5324
- var init_chunk_CNTXT5QJ = __esm({
5325
- "node_modules/@plasius/gpu-physics/dist/chunk-CNTXT5QJ.js"() {
5371
+ var physicsDebugOwner, physicsWorkerQueueClasses, defaultPhysicsWorkerProfile, physicsSimulationStageOrder, physicsWorkerProfileSpecs, physicsWorkerDagSpecs, physicsSimulationPlans, physicsWorkerManifests, physicsWorkerProfileNames;
5372
+ var init_chunk_PFUNZLNF = __esm({
5373
+ "node_modules/@plasius/gpu-physics/dist/chunk-PFUNZLNF.js"() {
5326
5374
  physicsDebugOwner = "physics";
5327
5375
  physicsWorkerQueueClasses = Object.freeze({
5328
5376
  simulation: "simulation",
@@ -6068,7 +6116,7 @@ var init_chunk_CNTXT5QJ = __esm({
6068
6116
  contactVisuals: { priority: 1, dependencies: ["worldSnapshot"] }
6069
6117
  }
6070
6118
  };
6071
- physicsSimulationPlanSpecs = Object.freeze({
6119
+ physicsSimulationPlans = Object.freeze({
6072
6120
  gameplay: Object.freeze({
6073
6121
  description: "Gameplay simulation plan that hands off a stable post-commit world snapshot to visual preparation.",
6074
6122
  snapshotStageId: "worldSnapshot",
@@ -6284,7 +6332,7 @@ var init_chunk_CNTXT5QJ = __esm({
6284
6332
  // node_modules/@plasius/gpu-physics/dist/browser.js
6285
6333
  var init_browser = __esm({
6286
6334
  "node_modules/@plasius/gpu-physics/dist/browser.js"() {
6287
- init_chunk_CNTXT5QJ();
6335
+ init_chunk_PFUNZLNF();
6288
6336
  }
6289
6337
  });
6290
6338
 
@@ -6301,27 +6349,27 @@ function injectStyles() {
6301
6349
  const style = document.createElement("style");
6302
6350
  style.id = STYLE_ID;
6303
6351
  style.textContent = `
6304
- :root {
6305
- color-scheme: light;
6306
- --plasius-paper: #f4f7f8;
6307
- --plasius-ink: #152028;
6308
- --plasius-muted: #5c6f7b;
6309
- --plasius-accent: #8f5634;
6310
- --plasius-panel: rgba(255, 255, 255, 0.82);
6311
- --plasius-border: rgba(21, 32, 40, 0.12);
6312
- --plasius-shadow: 0 20px 48px rgba(15, 24, 31, 0.16);
6313
- }
6314
- * {
6315
- box-sizing: border-box;
6316
- }
6317
- body {
6352
+ .${ROOT_CLASS} {
6353
+ color-scheme: dark;
6354
+ --plasius-paper: #081321;
6355
+ --plasius-ink: #edf4ff;
6356
+ --plasius-muted: #b6c5dd;
6357
+ --plasius-accent: #f3b16a;
6358
+ --plasius-panel: rgba(8, 19, 33, 0.72);
6359
+ --plasius-border: rgba(159, 185, 223, 0.18);
6360
+ --plasius-shadow: 0 24px 56px rgba(1, 6, 14, 0.44);
6318
6361
  margin: 0;
6319
- min-height: 100vh;
6362
+ min-height: 100%;
6320
6363
  font-family: "Fraunces", "Iowan Old Style", serif;
6321
6364
  color: var(--plasius-ink);
6322
6365
  background:
6323
- radial-gradient(circle at top left, rgba(255, 247, 238, 0.92), transparent 34%),
6324
- linear-gradient(180deg, #f6f8fb 0%, #d2dee6 48%, #b6c4ce 100%);
6366
+ radial-gradient(circle at 18% 12%, rgba(73, 101, 170, 0.28), transparent 30%),
6367
+ radial-gradient(circle at 82% 18%, rgba(240, 188, 103, 0.08), transparent 18%),
6368
+ linear-gradient(180deg, #04101d 0%, #0b1930 42%, #081321 100%);
6369
+ }
6370
+ .${ROOT_CLASS},
6371
+ .${ROOT_CLASS} * {
6372
+ box-sizing: border-box;
6325
6373
  }
6326
6374
  .plasius-demo {
6327
6375
  width: min(1560px, calc(100vw - 32px));
@@ -6355,7 +6403,7 @@ function injectStyles() {
6355
6403
  text-transform: uppercase;
6356
6404
  letter-spacing: 0.18em;
6357
6405
  font-size: 12px;
6358
- color: rgba(21, 32, 40, 0.56);
6406
+ color: rgba(226, 236, 255, 0.58);
6359
6407
  }
6360
6408
  .plasius-demo h1,
6361
6409
  .plasius-demo h2,
@@ -6373,7 +6421,7 @@ function injectStyles() {
6373
6421
  margin: 0;
6374
6422
  padding: 8px 12px;
6375
6423
  border-radius: 999px;
6376
- background: rgba(143, 86, 52, 0.12);
6424
+ background: rgba(243, 177, 106, 0.14);
6377
6425
  color: var(--plasius-accent);
6378
6426
  font-weight: 700;
6379
6427
  }
@@ -6395,8 +6443,8 @@ function injectStyles() {
6395
6443
  aspect-ratio: 16 / 9;
6396
6444
  display: block;
6397
6445
  border-radius: 20px;
6398
- border: 1px solid rgba(21, 32, 40, 0.08);
6399
- background: linear-gradient(180deg, #dce8ef 0%, #a9bfd0 42%, #0f5168 42%, #092433 100%);
6446
+ border: 1px solid rgba(159, 185, 223, 0.12);
6447
+ background: linear-gradient(180deg, #071220 0%, #132440 42%, #10344b 42%, #05111d 100%);
6400
6448
  }
6401
6449
  .plasius-demo__toolbar {
6402
6450
  position: absolute;
@@ -6416,9 +6464,9 @@ function injectStyles() {
6416
6464
  .plasius-demo button,
6417
6465
  .plasius-demo .plasius-toggle,
6418
6466
  .plasius-demo select {
6419
- border: 1px solid rgba(21, 32, 40, 0.12);
6467
+ border: 1px solid rgba(159, 185, 223, 0.18);
6420
6468
  border-radius: 999px;
6421
- background: rgba(255, 255, 255, 0.84);
6469
+ background: rgba(9, 20, 34, 0.84);
6422
6470
  color: var(--plasius-ink);
6423
6471
  padding: 10px 14px;
6424
6472
  }
@@ -6457,8 +6505,8 @@ function injectStyles() {
6457
6505
  bottom: 24px;
6458
6506
  padding: 10px 14px;
6459
6507
  border-radius: 16px;
6460
- background: rgba(255, 255, 255, 0.84);
6461
- border: 1px solid rgba(21, 32, 40, 0.1);
6508
+ background: rgba(9, 20, 34, 0.82);
6509
+ border: 1px solid rgba(159, 185, 223, 0.16);
6462
6510
  color: var(--plasius-muted);
6463
6511
  font-size: 12px;
6464
6512
  line-height: 1.45;
@@ -6470,7 +6518,7 @@ function injectStyles() {
6470
6518
  }
6471
6519
  .plasius-demo__footer {
6472
6520
  margin-top: 4px;
6473
- color: rgba(21, 32, 40, 0.66);
6521
+ color: rgba(226, 236, 255, 0.68);
6474
6522
  font-size: 13px;
6475
6523
  line-height: 1.6;
6476
6524
  }
@@ -6489,6 +6537,10 @@ function clamp(value, min, max) {
6489
6537
  function mix(a, b, t) {
6490
6538
  return a + (b - a) * t;
6491
6539
  }
6540
+ function pseudoRandom(seed) {
6541
+ const value = Math.sin(seed * 12.9898 + seed * seed * 17e-4) * 43758.5453;
6542
+ return value - Math.floor(value);
6543
+ }
6492
6544
  function vec3(x = 0, y = 0, z = 0) {
6493
6545
  return { x, y, z };
6494
6546
  }
@@ -6522,6 +6574,12 @@ function reflectVec3(vector, normal) {
6522
6574
  const unitNormal = normalizeVec3(normal);
6523
6575
  return subVec3(vector, scaleVec3(unitNormal, 2 * dotVec3(vector, unitNormal)));
6524
6576
  }
6577
+ function directionFromYaw(yaw) {
6578
+ return normalizeVec3(vec3(Math.sin(yaw), 0, Math.cos(yaw)));
6579
+ }
6580
+ function perpendicularOnWater(direction) {
6581
+ return vec3(-direction.z, 0, direction.x);
6582
+ }
6525
6583
  function rotateY(point, angle) {
6526
6584
  const cosine = Math.cos(angle);
6527
6585
  const sine = Math.sin(angle);
@@ -6704,7 +6762,7 @@ function buildDemoDom(root, options) {
6704
6762
  <section class="plasius-panel plasius-demo__status">
6705
6763
  <p id="demoStatus" class="plasius-demo__status-badge">Booting 3D scene\u2026</p>
6706
6764
  <p id="demoDetails" class="plasius-demo__status-text">
6707
- Preparing GLTF assets, cloth and fluid continuity plans, and adaptive quality metadata.
6765
+ Preparing a moonlit harbor scene, GLTF hull data, cloth and fluid continuity plans, and adaptive quality metadata.
6708
6766
  </p>
6709
6767
  </section>
6710
6768
  </section>
@@ -6732,9 +6790,9 @@ function buildDemoDom(root, options) {
6732
6790
  </div>
6733
6791
  <div class="plasius-demo__legend">
6734
6792
  <strong>Scene</strong>
6735
- GLTF ships carry collision metadata.<br />
6736
- Flag cloth and ocean waves scale by distance band.<br />
6737
- Ray-traced shadow and reflection style is preserved near the camera.
6793
+ GLTF ships carry hull mass and damping metadata.<br />
6794
+ Lanterns and torches warm the moonlit harbor.<br />
6795
+ Mass-aware collisions stay authoritative near the camera.
6738
6796
  </div>
6739
6797
  </section>
6740
6798
  <aside class="plasius-demo__sidebar">
@@ -6815,6 +6873,7 @@ function buildSceneSnapshot(state, shipModel) {
6815
6873
  })
6816
6874
  )
6817
6875
  ),
6876
+ shipPhysics: shipModel?.physics ?? null,
6818
6877
  physics: Object.freeze({
6819
6878
  profile: state.physics.profile,
6820
6879
  plan: state.physics.plan,
@@ -6858,28 +6917,39 @@ function readVisualNumber(value, fallback) {
6858
6917
  function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {}) {
6859
6918
  const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
6860
6919
  const defaults = {
6861
- skyTop: premiumShadows ? "#f0f7fb" : "#e8f1f7",
6862
- skyMid: premiumShadows ? "#c7d9e5" : "#b9ceda",
6863
- skyBottom: premiumShadows ? "#84a7bd" : "#7b9bb0",
6864
- seaTop: premiumShadows ? "#235064" : "#264c5f",
6865
- seaMid: premiumShadows ? "#153e53" : "#173d4f",
6866
- seaBottom: "#0b2433",
6867
- sunCore: "rgba(255, 244, 210, 0.9)",
6920
+ skyTop: premiumShadows ? "#040c1a" : "#06101f",
6921
+ skyMid: premiumShadows ? "#11203b" : "#152643",
6922
+ skyBottom: premiumShadows ? "#2f4468" : "#364d73",
6923
+ duskGlow: premiumShadows ? "rgba(116, 142, 201, 0.26)" : "rgba(104, 128, 188, 0.22)",
6924
+ seaTop: premiumShadows ? "#102946" : "#153050",
6925
+ seaMid: premiumShadows ? "#0a1d33" : "#0d2138",
6926
+ seaBottom: "#04101d",
6927
+ moonCore: "rgba(241, 246, 255, 0.98)",
6928
+ moonHalo: "rgba(167, 191, 255, 0.24)",
6929
+ moonReflection: "rgba(192, 214, 255, 0.22)",
6930
+ starColor: "rgba(232, 239, 255, 0.82)",
6931
+ ambientMist: "rgba(41, 63, 97, 0.16)",
6868
6932
  reflectionStrength: lightingSnapshot.currentLevel.config.reflectionStrength,
6869
6933
  shadowAccent: lightingSnapshot.currentLevel.config.shadowStrength,
6870
- waveAmplitude: 1,
6871
- waveDirection: { x: 0.86, z: 0.34 },
6872
- wavePhaseSpeed: 1,
6873
- wakeStrength: 0.24,
6874
- wakeLength: 15,
6875
- collisionRippleStrength: 0.34,
6876
- waterNear: { r: 0.12, g: 0.36, b: 0.46 },
6877
- waterFar: { r: 0.28, g: 0.56, b: 0.68 },
6878
- harborWall: { r: 0.48, g: 0.4, b: 0.32 },
6879
- harborDeck: { r: 0.5, g: 0.34, b: 0.22 },
6880
- harborTower: { r: 0.34, g: 0.32, b: 0.36 },
6881
- flagColor: { r: 0.76, g: 0.24, b: 0.18 },
6882
- flagMotion: 1
6934
+ waveAmplitude: 0.94,
6935
+ waveDirection: { x: 0.88, z: 0.28 },
6936
+ wavePhaseSpeed: 0.88,
6937
+ wakeStrength: 0.31,
6938
+ wakeLength: 18,
6939
+ collisionRippleStrength: 0.42,
6940
+ waterNear: { r: 0.08, g: 0.23, b: 0.33 },
6941
+ waterFar: { r: 0.18, g: 0.35, b: 0.49 },
6942
+ harborWall: { r: 0.26, g: 0.24, b: 0.28 },
6943
+ harborDeck: { r: 0.33, g: 0.22, b: 0.16 },
6944
+ harborTower: { r: 0.23, g: 0.24, b: 0.29 },
6945
+ flagColor: { r: 0.66, g: 0.16, b: 0.13 },
6946
+ flagMotion: 0.92,
6947
+ lanternCore: { r: 0.98, g: 0.8, b: 0.48 },
6948
+ lanternGlow: { r: 1, g: 0.56, b: 0.2 },
6949
+ lanternReflectionStrength: 0.42,
6950
+ torchCore: { r: 0.99, g: 0.72, b: 0.36 },
6951
+ torchGlow: { r: 0.98, g: 0.38, b: 0.15 },
6952
+ collisionFlash: "rgba(255, 212, 168, 0.16)"
6883
6953
  };
6884
6954
  return {
6885
6955
  skyTop: typeof customVisuals.skyTop === "string" ? customVisuals.skyTop : defaults.skyTop,
@@ -6888,7 +6958,12 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6888
6958
  seaTop: typeof customVisuals.seaTop === "string" ? customVisuals.seaTop : defaults.seaTop,
6889
6959
  seaMid: typeof customVisuals.seaMid === "string" ? customVisuals.seaMid : defaults.seaMid,
6890
6960
  seaBottom: typeof customVisuals.seaBottom === "string" ? customVisuals.seaBottom : defaults.seaBottom,
6891
- sunCore: typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.sunCore,
6961
+ duskGlow: typeof customVisuals.duskGlow === "string" ? customVisuals.duskGlow : defaults.duskGlow,
6962
+ moonCore: typeof customVisuals.moonCore === "string" ? customVisuals.moonCore : typeof customVisuals.sunCore === "string" ? customVisuals.sunCore : defaults.moonCore,
6963
+ moonHalo: typeof customVisuals.moonHalo === "string" ? customVisuals.moonHalo : defaults.moonHalo,
6964
+ moonReflection: typeof customVisuals.moonReflection === "string" ? customVisuals.moonReflection : defaults.moonReflection,
6965
+ starColor: typeof customVisuals.starColor === "string" ? customVisuals.starColor : defaults.starColor,
6966
+ ambientMist: typeof customVisuals.ambientMist === "string" ? customVisuals.ambientMist : defaults.ambientMist,
6892
6967
  reflectionStrength: readVisualNumber(
6893
6968
  customVisuals.reflectionStrength,
6894
6969
  defaults.reflectionStrength
@@ -6909,7 +6984,16 @@ function resolveVisualConfig(nearLighting, lightingSnapshot, customVisuals = {})
6909
6984
  harborDeck: normalizeColorOverride(customVisuals.harborDeck, defaults.harborDeck),
6910
6985
  harborTower: normalizeColorOverride(customVisuals.harborTower, defaults.harborTower),
6911
6986
  flagColor: normalizeColorOverride(customVisuals.flagColor, defaults.flagColor),
6912
- flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion)
6987
+ flagMotion: readVisualNumber(customVisuals.flagMotion, defaults.flagMotion),
6988
+ lanternCore: normalizeColorOverride(customVisuals.lanternCore, defaults.lanternCore),
6989
+ lanternGlow: normalizeColorOverride(customVisuals.lanternGlow, defaults.lanternGlow),
6990
+ lanternReflectionStrength: readVisualNumber(
6991
+ customVisuals.lanternReflectionStrength,
6992
+ defaults.lanternReflectionStrength
6993
+ ),
6994
+ torchCore: normalizeColorOverride(customVisuals.torchCore, defaults.torchCore),
6995
+ torchGlow: normalizeColorOverride(customVisuals.torchGlow, defaults.torchGlow),
6996
+ collisionFlash: typeof customVisuals.collisionFlash === "string" ? customVisuals.collisionFlash : defaults.collisionFlash
6913
6997
  };
6914
6998
  }
6915
6999
  function buildClothSurface(model, state, meshDetail, visuals) {
@@ -7144,18 +7228,34 @@ function createSceneState(options) {
7144
7228
  {
7145
7229
  id: "northwind",
7146
7230
  position: vec3(-5.2, 0, 7.2),
7147
- velocity: vec3(2.1, 0, -1.6),
7148
- rotationY: 0.42,
7149
- angularVelocity: 0.18,
7150
- tint: { r: 0.62, g: 0.39, b: 0.23 }
7231
+ velocity: vec3(2.35, 0, -1.08),
7232
+ rotationY: 0.58,
7233
+ angularVelocity: 0.09,
7234
+ tint: { r: 0.62, g: 0.39, b: 0.23 },
7235
+ massScale: 1.42,
7236
+ cruiseSpeed: 2.25,
7237
+ throttleResponse: 0.46,
7238
+ rudderResponse: 0.54,
7239
+ wanderPhase: 0.35,
7240
+ lanterns: SHIP_LANTERNS,
7241
+ lanternStrength: 1.06,
7242
+ collisionRadiusScale: 1.04
7151
7243
  },
7152
7244
  {
7153
7245
  id: "tidecaller",
7154
7246
  position: vec3(4.8, 0, 4.4),
7155
- velocity: vec3(-1.85, 0, 1.25),
7156
- rotationY: -2.62,
7157
- angularVelocity: -0.14,
7158
- tint: { r: 0.48, g: 0.28, b: 0.19 }
7247
+ velocity: vec3(-2.15, 0, 1.74),
7248
+ rotationY: -2.48,
7249
+ angularVelocity: -0.2,
7250
+ tint: { r: 0.48, g: 0.28, b: 0.19 },
7251
+ massScale: 0.84,
7252
+ cruiseSpeed: 2.68,
7253
+ throttleResponse: 0.7,
7254
+ rudderResponse: 0.78,
7255
+ wanderPhase: 1.6,
7256
+ lanterns: SHIP_LANTERNS,
7257
+ lanternStrength: 1.18,
7258
+ collisionRadiusScale: 0.94
7159
7259
  }
7160
7260
  ],
7161
7261
  sprays: [],
@@ -7177,41 +7277,71 @@ function setListContent(element, values) {
7177
7277
  element.innerHTML = values.map((value) => `<li>${value}</li>`).join("");
7178
7278
  }
7179
7279
  function drawSkyAndShore(ctx, canvas, state, nearLighting, reflectionStrength, shadowStrength, visuals) {
7180
- const premiumShadows = nearLighting.primaryShadowSource === "ray-traced-primary";
7181
7280
  const sky = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.5);
7182
7281
  sky.addColorStop(0, visuals.skyTop);
7183
- sky.addColorStop(0.6, visuals.skyMid);
7282
+ sky.addColorStop(0.54, visuals.skyMid);
7184
7283
  sky.addColorStop(1, visuals.skyBottom);
7185
7284
  ctx.fillStyle = sky;
7186
7285
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7286
+ for (let index = 0; index < 70; index += 1) {
7287
+ const x = pseudoRandom(index + 13) * canvas.width;
7288
+ const y = pseudoRandom(index * 7 + 5) * canvas.height * 0.42;
7289
+ const twinkle = 0.45 + Math.sin(state.time * 1.4 + index * 0.73) * 0.25;
7290
+ const radius = 0.6 + pseudoRandom(index * 11 + 2) * 1.9;
7291
+ ctx.fillStyle = visuals.starColor.replace(/[\d.]+\)$/u, `${clamp(twinkle, 0.16, 0.92)})`);
7292
+ ctx.beginPath();
7293
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
7294
+ ctx.fill();
7295
+ }
7296
+ const horizonGlow = ctx.createLinearGradient(0, canvas.height * 0.22, 0, canvas.height * 0.62);
7297
+ horizonGlow.addColorStop(0, "rgba(0, 0, 0, 0)");
7298
+ horizonGlow.addColorStop(1, visuals.duskGlow);
7299
+ ctx.fillStyle = horizonGlow;
7300
+ ctx.fillRect(0, canvas.height * 0.2, canvas.width, canvas.height * 0.45);
7187
7301
  const shoreline = ctx.createLinearGradient(0, canvas.height * 0.45, 0, canvas.height);
7188
7302
  shoreline.addColorStop(0, visuals.seaTop);
7189
7303
  shoreline.addColorStop(0.52, visuals.seaMid);
7190
7304
  shoreline.addColorStop(1, visuals.seaBottom);
7191
7305
  ctx.fillStyle = shoreline;
7192
7306
  ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7193
- const sunX = mix(canvas.width * 0.16, canvas.width * 0.84, (Math.sin(state.time * 0.12) + 1) * 0.5);
7194
- const sunY = canvas.height * 0.14 + Math.cos(state.time * 0.12) * 22;
7195
- const sun = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 90);
7196
- sun.addColorStop(0, visuals.sunCore);
7197
- sun.addColorStop(1, "rgba(255, 244, 210, 0)");
7198
- ctx.fillStyle = sun;
7307
+ const moonX = canvas.width * 0.76 + Math.sin(state.time * 0.045) * 18;
7308
+ const moonY = canvas.height * 0.17 + Math.cos(state.time * 0.05) * 10;
7309
+ const moon = ctx.createRadialGradient(moonX, moonY, 14, moonX, moonY, 126);
7310
+ moon.addColorStop(0, visuals.moonCore);
7311
+ moon.addColorStop(0.46, visuals.moonHalo);
7312
+ moon.addColorStop(1, "rgba(167, 191, 255, 0)");
7313
+ ctx.fillStyle = moon;
7314
+ ctx.beginPath();
7315
+ ctx.arc(moonX, moonY, 94, 0, Math.PI * 2);
7316
+ ctx.fill();
7317
+ const moonCore = ctx.createRadialGradient(moonX, moonY, 4, moonX, moonY, 28);
7318
+ moonCore.addColorStop(0, "rgba(255, 255, 255, 0.98)");
7319
+ moonCore.addColorStop(1, visuals.moonCore);
7320
+ ctx.fillStyle = moonCore;
7199
7321
  ctx.beginPath();
7200
- ctx.arc(sunX, sunY, 90, 0, Math.PI * 2);
7322
+ ctx.arc(moonX, moonY, 24, 0, Math.PI * 2);
7201
7323
  ctx.fill();
7202
- const track = ctx.createLinearGradient(sunX, canvas.height * 0.46, sunX, canvas.height * 0.96);
7203
- track.addColorStop(0, `rgba(255, 243, 214, ${0.08 + reflectionStrength * 0.18})`);
7204
- track.addColorStop(0.42, `rgba(224, 242, 255, ${0.04 + reflectionStrength * 0.2})`);
7205
- track.addColorStop(1, "rgba(224, 242, 255, 0)");
7324
+ const track = ctx.createLinearGradient(moonX, canvas.height * 0.44, moonX, canvas.height * 0.98);
7325
+ track.addColorStop(0, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.08 + reflectionStrength * 0.12})`));
7326
+ track.addColorStop(0.42, visuals.moonReflection.replace(/[\d.]+\)$/u, `${0.04 + reflectionStrength * 0.18})`));
7327
+ track.addColorStop(1, "rgba(192, 214, 255, 0)");
7206
7328
  ctx.save();
7207
7329
  ctx.globalCompositeOperation = "screen";
7208
7330
  ctx.fillStyle = track;
7209
7331
  ctx.beginPath();
7210
- ctx.ellipse(sunX, canvas.height * 0.72, 46 + shadowStrength * 60, canvas.height * 0.26, 0, 0, Math.PI * 2);
7332
+ ctx.ellipse(moonX, canvas.height * 0.75, 38 + shadowStrength * 42, canvas.height * 0.24, 0, 0, Math.PI * 2);
7211
7333
  ctx.fill();
7212
7334
  ctx.restore();
7335
+ const mist = ctx.createLinearGradient(0, canvas.height * 0.5, 0, canvas.height);
7336
+ mist.addColorStop(0, "rgba(0, 0, 0, 0)");
7337
+ mist.addColorStop(1, visuals.ambientMist);
7338
+ ctx.fillStyle = mist;
7339
+ ctx.fillRect(0, canvas.height * 0.45, canvas.width, canvas.height * 0.55);
7213
7340
  if (state.collisionFlash > 0.01) {
7214
- ctx.fillStyle = `rgba(255, 243, 228, ${state.collisionFlash * 0.14})`;
7341
+ ctx.fillStyle = visuals.collisionFlash.replace(
7342
+ /[\d.]+\)$/u,
7343
+ `${clamp(state.collisionFlash * 0.22, 0, 0.26)})`
7344
+ );
7215
7345
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7216
7346
  }
7217
7347
  }
@@ -7310,7 +7440,7 @@ function pushHarborGeometry(camera, viewport, triangles, visuals) {
7310
7440
  }
7311
7441
  }
7312
7442
  function renderShipRigging(ctx, ship, camera, viewport) {
7313
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7443
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7314
7444
  const mastBase = transformPoint(vec3(0, 0.38, -0.4), transform);
7315
7445
  const mastTop = transformPoint(vec3(0, 3.8, -0.2), transform);
7316
7446
  const aftBase = transformPoint(vec3(-0.25, 0.32, -1.9), transform);
@@ -7414,6 +7544,35 @@ function renderWaterHighlights(ctx, waterBands, camera, viewport) {
7414
7544
  }
7415
7545
  }
7416
7546
  }
7547
+ function readPhysicsNumber(physics, key, fallback) {
7548
+ const value = physics?.[key];
7549
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
7550
+ }
7551
+ function getShipMass(ship, shipModel) {
7552
+ const baseMass = readPhysicsNumber(shipModel.physics, "mass", 3200);
7553
+ return baseMass * readVisualNumber(ship.massScale, 1);
7554
+ }
7555
+ function getShipHalfExtents(ship, shipModel) {
7556
+ const physicsHalfExtents = Array.isArray(shipModel.physics.halfExtents) ? shipModel.physics.halfExtents : [1.35, 0.95, 3.9];
7557
+ const scale = SHIP_SCALE * readVisualNumber(ship.collisionRadiusScale, 1);
7558
+ return {
7559
+ x: physicsHalfExtents[0] * scale,
7560
+ y: physicsHalfExtents[1] * scale,
7561
+ z: physicsHalfExtents[2] * scale
7562
+ };
7563
+ }
7564
+ function getShipCollisionRadius(ship, shipModel) {
7565
+ const halfExtents = getShipHalfExtents(ship, shipModel);
7566
+ return Math.max(halfExtents.x * 1.08, halfExtents.z * 0.62);
7567
+ }
7568
+ function getShipInverseMass(ship, shipModel) {
7569
+ return 1 / Math.max(1, getShipMass(ship, shipModel));
7570
+ }
7571
+ function getShipInverseInertia(ship, shipModel) {
7572
+ const radius = getShipCollisionRadius(ship, shipModel);
7573
+ const inertia = getShipMass(ship, shipModel) * radius * radius * 0.72;
7574
+ return 1 / Math.max(1, inertia);
7575
+ }
7417
7576
  function spawnSpray(state, point, intensity) {
7418
7577
  const count = state.fluidDetail.getSnapshot().currentLevel.config.splashCount;
7419
7578
  for (let index = 0; index < count; index += 1) {
@@ -7426,55 +7585,182 @@ function spawnSpray(state, point, intensity) {
7426
7585
  });
7427
7586
  }
7428
7587
  }
7429
- function updateShips(state, dt, shipModel) {
7588
+ function resolveShipRoute(ship, state, radius) {
7589
+ if (typeof ship.routeDirection !== "number") {
7590
+ ship.routeDirection = ship.velocity.x >= 0 ? 1 : -1;
7591
+ }
7592
+ if (ship.position.x > HARBOR_BOUNDS.maxX - radius * 1.1) {
7593
+ ship.routeDirection = -1;
7594
+ } else if (ship.position.x < HARBOR_BOUNDS.minX + radius * 1.1) {
7595
+ ship.routeDirection = 1;
7596
+ }
7597
+ const wander = Math.sin(state.time * 0.22 + readVisualNumber(ship.wanderPhase, 0));
7598
+ const crossCurrent = Math.cos(state.time * 0.31 + readVisualNumber(ship.wanderPhase, 0));
7599
+ const laneCenter = ship.id === "northwind" ? 10.2 + wander * 2.1 + crossCurrent * 0.6 : 7 + wander * 3.3 - crossCurrent * 1.1;
7600
+ const targetX = ship.routeDirection > 0 ? HARBOR_BOUNDS.maxX - radius * 1.7 : HARBOR_BOUNDS.minX + radius * 1.7;
7601
+ return vec3(targetX, 0, clamp(laneCenter, HARBOR_BOUNDS.minZ + 1.8, HARBOR_BOUNDS.maxZ - 1.8));
7602
+ }
7603
+ function updateShipMotion(state, ship, dt, shipModel) {
7430
7604
  const physics = shipModel.physics;
7431
- const halfExtents = physics.halfExtents ?? [1.35, 0.95, 3.9];
7605
+ const massScale = Math.max(0.55, readVisualNumber(ship.massScale, 1));
7606
+ const radius = getShipCollisionRadius(ship, shipModel);
7607
+ const waterline = readPhysicsNumber(physics, "waterline", 0.42);
7608
+ const linearDamping = readPhysicsNumber(physics, "linearDamping", 0.04);
7609
+ const angularDamping = readPhysicsNumber(physics, "angularDamping", 0.08);
7610
+ const throttleResponse = readVisualNumber(ship.throttleResponse, 0.58);
7611
+ const rudderResponse = readVisualNumber(ship.rudderResponse, 0.62);
7612
+ const cruiseSpeed = readVisualNumber(ship.cruiseSpeed, 2.4);
7613
+ ship.collisionCooldown = Math.max(0, readVisualNumber(ship.collisionCooldown, 0) - dt);
7614
+ const forward = directionFromYaw(ship.rotationY);
7615
+ const lateral = perpendicularOnWater(forward);
7616
+ const routeTarget = resolveShipRoute(ship, state, radius);
7617
+ const desiredHeading = Math.atan2(routeTarget.x - ship.position.x, routeTarget.z - ship.position.z);
7618
+ const headingError = Math.atan2(
7619
+ Math.sin(desiredHeading - ship.rotationY),
7620
+ Math.cos(desiredHeading - ship.rotationY)
7621
+ );
7622
+ ship.angularVelocity += headingError * rudderResponse * dt * (1.18 / Math.sqrt(massScale)) + Math.sin(state.time * 0.9 + readVisualNumber(ship.wanderPhase, 0)) * dt * 0.04;
7623
+ const waveDirection = resolveWaveDirection(state);
7624
+ const forwardSpeed = dotVec3(ship.velocity, forward);
7625
+ const lateralSpeed = dotVec3(ship.velocity, lateral);
7626
+ const thrust = (cruiseSpeed - forwardSpeed) * throttleResponse;
7627
+ const currentDrift = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.016;
7628
+ const acceleration = addVec3(
7629
+ scaleVec3(forward, thrust),
7630
+ addVec3(
7631
+ scaleVec3(lateral, -lateralSpeed * (1.28 + rudderResponse * 0.4)),
7632
+ scaleVec3(waveDirection, currentDrift / Math.sqrt(massScale))
7633
+ )
7634
+ );
7635
+ ship.velocity = addVec3(ship.velocity, scaleVec3(acceleration, dt));
7636
+ ship.velocity = scaleVec3(
7637
+ ship.velocity,
7638
+ Math.max(0, 1 - linearDamping / Math.pow(massScale, 0.22) * dt)
7639
+ );
7640
+ ship.angularVelocity *= Math.max(
7641
+ 0,
7642
+ 1 - angularDamping / Math.pow(massScale, 0.15) * dt
7643
+ );
7644
+ ship.rotationY += ship.angularVelocity * dt;
7645
+ ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7646
+ ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.24 + waterline;
7647
+ }
7648
+ function resolveBoundaryCollision(ship, state, shipModel) {
7649
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.56;
7650
+ const radius = getShipCollisionRadius(ship, shipModel);
7651
+ const boundaries = [
7652
+ { axis: "x", min: HARBOR_BOUNDS.minX + radius, max: HARBOR_BOUNDS.maxX - radius, normalMin: vec3(1, 0, 0), normalMax: vec3(-1, 0, 0) },
7653
+ { axis: "z", min: HARBOR_BOUNDS.minZ + radius, max: HARBOR_BOUNDS.maxZ - radius, normalMin: vec3(0, 0, 1), normalMax: vec3(0, 0, -1) }
7654
+ ];
7655
+ for (const boundary of boundaries) {
7656
+ if (ship.position[boundary.axis] < boundary.min) {
7657
+ ship.position[boundary.axis] = boundary.min;
7658
+ const normal = boundary.normalMin;
7659
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7660
+ if (speedIntoWall < 0) {
7661
+ ship.velocity = subVec3(
7662
+ ship.velocity,
7663
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7664
+ );
7665
+ const tangent = vec3(-normal.z, 0, normal.x);
7666
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7667
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7668
+ ship.angularVelocity += tangentSpeed * 4e-3;
7669
+ }
7670
+ } else if (ship.position[boundary.axis] > boundary.max) {
7671
+ ship.position[boundary.axis] = boundary.max;
7672
+ const normal = boundary.normalMax;
7673
+ const speedIntoWall = dotVec3(ship.velocity, normal);
7674
+ if (speedIntoWall < 0) {
7675
+ ship.velocity = subVec3(
7676
+ ship.velocity,
7677
+ scaleVec3(normal, (1 + restitution) * speedIntoWall)
7678
+ );
7679
+ const tangent = vec3(-normal.z, 0, normal.x);
7680
+ const tangentSpeed = dotVec3(ship.velocity, tangent);
7681
+ ship.velocity = subVec3(ship.velocity, scaleVec3(tangent, tangentSpeed * 0.12));
7682
+ ship.angularVelocity += tangentSpeed * 4e-3;
7683
+ }
7684
+ }
7685
+ }
7686
+ }
7687
+ function resolveShipCollision(state, a, b, shipModel) {
7688
+ const delta = subVec3(b.position, a.position);
7689
+ const radiusA = getShipCollisionRadius(a, shipModel);
7690
+ const radiusB = getShipCollisionRadius(b, shipModel);
7691
+ const distance = Math.hypot(delta.x, delta.z);
7692
+ const minDistance = radiusA + radiusB;
7693
+ if (distance >= minDistance) {
7694
+ return false;
7695
+ }
7696
+ 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)));
7697
+ const tangent = vec3(-normal.z, 0, normal.x);
7698
+ const penetration = minDistance - distance;
7699
+ const invMassA = getShipInverseMass(a, shipModel);
7700
+ const invMassB = getShipInverseMass(b, shipModel);
7701
+ const invMassSum = invMassA + invMassB;
7702
+ const correction = scaleVec3(normal, penetration / Math.max(1e-4, invMassSum) * 0.72);
7703
+ a.position = subVec3(a.position, scaleVec3(correction, invMassA));
7704
+ b.position = addVec3(b.position, scaleVec3(correction, invMassB));
7705
+ const relativeVelocity = subVec3(b.velocity, a.velocity);
7706
+ const velocityAlongNormal = dotVec3(relativeVelocity, normal);
7707
+ const restitution = readPhysicsNumber(shipModel.physics, "restitution", 0.22) * 0.88;
7708
+ if (velocityAlongNormal < 0) {
7709
+ const impulseMagnitude = -(1 + restitution) * velocityAlongNormal / Math.max(1e-4, invMassSum);
7710
+ const impulse = scaleVec3(normal, impulseMagnitude);
7711
+ a.velocity = subVec3(a.velocity, scaleVec3(impulse, invMassA));
7712
+ b.velocity = addVec3(b.velocity, scaleVec3(impulse, invMassB));
7713
+ const tangentSpeed = dotVec3(relativeVelocity, tangent);
7714
+ const frictionMagnitude = clamp(
7715
+ -tangentSpeed / Math.max(1e-4, invMassSum),
7716
+ -impulseMagnitude * 0.16,
7717
+ impulseMagnitude * 0.16
7718
+ );
7719
+ const frictionImpulse = scaleVec3(tangent, frictionMagnitude);
7720
+ a.velocity = subVec3(a.velocity, scaleVec3(frictionImpulse, invMassA));
7721
+ b.velocity = addVec3(b.velocity, scaleVec3(frictionImpulse, invMassB));
7722
+ a.angularVelocity -= tangentSpeed * radiusA * getShipInverseInertia(a, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7723
+ b.angularVelocity += tangentSpeed * radiusB * getShipInverseInertia(b, shipModel) * 0.2 + impulseMagnitude * 24e-5;
7724
+ const impactSpeed = Math.abs(velocityAlongNormal);
7725
+ if (impactSpeed > 0.18 && Math.max(readVisualNumber(a.collisionCooldown, 0), readVisualNumber(b.collisionCooldown, 0)) <= 0) {
7726
+ const contactPoint = vec3(
7727
+ (a.position.x + b.position.x) * 0.5,
7728
+ (a.position.y + b.position.y) * 0.5 + 0.14,
7729
+ (a.position.z + b.position.z) * 0.5
7730
+ );
7731
+ spawnSpray(state, contactPoint, impactSpeed * 2.4 + penetration * 8);
7732
+ state.waveImpulses.push({
7733
+ x: contactPoint.x,
7734
+ z: contactPoint.z,
7735
+ strength: clamp(0.24 + impactSpeed * 0.46 + penetration * 0.9, 0.2, 1.7),
7736
+ radius: 0.9 + penetration * 1.4,
7737
+ life: 1
7738
+ });
7739
+ state.collisionCount += 1;
7740
+ state.collisionFlash = Math.max(
7741
+ state.collisionFlash,
7742
+ clamp(impactSpeed * 0.55 + penetration * 1.8, 0.16, 1)
7743
+ );
7744
+ a.collisionCooldown = 0.2;
7745
+ b.collisionCooldown = 0.2;
7746
+ }
7747
+ }
7748
+ state.contactCount += 1;
7749
+ return true;
7750
+ }
7751
+ function updateShips(state, dt, shipModel) {
7432
7752
  let collided = false;
7433
7753
  state.contactCount = 0;
7434
7754
  for (const ship of state.ships) {
7435
- ship.position = addVec3(ship.position, scaleVec3(ship.velocity, dt));
7436
- ship.rotationY += ship.angularVelocity * dt;
7437
- ship.velocity = scaleVec3(ship.velocity, 1 - (physics.linearDamping ?? 0.04) * dt);
7438
- ship.angularVelocity *= 1 - (physics.angularDamping ?? 0.08) * dt;
7439
- ship.position.y = sampleWave(state, ship.position.x, ship.position.z, state.time) * 0.22 + (physics.waterline ?? 0.42);
7440
- if (Math.abs(ship.position.x) > 10) {
7441
- ship.velocity.x *= -1;
7442
- ship.angularVelocity *= -1;
7443
- }
7444
- if (ship.position.z < 2 || ship.position.z > 16) {
7445
- ship.velocity.z *= -1;
7446
- ship.angularVelocity *= -1;
7447
- }
7755
+ updateShipMotion(state, ship, dt, shipModel);
7756
+ resolveBoundaryCollision(ship, state, shipModel);
7448
7757
  }
7449
- const [a, b] = state.ships;
7450
- const dx = b.position.x - a.position.x;
7451
- const dz = b.position.z - a.position.z;
7452
- const overlapX = Math.abs(dx) < halfExtents[0] * 1.7;
7453
- const overlapZ = Math.abs(dz) < halfExtents[2] * 0.8;
7454
- if (overlapX && overlapZ) {
7455
- const restitution = physics.restitution ?? 0.22;
7456
- const swapX = a.velocity.x;
7457
- const swapZ = a.velocity.z;
7458
- a.velocity.x = -b.velocity.x * (0.86 + restitution);
7459
- a.velocity.z = -b.velocity.z * (0.82 + restitution);
7460
- b.velocity.x = -swapX * (0.86 + restitution);
7461
- b.velocity.z = -swapZ * (0.82 + restitution);
7462
- a.angularVelocity += 0.55;
7463
- b.angularVelocity -= 0.55;
7464
- 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);
7465
- spawnSpray(state, contactPoint, Math.abs(dx) + Math.abs(dz));
7466
- state.waveImpulses.push({
7467
- x: contactPoint.x,
7468
- z: contactPoint.z,
7469
- strength: Math.min(1.4, 0.2 + (Math.abs(dx) + Math.abs(dz)) * 0.18),
7470
- radius: 0.8,
7471
- life: 1
7472
- });
7473
- state.collisionCount += 1;
7474
- state.contactCount = 1;
7475
- collided = true;
7758
+ for (let index = 0; index < state.ships.length; index += 1) {
7759
+ for (let otherIndex = index + 1; otherIndex < state.ships.length; otherIndex += 1) {
7760
+ collided = resolveShipCollision(state, state.ships[index], state.ships[otherIndex], shipModel) || collided;
7761
+ }
7476
7762
  }
7477
- state.collisionFlash = collided ? 1 : Math.max(0, state.collisionFlash - dt * 1.8);
7763
+ state.collisionFlash = collided ? Math.max(0.12, state.collisionFlash) : Math.max(0, state.collisionFlash - dt * 1.3);
7478
7764
  }
7479
7765
  function updateWaveImpulses(state, dt) {
7480
7766
  state.waveImpulses = state.waveImpulses.map((impulse) => ({
@@ -7587,7 +7873,7 @@ function renderFlagPole(ctx, camera, viewport) {
7587
7873
  function renderShipShadow(ctx, shipModel, ship, state, camera, viewport, lightDir, shadowStrength) {
7588
7874
  const bounds = shipModel.bounds;
7589
7875
  const keelY = (shipModel.physics.waterline ?? 0.42) - 0.28;
7590
- const transform = { position: ship.position, rotationY: ship.rotationY, scale: 1.1 };
7876
+ const transform = { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE };
7591
7877
  const hullCorners = [
7592
7878
  vec3(bounds.min[0], keelY, bounds.min[2]),
7593
7879
  vec3(bounds.max[0], keelY, bounds.min[2]),
@@ -7613,6 +7899,97 @@ function renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength
7613
7899
  blur: 12 + shadowStrength * 20
7614
7900
  });
7615
7901
  }
7902
+ function renderGlowLight(ctx, point, camera, viewport, coreColor, glowColor, glowScale, reflectionStrength = 0, state = null) {
7903
+ const projected = projectPoint(point, camera, viewport);
7904
+ if (!projected) {
7905
+ return;
7906
+ }
7907
+ const radius = clamp(1 / projected.depth * 420 * glowScale, 4, 34);
7908
+ const halo = ctx.createRadialGradient(projected.x, projected.y, radius * 0.12, projected.x, projected.y, radius);
7909
+ halo.addColorStop(0, colorToRgba(coreColor, 0.98));
7910
+ halo.addColorStop(0.5, colorToRgba(glowColor, 0.42));
7911
+ halo.addColorStop(1, colorToRgba(glowColor, 0));
7912
+ ctx.save();
7913
+ ctx.globalCompositeOperation = "screen";
7914
+ ctx.fillStyle = halo;
7915
+ ctx.beginPath();
7916
+ ctx.arc(projected.x, projected.y, radius, 0, Math.PI * 2);
7917
+ ctx.fill();
7918
+ ctx.restore();
7919
+ ctx.fillStyle = colorToRgba(coreColor, 0.98);
7920
+ ctx.beginPath();
7921
+ ctx.arc(projected.x, projected.y, Math.max(1.2, radius * 0.16), 0, Math.PI * 2);
7922
+ ctx.fill();
7923
+ if (state && reflectionStrength > 0) {
7924
+ const waterline = sampleWave(state, point.x, point.z, state.time) * 0.22;
7925
+ const reflectedPoint = vec3(point.x, waterline - (point.y - waterline) * 0.58, point.z + 0.08);
7926
+ const reflected = projectPoint(reflectedPoint, camera, viewport);
7927
+ if (reflected) {
7928
+ const reflectionRadius = radius * 0.72;
7929
+ const glow = ctx.createRadialGradient(
7930
+ reflected.x,
7931
+ reflected.y,
7932
+ reflectionRadius * 0.1,
7933
+ reflected.x,
7934
+ reflected.y,
7935
+ reflectionRadius
7936
+ );
7937
+ glow.addColorStop(0, colorToRgba(coreColor, reflectionStrength * 0.34));
7938
+ glow.addColorStop(1, colorToRgba(glowColor, 0));
7939
+ ctx.save();
7940
+ ctx.globalCompositeOperation = "screen";
7941
+ ctx.fillStyle = glow;
7942
+ ctx.beginPath();
7943
+ ctx.ellipse(
7944
+ reflected.x,
7945
+ reflected.y,
7946
+ reflectionRadius * 0.34,
7947
+ reflectionRadius,
7948
+ 0,
7949
+ 0,
7950
+ Math.PI * 2
7951
+ );
7952
+ ctx.fill();
7953
+ ctx.restore();
7954
+ }
7955
+ }
7956
+ }
7957
+ function renderShipLanterns(ctx, ship, state, camera, viewport, visuals) {
7958
+ const lanterns = Array.isArray(ship.lanterns) ? ship.lanterns : SHIP_LANTERNS;
7959
+ const strength = readVisualNumber(ship.lanternStrength, 1);
7960
+ for (const lantern of lanterns) {
7961
+ const position = transformPoint(
7962
+ vec3(lantern.x, lantern.y, lantern.z),
7963
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE }
7964
+ );
7965
+ renderGlowLight(
7966
+ ctx,
7967
+ position,
7968
+ camera,
7969
+ viewport,
7970
+ visuals.lanternCore,
7971
+ visuals.lanternGlow,
7972
+ lantern.glow * strength,
7973
+ visuals.lanternReflectionStrength,
7974
+ state
7975
+ );
7976
+ }
7977
+ }
7978
+ function renderHarborTorches(ctx, state, camera, viewport, visuals) {
7979
+ for (const torch of HARBOR_TORCHES) {
7980
+ renderGlowLight(
7981
+ ctx,
7982
+ vec3(torch.x, torch.y, torch.z),
7983
+ camera,
7984
+ viewport,
7985
+ visuals.torchCore,
7986
+ visuals.torchGlow,
7987
+ torch.glow,
7988
+ visuals.lanternReflectionStrength * 0.55,
7989
+ state
7990
+ );
7991
+ }
7992
+ }
7616
7993
  function renderScene(ctx, canvas, state, shipModel, dom) {
7617
7994
  const viewport = { width: canvas.width, height: canvas.height };
7618
7995
  const camera = buildCamera(state, canvas);
@@ -7622,7 +7999,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7622
7999
  importance: state.focus === "lighting" ? "critical" : "high"
7623
8000
  });
7624
8001
  const nearLighting = lightingPlan.bands.find((entry) => entry.band === "near") ?? lightingPlan.bands[0];
7625
- const lightDir = normalizeVec3(vec3(-0.45, 0.85, -0.24));
8002
+ const lightDir = normalizeVec3(vec3(-0.22, 0.94, -0.31));
7626
8003
  const lightingSnapshot = state.lightingDetail.getSnapshot();
7627
8004
  const visuals = resolveVisualConfig(
7628
8005
  nearLighting,
@@ -7696,7 +8073,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7696
8073
  for (const ship of state.ships) {
7697
8074
  buildTrianglesFromMesh(
7698
8075
  shipModel,
7699
- { position: ship.position, rotationY: ship.rotationY, scale: 1.1 },
8076
+ { position: ship.position, rotationY: ship.rotationY, scale: SHIP_SCALE },
7700
8077
  ship.tint,
7701
8078
  camera,
7702
8079
  viewport,
@@ -7710,10 +8087,12 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7710
8087
  renderFlagShadow(ctx, cloth, camera, viewport, lightDir, shadowStrength);
7711
8088
  drawTriangles(ctx, triangles, lightDir, reflectionStrength, camera, shadowStrength);
7712
8089
  renderWaterHighlights(ctx, water.bandMeshes, camera, viewport);
8090
+ renderHarborTorches(ctx, state, camera, viewport, visuals);
7713
8091
  renderFlagPole(ctx, camera, viewport);
7714
8092
  renderClothAccent(ctx, cloth, camera, viewport);
7715
8093
  for (const ship of state.ships) {
7716
8094
  renderShipRigging(ctx, ship, camera, viewport);
8095
+ renderShipLanterns(ctx, ship, state, camera, viewport, visuals);
7717
8096
  }
7718
8097
  renderSprays(ctx, state.sprays, camera, viewport);
7719
8098
  const debugSnapshot = state.debugSession.getSnapshot();
@@ -7725,8 +8104,10 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7725
8104
  const sceneMetrics = [
7726
8105
  `focus: ${state.focus}`,
7727
8106
  `ships: ${state.ships.length} active GLTF hulls`,
8107
+ `moonlight: cold overhead key + ${HARBOR_TORCHES.length + state.ships.length * SHIP_LANTERNS.length} warm deck and harbor lights`,
7728
8108
  `physics snapshot: ${state.physics.snapshot.stage} (${state.physics.snapshot.stability})`,
7729
- `physics contacts: ${state.physics.snapshot.contactCount ?? 0}`,
8109
+ `physics contacts: ${state.contactCount}`,
8110
+ `mass split: ${state.ships.map((ship) => `${ship.id} ${(getShipMass(ship, shipModel) / 1e3).toFixed(1)}t`).join(" \xB7 ")}`,
7730
8111
  `cloth band: ${cloth.band} -> ${cloth.representation.output}`,
7731
8112
  `fluid near band: ${water.bandMeshes[0].representation.output}`,
7732
8113
  `lighting profile: ${lightingPlan.profile} (${lightingDistanceBands.length} bands)`
@@ -7749,8 +8130,8 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7749
8130
  ];
7750
8131
  const sceneNotes = state.focus === "physics" ? [
7751
8132
  "Stable world snapshots are taken after the authoritative rigid-body commit and before visual follow-up work.",
7752
- "The ships collide on GLTF-derived hull volumes while cloth and fluid remain downstream visual consumers.",
7753
- "Near-field lighting keeps the ray-traced-primary shadow impression so the collision read stays crisp."
8133
+ "The ships collide with mass-weighted impulses and positional correction, so the heavier hull keeps more of its line.",
8134
+ "Moonlight keeps the overall read legible while lanterns and torches make collision moments easy to track against the water."
7754
8135
  ] : SCENE_NOTES;
7755
8136
  const custom = state.demoDescription ?? null;
7756
8137
  setListContent(
@@ -7767,7 +8148,7 @@ function renderScene(ctx, canvas, state, shipModel, dom) {
7767
8148
  );
7768
8149
  setListContent(dom.sceneNotes, Array.isArray(custom?.notes) ? custom.notes : sceneNotes);
7769
8150
  dom.status.textContent = typeof custom?.status === "string" ? custom.status : `3D scene live \xB7 ${state.lastDecision.metrics.fps.toFixed(1)} FPS`;
7770
- 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}.`;
8151
+ 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}.`;
7771
8152
  }
7772
8153
  function updateSceneState(state, dt, shipModel) {
7773
8154
  updateShips(state, dt, shipModel);
@@ -7786,7 +8167,9 @@ function syncTextState(state, shipModel) {
7786
8167
  y: Number(ship.position.y.toFixed(2)),
7787
8168
  z: Number(ship.position.z.toFixed(2)),
7788
8169
  vx: Number(ship.velocity.x.toFixed(2)),
7789
- vz: Number(ship.velocity.z.toFixed(2))
8170
+ vz: Number(ship.velocity.z.toFixed(2)),
8171
+ massKg: Math.round(getShipMass(ship, shipModel)),
8172
+ lanterns: Array.isArray(ship.lanterns) ? ship.lanterns.length : 0
7790
8173
  })),
7791
8174
  shipPhysics: shipModel.physics,
7792
8175
  sprays: state.sprays.length,
@@ -7814,6 +8197,7 @@ function syncTextState(state, shipModel) {
7814
8197
  async function mountGpuShowcase(options = {}) {
7815
8198
  injectStyles();
7816
8199
  const root = options.root ?? document.body;
8200
+ root.classList?.add?.(ROOT_CLASS);
7817
8201
  const previousMarkup = root.innerHTML;
7818
8202
  const previousRenderGameToText = window.render_game_to_text;
7819
8203
  const previousAdvanceTime = window.advanceTime;
@@ -7833,8 +8217,11 @@ async function mountGpuShowcase(options = {}) {
7833
8217
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7834
8218
  syncTextState(state, shipModel);
7835
8219
  const ctx = dom.canvas.getContext("2d");
8220
+ if (!ctx) {
8221
+ throw new Error("2D canvas context is required for the shared showcase.");
8222
+ }
8223
+ let animationFrameId = 0;
7836
8224
  let destroyed = false;
7837
- let frameHandle = null;
7838
8225
  const renderFrame = (nowMs) => {
7839
8226
  if (destroyed) {
7840
8227
  return;
@@ -7855,7 +8242,7 @@ async function mountGpuShowcase(options = {}) {
7855
8242
  state.demoDescription = resolveSceneDescription(state, options, shipModel).description;
7856
8243
  renderScene(ctx, dom.canvas, state, shipModel, dom);
7857
8244
  syncTextState(state, shipModel);
7858
- frameHandle = requestAnimationFrame(renderFrame);
8245
+ animationFrameId = requestAnimationFrame(renderFrame);
7859
8246
  };
7860
8247
  const handlePauseClick = () => {
7861
8248
  state.paused = !state.paused;
@@ -7874,34 +8261,43 @@ async function mountGpuShowcase(options = {}) {
7874
8261
  dom.pauseButton.addEventListener("click", handlePauseClick);
7875
8262
  dom.stressToggle.addEventListener("change", handleStressChange);
7876
8263
  dom.focusMode.addEventListener("change", handleFocusChange);
7877
- frameHandle = requestAnimationFrame(renderFrame);
8264
+ animationFrameId = requestAnimationFrame(renderFrame);
8265
+ const destroy = () => {
8266
+ if (destroyed) {
8267
+ return;
8268
+ }
8269
+ destroyed = true;
8270
+ if (animationFrameId) {
8271
+ cancelAnimationFrame(animationFrameId);
8272
+ }
8273
+ dom.pauseButton.removeEventListener("click", handlePauseClick);
8274
+ dom.stressToggle.removeEventListener("change", handleStressChange);
8275
+ dom.focusMode.removeEventListener("change", handleFocusChange);
8276
+ try {
8277
+ if (typeof options.destroyState === "function") {
8278
+ options.destroyState(state.packageState);
8279
+ }
8280
+ } finally {
8281
+ state.packageState = void 0;
8282
+ }
8283
+ root.classList?.remove?.(ROOT_CLASS);
8284
+ root.innerHTML = previousMarkup;
8285
+ if (typeof previousRenderGameToText === "function") {
8286
+ window.render_game_to_text = previousRenderGameToText;
8287
+ } else {
8288
+ delete window.render_game_to_text;
8289
+ }
8290
+ if (typeof previousAdvanceTime === "function") {
8291
+ window.advanceTime = previousAdvanceTime;
8292
+ } else {
8293
+ delete window.advanceTime;
8294
+ }
8295
+ };
7878
8296
  return {
7879
8297
  state,
7880
8298
  shipModel,
7881
8299
  canvas: dom.canvas,
7882
- destroy() {
7883
- if (destroyed) {
7884
- return;
7885
- }
7886
- destroyed = true;
7887
- if (frameHandle != null) {
7888
- cancelAnimationFrame(frameHandle);
7889
- }
7890
- dom.pauseButton.removeEventListener("click", handlePauseClick);
7891
- dom.stressToggle.removeEventListener("change", handleStressChange);
7892
- dom.focusMode.removeEventListener("change", handleFocusChange);
7893
- root.innerHTML = previousMarkup;
7894
- if (typeof previousRenderGameToText === "function") {
7895
- window.render_game_to_text = previousRenderGameToText;
7896
- } else {
7897
- delete window.render_game_to_text;
7898
- }
7899
- if (typeof previousAdvanceTime === "function") {
7900
- window.advanceTime = previousAdvanceTime;
7901
- } else {
7902
- delete window.advanceTime;
7903
- }
7904
- }
8300
+ destroy
7905
8301
  };
7906
8302
  }
7907
8303
  function updatePhysicsSnapshot(state, shipModel) {
@@ -7915,7 +8311,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7915
8311
  animationInputRevision: state.frame,
7916
8312
  bodyCount: state.ships.length + 2,
7917
8313
  dynamicBodyCount: state.ships.length,
7918
- contactCount: state.collisionFlash > 0.02 ? 1 : 0,
8314
+ contactCount: state.contactCount,
7919
8315
  metadata: {
7920
8316
  collisionCount: state.collisionCount,
7921
8317
  contactCount: state.contactCount,
@@ -7924,7 +8320,7 @@ function updatePhysicsSnapshot(state, shipModel) {
7924
8320
  }
7925
8321
  });
7926
8322
  }
7927
- var STYLE_ID, DEFAULT_TITLE, DEFAULT_SUBTITLE, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH;
8323
+ var STYLE_ID, ROOT_CLASS, DEFAULT_TITLE, DEFAULT_SUBTITLE, SHIP_SCALE, HARBOR_BOUNDS, CAMERA_PRESETS, showcaseFocusModes, SCENE_NOTES, UNIT_BOX_MESH, SHIP_LANTERNS, HARBOR_TORCHES;
7928
8324
  var init_showcase_runtime = __esm({
7929
8325
  "src/showcase-runtime.js"() {
7930
8326
  init_dist();
@@ -7936,8 +8332,16 @@ var init_showcase_runtime = __esm({
7936
8332
  init_asset_url();
7937
8333
  init_gltf_loader();
7938
8334
  STYLE_ID = "plasius-shared-3d-showcase-style";
8335
+ ROOT_CLASS = "plasius-showcase-root";
7939
8336
  DEFAULT_TITLE = "Flag by the Sea";
7940
8337
  DEFAULT_SUBTITLE = "Shared 3D validation scene using GLTF ships, cloth, fluid continuity, adaptive performance, and telemetry.";
8338
+ SHIP_SCALE = 1.1;
8339
+ HARBOR_BOUNDS = Object.freeze({
8340
+ minX: -11.2,
8341
+ maxX: 11.2,
8342
+ minZ: 1.8,
8343
+ maxZ: 17.2
8344
+ });
7941
8345
  CAMERA_PRESETS = Object.freeze({
7942
8346
  integrated: Object.freeze({ yaw: -0.55, pitch: 0.34, distance: 27, target: [0, 2.2, 0] }),
7943
8347
  lighting: Object.freeze({ yaw: -0.28, pitch: 0.28, distance: 23, target: [0, 2.8, 0] }),
@@ -7949,10 +8353,10 @@ var init_showcase_runtime = __esm({
7949
8353
  });
7950
8354
  showcaseFocusModes = Object.freeze(Object.keys(CAMERA_PRESETS));
7951
8355
  SCENE_NOTES = Object.freeze([
7952
- "Ships are loaded from a GLTF asset and carry physics metadata from node extras.",
7953
- "Near-field lighting uses the ray-traced-primary shadow and reflection path before stepping down by distance band.",
7954
- "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands.",
7955
- "Performance pressure reduces visual detail before authoritative collision motion is touched."
8356
+ "Ships are loaded from a GLTF asset and carry mass, damping, restitution, and hull extents from node extras.",
8357
+ "Moonlight sets the cold ambient read while deck lanterns and harbor torches provide warm local contrast.",
8358
+ "Cloth and fluid continuity stay coherent across near, mid, far, and horizon bands even in the darker night palette.",
8359
+ "Performance pressure reduces visual detail before mass-weighted authoritative collision motion is touched."
7956
8360
  ]);
7957
8361
  UNIT_BOX_MESH = Object.freeze({
7958
8362
  positions: Object.freeze([
@@ -8020,6 +8424,17 @@ var init_showcase_runtime = __esm({
8020
8424
  0
8021
8425
  ])
8022
8426
  });
8427
+ SHIP_LANTERNS = Object.freeze([
8428
+ Object.freeze({ x: 0.94, y: 1.54, z: 2.52, glow: 1 }),
8429
+ Object.freeze({ x: -0.9, y: 1.58, z: 2.44, glow: 0.92 }),
8430
+ Object.freeze({ x: 0.62, y: 1.42, z: -2.18, glow: 0.88 }),
8431
+ Object.freeze({ x: -0.58, y: 1.46, z: -2.04, glow: 0.84 })
8432
+ ]);
8433
+ HARBOR_TORCHES = Object.freeze([
8434
+ Object.freeze({ x: -5.2, y: 1.25, z: 1.36, glow: 1.1 }),
8435
+ Object.freeze({ x: -8.6, y: 2.48, z: -0.72, glow: 1 }),
8436
+ Object.freeze({ x: -10.4, y: 1.28, z: 0.82, glow: 0.92 })
8437
+ ]);
8023
8438
  }
8024
8439
  });
8025
8440