ugly-game 0.5.5 → 0.5.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,6 +1,3 @@
1
- /** One cube instance: world position, per-axis scale, a hue (< 0 ⇒ neutral ground), and an optional
2
- * emissive flag (> 0 ⇒ the cube self-glows at full brightness, bypassing sun/shadow/AO — used for the
3
- * place ghost, destroy marker, projectiles, and sparkles so effect cubes read as distinct from terrain). */
4
1
  export interface CubeInstance {
5
2
  x: number;
6
3
  y: number;
@@ -37,7 +34,27 @@ export declare class GpuShadowRenderer {
37
34
  private overlayBG;
38
35
  private overlayScratch;
39
36
  private overlayCount;
37
+ private cullPipe;
38
+ private cullBGL;
39
+ private cap;
40
+ private camArgs;
41
+ private camVis;
42
+ private camCull;
43
+ private camCullBG;
44
+ private lightArgs;
45
+ private lightVis;
46
+ private lightCull;
47
+ private lightCullBG;
48
+ private statsBuf;
49
+ /** Last frame's visible counts (main-pass camera cull, shadow-pass light cull) + total — for IR/telemetry. */
50
+ lastCull: {
51
+ total: number;
52
+ camVisible: number;
53
+ lightVisible: number;
54
+ };
40
55
  constructor(device: GPUDevice, format: GPUTextureFormat);
56
+ private makeCullBuf;
57
+ private makeCullResources;
41
58
  private scratch;
42
59
  private fill;
43
60
  private makeBindGroups;
@@ -48,6 +65,15 @@ export declare class GpuShadowRenderer {
48
65
  updateInstances(instances: CubeInstance[]): void;
49
66
  /** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
50
67
  updateOverlay(instances: CubeInstance[]): void;
68
+ /** Write a frustum's cull uniform: 6 Gribb-Hartmann planes from `vp` + the live instance count. */
69
+ private writeCull;
51
70
  /** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. */
52
71
  frame(enc: GPUCommandEncoder, colorView: GPUTextureView, sceneDepth: GPUTextureView, camVP: Float32Array, lightVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], ambient: number, load?: boolean): void;
72
+ /** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
73
+ * safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
74
+ readCullStats(): Promise<{
75
+ total: number;
76
+ camVisible: number;
77
+ lightVisible: number;
78
+ }>;
53
79
  }
package/dist/gpuShadow.js CHANGED
@@ -8,7 +8,12 @@
8
8
  // Everything is a cube instance — including the ground (a big flat cube) — so one geometry
9
9
  // and one vertex path serve both passes. Per-instance non-uniform scale (vec3) lets the
10
10
  // ground be flat while props are boxy.
11
+ /** One cube instance: world position, per-axis scale, a hue (< 0 ⇒ neutral ground), and an optional
12
+ * emissive flag (> 0 ⇒ the cube self-glows at full brightness, bypassing sun/shadow/AO — used for the
13
+ * place ghost, destroy marker, projectiles, and sparkles so effect cubes read as distinct from terrain). */
14
+ import { frustumPlanes } from './mat4';
11
15
  const SHADOW_SIZE = 2048;
16
+ const CULL_WG = 64; // compute-cull workgroup size
12
17
  const OVERLAY_CAP = 512; // max translucent overlay cubes per frame (the place ghost + headroom)
13
18
  // Build the 36-vertex cube (6 faces × 2 tris) in JS and inject as WGSL const arrays —
14
19
  // bulletproof + readable vs. bit-twiddling from vertex_index. cullMode is 'none', so winding
@@ -61,19 +66,60 @@ fn hsv(hh: f32) -> vec3<f32> {
61
66
  if (h < 5.0) { return vec3(x, 0.0, 1.0); } return vec3(1.0, 0.0, x);
62
67
  }
63
68
  `;
69
+ // GPU-DRIVEN CULLING. The whole instance buffer stays resident; a compute pass frustum-culls it and compacts the
70
+ // survivors' indices into a `visible` list + an indirect draw's instanceCount. The draw passes then index
71
+ // insts[visible[ii]] and drawIndirect the compacted count — so the vertex shader + rasterizer only ever touch
72
+ // in-frustum cubes. Two culls per frame: the MAIN pass against the CAMERA frustum, the SHADOW pass against the
73
+ // LIGHT (ortho) frustum, so shadows from off-screen occluders stay correct while both passes shrink.
74
+ //
75
+ // The test is an ORIENTED-BOX vs plane separating test (NOT a bounding sphere). Each cube's world extent along a
76
+ // plane normal n is |n·bx|+|n·by|+|n·bz|, where bx/by/bz are the box's rotated semi-axes (0.5·scl rotated by the
77
+ // instance quaternion). This is exact for any size/aspect/rotation — critical because a bounding SPHERE from the
78
+ // centre wrongly culls a big flat cube (a ground plane whose centre sits far behind the camera while the ground
79
+ // underfoot is plainly in view). Mirrors gpuDriven.ts; validated against a CPU box-vs-plane oracle.
80
+ const CULL_WGSL = /* wgsl */ `
81
+ struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> };
82
+ struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
83
+ struct CullCam { planes: array<vec4<f32>, 6>, cnt: vec4<f32> }; // cnt.x = instance count
84
+ @group(0) @binding(0) var<storage, read> insts: array<Inst>;
85
+ @group(0) @binding(1) var<storage, read_write> args: Args;
86
+ @group(0) @binding(2) var<storage, read_write> visible: array<u32>;
87
+ @group(0) @binding(3) var<uniform> cc: CullCam;
88
+ fn qr(q: vec4<f32>, v: vec3<f32>) -> vec3<f32> { let t = 2.0 * cross(q.xyz, v); return v + q.w * t + cross(q.xyz, t); }
89
+ @compute @workgroup_size(${CULL_WG})
90
+ fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
91
+ let i = gid.x;
92
+ if (i >= u32(cc.cnt.x)) { return; }
93
+ let inst = insts[i];
94
+ let c = inst.pos.xyz;
95
+ let h = 0.5 * inst.scl.xyz;
96
+ let bx = qr(inst.rot, vec3<f32>(h.x, 0.0, 0.0)); // rotated box semi-axes
97
+ let by = qr(inst.rot, vec3<f32>(0.0, h.y, 0.0));
98
+ let bz = qr(inst.rot, vec3<f32>(0.0, 0.0, h.z));
99
+ for (var p = 0u; p < 6u; p++) {
100
+ let pl = cc.planes[p];
101
+ let e = abs(dot(pl.xyz, bx)) + abs(dot(pl.xyz, by)) + abs(dot(pl.xyz, bz)); // box half-width along the normal
102
+ if (dot(pl.xyz, c) + pl.w < -e) { return; }
103
+ }
104
+ let slot = atomicAdd(&args.ic, 1u);
105
+ visible[slot] = i;
106
+ }
107
+ `;
64
108
  const SHADOW_WGSL = COMMON_WGSL + /* wgsl */ `
109
+ @group(0) @binding(2) var<storage, read> vis: array<u32>;
65
110
  @vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
66
- return cam.lightVP * vec4(worldOf(insts[ii], vi), 1.0);
111
+ return cam.lightVP * vec4(worldOf(insts[vis[ii]], vi), 1.0);
67
112
  }
68
113
  `;
69
114
  const MAIN_WGSL = COMMON_WGSL + /* wgsl */ `
70
115
  @group(0) @binding(2) var shadowMap: texture_depth_2d;
71
116
  @group(0) @binding(3) var shadowSamp: sampler_comparison;
117
+ @group(0) @binding(4) var<storage, read> vis: array<u32>;
72
118
 
73
119
  struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32, @location(4) blk: vec3<f32>, @location(5) objN: vec3<f32> };
74
120
 
75
121
  @vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
76
- let inst = insts[ii];
122
+ let inst = insts[vis[ii]];
77
123
  let world = worldOf(inst, vi);
78
124
  var o: VOut;
79
125
  o.clip = cam.camVP * vec4(world, 1.0);
@@ -171,6 +217,21 @@ export class GpuShadowRenderer {
171
217
  overlayBG;
172
218
  overlayScratch = new Float32Array(OVERLAY_CAP * 12);
173
219
  overlayCount = 0;
220
+ // GPU-driven culling: one compute pipeline; per-frustum (camera + light) indirect args + compacted visible lists.
221
+ cullPipe;
222
+ cullBGL;
223
+ cap = 0;
224
+ camArgs;
225
+ camVis;
226
+ camCull;
227
+ camCullBG;
228
+ lightArgs;
229
+ lightVis;
230
+ lightCull;
231
+ lightCullBG;
232
+ statsBuf; // readback: [camVisible, lightVisible] for the IR/tests
233
+ /** Last frame's visible counts (main-pass camera cull, shadow-pass light cull) + total — for IR/telemetry. */
234
+ lastCull = { total: 0, camVisible: 0, lightVisible: 0 };
174
235
  constructor(device, format) {
175
236
  this.device = device;
176
237
  this.camBuf = device.createBuffer({ size: this.cam.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
@@ -181,6 +242,7 @@ export class GpuShadowRenderer {
181
242
  const shadowBGL = device.createBindGroupLayout({ entries: [
182
243
  { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
183
244
  { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
245
+ { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // light-visible list
184
246
  ] });
185
247
  this.shadowPipe = device.createRenderPipeline({
186
248
  layout: device.createPipelineLayout({ bindGroupLayouts: [shadowBGL] }),
@@ -193,6 +255,7 @@ export class GpuShadowRenderer {
193
255
  { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
194
256
  { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
195
257
  { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
258
+ { binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // camera-visible list
196
259
  ] });
197
260
  this.mainPipe = device.createRenderPipeline({
198
261
  layout: device.createPipelineLayout({ bindGroupLayouts: [mainBGL] }),
@@ -224,6 +287,44 @@ export class GpuShadowRenderer {
224
287
  this.overlayBG = device.createBindGroup({ layout: alphaBGL, entries: [
225
288
  { binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.overlayBuf } }
226
289
  ] });
290
+ // GPU-driven cull: compute pipeline shared by the camera + light dispatches (each with its own args/vis/uniform).
291
+ const cullMod = device.createShaderModule({ code: CULL_WGSL });
292
+ this.cullBGL = device.createBindGroupLayout({ entries: [
293
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // insts
294
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // args (atomic ic)
295
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // visible list
296
+ { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // planes + count
297
+ ] });
298
+ this.cullPipe = device.createComputePipeline({
299
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.cullBGL] }),
300
+ compute: { module: cullMod, entryPoint: 'cull' },
301
+ });
302
+ }
303
+ // One frustum's cull resources: indirect args (drawIndirect), a compacted visible-index list sized to the
304
+ // instance capacity, and a small uniform holding the 6 planes + the live instance count.
305
+ makeCullBuf() {
306
+ const d = this.device;
307
+ const args = d.createBuffer({ size: 4 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
308
+ const vis = d.createBuffer({ size: Math.max(1, this.cap) * 4, usage: GPUBufferUsage.STORAGE });
309
+ const cull = d.createBuffer({ size: 6 * 16 + 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
310
+ const bg = d.createBindGroup({ layout: this.cullBGL, entries: [
311
+ { binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: args } },
312
+ { binding: 2, resource: { buffer: vis } }, { binding: 3, resource: { buffer: cull } }
313
+ ] });
314
+ return { args, vis, cull, bg };
315
+ }
316
+ makeCullResources() {
317
+ const cam = this.makeCullBuf();
318
+ this.camArgs = cam.args;
319
+ this.camVis = cam.vis;
320
+ this.camCull = cam.cull;
321
+ this.camCullBG = cam.bg;
322
+ const lit = this.makeCullBuf();
323
+ this.lightArgs = lit.args;
324
+ this.lightVis = lit.vis;
325
+ this.lightCull = lit.cull;
326
+ this.lightCullBG = lit.bg;
327
+ this.statsBuf = this.device.createBuffer({ size: 4 * 4 * 2, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
227
328
  }
228
329
  scratch = new Float32Array(0);
229
330
  fill(scratch, instances, n) {
@@ -244,17 +345,21 @@ export class GpuShadowRenderer {
244
345
  }
245
346
  }
246
347
  makeBindGroups() {
348
+ this.makeCullResources(); // (re)create the cull buffers against the fresh instBuf/cap first — the draw passes bind their visible lists
247
349
  this.shadowBG = this.device.createBindGroup({ layout: this.shadowBGL, entries: [
248
350
  { binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
351
+ { binding: 2, resource: { buffer: this.lightVis } },
249
352
  ] });
250
353
  this.mainBG = this.device.createBindGroup({ layout: this.mainBGL, entries: [
251
354
  { binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
252
355
  { binding: 2, resource: this.shadowView }, { binding: 3, resource: this.shadowSampler },
356
+ { binding: 4, resource: { buffer: this.camVis } },
253
357
  ] });
254
358
  }
255
359
  /** Static scene: sizes the buffer to exactly these instances. */
256
360
  setScene(instances) {
257
361
  this.count = instances.length;
362
+ this.cap = this.count;
258
363
  this.scratch = new Float32Array(this.count * 12);
259
364
  this.fill(this.scratch, instances, this.count);
260
365
  this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
@@ -263,6 +368,7 @@ export class GpuShadowRenderer {
263
368
  }
264
369
  /** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
265
370
  setCapacity(cap) {
371
+ this.cap = cap;
266
372
  this.scratch = new Float32Array(cap * 12);
267
373
  this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
268
374
  this.count = 0;
@@ -282,6 +388,19 @@ export class GpuShadowRenderer {
282
388
  this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * 12);
283
389
  this.overlayCount = n;
284
390
  }
391
+ /** Write a frustum's cull uniform: 6 Gribb-Hartmann planes from `vp` + the live instance count. */
392
+ writeCull(buf, vp) {
393
+ const planes = frustumPlanes(vp);
394
+ const u = new Float32Array(6 * 4 + 4);
395
+ for (let p = 0; p < 6; p++) {
396
+ u[p * 4] = planes[p][0];
397
+ u[p * 4 + 1] = planes[p][1];
398
+ u[p * 4 + 2] = planes[p][2];
399
+ u[p * 4 + 3] = planes[p][3];
400
+ }
401
+ u[24] = this.count;
402
+ this.device.queue.writeBuffer(buf, 0, u);
403
+ }
285
404
  /** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. */
286
405
  frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false) {
287
406
  this.cam.set(camVP, 0);
@@ -295,27 +414,58 @@ export class GpuShadowRenderer {
295
414
  this.cam[38] = sunCol[2];
296
415
  this.cam[39] = SHADOW_SIZE;
297
416
  this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
298
- // Pass 1: occluder depth from the sun.
417
+ // GPU-DRIVEN CULL: reset each frustum's indirect args ([vertexCount=36, instanceCount=0, 0, 0]), then compute-cull
418
+ // — main pass vs the camera frustum, shadow pass vs the light frustum. Each dispatch atomically compacts its
419
+ // survivors into a visible list + counts them in instanceCount, which the matching drawIndirect then consumes.
420
+ const resetArgs = new Uint32Array([36, 0, 0, 0]);
421
+ this.device.queue.writeBuffer(this.camArgs, 0, resetArgs);
422
+ this.device.queue.writeBuffer(this.lightArgs, 0, resetArgs);
423
+ this.writeCull(this.camCull, camVP);
424
+ this.writeCull(this.lightCull, lightVP);
425
+ if (this.count > 0) {
426
+ const cp = enc.beginComputePass();
427
+ cp.setPipeline(this.cullPipe);
428
+ cp.setBindGroup(0, this.camCullBG);
429
+ cp.dispatchWorkgroups(Math.ceil(this.count / CULL_WG));
430
+ cp.setBindGroup(0, this.lightCullBG);
431
+ cp.dispatchWorkgroups(Math.ceil(this.count / CULL_WG));
432
+ cp.end();
433
+ }
434
+ // Pass 1: occluder depth from the sun (light-frustum-culled).
299
435
  const sp = enc.beginRenderPass({ colorAttachments: [], depthStencilAttachment: {
300
436
  view: this.shadowView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store'
301
437
  } });
302
438
  sp.setPipeline(this.shadowPipe);
303
439
  sp.setBindGroup(0, this.shadowBG);
304
- sp.draw(36, this.count);
440
+ sp.drawIndirect(this.lightArgs, 0);
305
441
  sp.end();
306
- // Pass 2: camera, shadow-sampled.
442
+ // Pass 2: camera, shadow-sampled (camera-frustum-culled).
307
443
  const mp = enc.beginRenderPass({
308
444
  colorAttachments: [{ view: colorView, ...(load ? { loadOp: 'load' } : { clearValue: { r: 0.04, g: 0.05, b: 0.07, a: 1 }, loadOp: 'clear' }), storeOp: 'store' }],
309
445
  depthStencilAttachment: { view: sceneDepth, ...(load ? { depthLoadOp: 'load' } : { depthClearValue: 1.0, depthLoadOp: 'clear' }), depthStoreOp: 'store' }
310
446
  });
311
447
  mp.setPipeline(this.mainPipe);
312
448
  mp.setBindGroup(0, this.mainBG);
313
- mp.draw(36, this.count);
449
+ mp.drawIndirect(this.camArgs, 0);
314
450
  if (this.overlayCount > 0) {
315
451
  mp.setPipeline(this.alphaPipe);
316
452
  mp.setBindGroup(0, this.overlayBG);
317
453
  mp.draw(36, this.overlayCount);
318
- } // translucent overlay last
454
+ } // translucent overlay last (uncapped, no cull)
319
455
  mp.end();
456
+ this.lastCull.total = this.count;
457
+ }
458
+ /** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
459
+ * safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
460
+ async readCullStats() {
461
+ const enc = this.device.createCommandEncoder();
462
+ enc.copyBufferToBuffer(this.camArgs, 0, this.statsBuf, 0, 16); // args[1] = instanceCount (the visible count)
463
+ enc.copyBufferToBuffer(this.lightArgs, 0, this.statsBuf, 16, 16);
464
+ this.device.queue.submit([enc.finish()]);
465
+ await this.statsBuf.mapAsync(GPUMapMode.READ);
466
+ const a = new Uint32Array(this.statsBuf.getMappedRange().slice(0));
467
+ this.statsBuf.unmap();
468
+ this.lastCull = { total: this.count, camVisible: a[1], lightVisible: a[5] };
469
+ return this.lastCull;
320
470
  }
321
471
  }
package/dist/mat4.d.ts CHANGED
@@ -12,6 +12,6 @@ export declare function ortho(l: number, r: number, b: number, t: number, n: num
12
12
  export declare function lightViewProjection(dir: Vec3, center: Vec3, extent: number, depth: number): Mat4;
13
13
  export type Plane = readonly [number, number, number, number];
14
14
  /** Extract + normalize the 6 frustum planes from a column-major view-proj (Gribb-Hartmann). */
15
- export declare function frustumPlanes(m: Mat4): Plane[];
15
+ export declare function frustumPlanes(m: ArrayLike<number>): Plane[];
16
16
  /** Sphere vs frustum: false only if the sphere is fully outside any plane. */
17
17
  export declare function sphereVisible(planes: Plane[], x: number, y: number, z: number, radius: number): boolean;
@@ -7,8 +7,10 @@ export declare class SharedClock {
7
7
  private pinned;
8
8
  constructor(tickMs: number, // ms per tick (e.g. 1000/60)
9
9
  now: () => number);
10
- /** From the server `hello`. */
11
- setEpoch(epoch: number): void;
10
+ /** From the server `hello`. Pass the server's `now` to seed a PROVISIONAL offset so `targetTick()` is roughly
11
+ * right immediately — the window between joining and the first ping/pong returning is exactly when lockstep
12
+ * clients stamp their first commands, and an offset of 0 there desyncs them. A real ping sample overrides it. */
13
+ setEpoch(epoch: number, serverNow?: number): void;
12
14
  get hasEpoch(): boolean;
13
15
  get epochMs(): number;
14
16
  /** Feed a ping round-trip: `sentAt` = local ms we sent, `serverNow` = server ms in the pong. Keeps the sample
@@ -17,10 +17,14 @@ export class SharedClock {
17
17
  this.tickMs = tickMs;
18
18
  this.now = now;
19
19
  }
20
- /** From the server `hello`. */
21
- setEpoch(epoch) {
20
+ /** From the server `hello`. Pass the server's `now` to seed a PROVISIONAL offset so `targetTick()` is roughly
21
+ * right immediately — the window between joining and the first ping/pong returning is exactly when lockstep
22
+ * clients stamp their first commands, and an offset of 0 there desyncs them. A real ping sample overrides it. */
23
+ setEpoch(epoch, serverNow) {
22
24
  this.epoch = epoch;
23
25
  this.pinned = true;
26
+ if (serverNow !== undefined && this.bestRtt === Infinity)
27
+ this.offset = serverNow - this.now();
24
28
  }
25
29
  get hasEpoch() {
26
30
  return this.pinned;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-game",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {