multyx-client 0.1.6 → 0.1.8

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.
@@ -19,7 +19,7 @@ class Controller {
19
19
  };
20
20
  document.addEventListener('keydown', e => {
21
21
  if (this.preventDefault)
22
- e.preventDefault;
22
+ e.preventDefault();
23
23
  const key = e.key.toLowerCase();
24
24
  // When holding down key
25
25
  if (this.keys[key] && this.listening.has('keyhold')) {
@@ -40,7 +40,7 @@ class Controller {
40
40
  });
41
41
  document.addEventListener('keyup', e => {
42
42
  if (this.preventDefault)
43
- e.preventDefault;
43
+ e.preventDefault();
44
44
  const key = e.key.toLowerCase();
45
45
  delete this.keys[key];
46
46
  delete this.keys[e.code];
@@ -52,7 +52,7 @@ class Controller {
52
52
  // Mouse input events
53
53
  document.addEventListener('mousedown', e => {
54
54
  if (this.preventDefault)
55
- e.preventDefault;
55
+ e.preventDefault();
56
56
  if (this.mouseGetter) {
57
57
  const mouse = this.mouseGetter();
58
58
  this.mouse.x = mouse.x;
@@ -70,7 +70,7 @@ class Controller {
70
70
  });
71
71
  document.addEventListener('mouseup', e => {
72
72
  if (this.preventDefault)
73
- e.preventDefault;
73
+ e.preventDefault();
74
74
  if (this.mouseGetter) {
75
75
  const mouse = this.mouseGetter();
76
76
  this.mouse.x = mouse.x;
@@ -88,7 +88,7 @@ class Controller {
88
88
  });
89
89
  document.addEventListener('mousemove', e => {
90
90
  if (this.preventDefault)
91
- e.preventDefault;
91
+ e.preventDefault();
92
92
  if (this.mouseGetter) {
93
93
  const mouse = this.mouseGetter();
94
94
  this.mouse.x = mouse.x;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Add, Done } from './utils';
1
+ import { Add, Done, Interpolate } from './utils';
2
2
  import { RawObject } from "./types";
3
3
  import { Controller } from "./controller";
4
4
  import { MultyxClientObject } from "./items";
@@ -27,6 +27,7 @@ export default class Multyx {
27
27
  static Native: symbol;
28
28
  static Custom: symbol;
29
29
  static Any: symbol;
30
+ static Interpolate: typeof Interpolate;
30
31
  constructor(options?: Options, callback?: () => void);
31
32
  /**
32
33
  * Listen for a message from the server
package/dist/index.js CHANGED
@@ -259,4 +259,5 @@ Multyx.Edit = Symbol('edit');
259
259
  Multyx.Native = Symbol('native');
260
260
  Multyx.Custom = Symbol('custom');
261
261
  Multyx.Any = Symbol('any');
262
+ Multyx.Interpolate = utils_1.Interpolate;
262
263
  exports.default = Multyx;
@@ -2,6 +2,7 @@ import Multyx from '../';
2
2
  import { type MultyxClientItem, type MultyxClientObject, MultyxClientValue } from '.';
3
3
  import { Edit, EditWrapper, Unpack } from '../utils';
4
4
  export default class MultyxClientList {
5
+ readonly type = "list";
5
6
  protected list: MultyxClientItem[];
6
7
  private multyx;
7
8
  propertyPath: string[];
@@ -62,4 +63,8 @@ export default class MultyxClientList {
62
63
  toString: () => string;
63
64
  valueOf: () => any[];
64
65
  [Symbol.toPrimitive]: () => any[];
66
+ hydrateFromServer(values: any[]): void;
67
+ private tryApplyServerValue;
68
+ private notifyIndexWaiters;
69
+ private enqueueEditCallbacks;
65
70
  }
@@ -8,6 +8,7 @@ const _1 = require(".");
8
8
  const utils_1 = require("../utils");
9
9
  const router_1 = __importDefault(require("./router"));
10
10
  const message_1 = require("../message");
11
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
12
  class MultyxClientList {
12
13
  addEditCallback(callback) {
13
14
  this.editCallbacks.push(callback);
@@ -73,6 +74,7 @@ class MultyxClientList {
73
74
  }
74
75
  }
75
76
  constructor(multyx, list, propertyPath = [], editable) {
77
+ this.type = 'list';
76
78
  this.editCallbacks = [];
77
79
  this.toString = () => this.value.toString();
78
80
  this.valueOf = () => this.value;
@@ -151,40 +153,39 @@ class MultyxClientList {
151
153
  this.set(parseInt(path[0]), new utils_1.EditWrapper({}));
152
154
  next = this.get(parseInt(path[0]));
153
155
  }
156
+ if (!next || next instanceof _1.MultyxClientValue) {
157
+ return false;
158
+ }
154
159
  return next.set(path.slice(1), value);
155
160
  }
156
161
  set(index, value) {
157
- var _b, _c;
158
162
  if (Array.isArray(index))
159
163
  return this.recursiveSet(index, value);
160
164
  const oldValue = this.get(index);
161
165
  const serverSet = value instanceof utils_1.EditWrapper;
162
166
  const allowed = serverSet || this.editable;
163
- if (serverSet || (0, _1.IsMultyxClientItem)(value))
164
- value = value.value;
165
- if (value === undefined)
167
+ const incoming = (serverSet || (0, _1.IsMultyxClientItem)(value)) ? value.value : value;
168
+ if (incoming === undefined)
166
169
  return this.delete(index, serverSet);
167
- // If value is a MultyxClientValue, set the value
168
- if (this.list[index] instanceof _1.MultyxClientValue && typeof value != 'object') {
169
- return this.list[index].set(serverSet ? new utils_1.EditWrapper(value) : value);
170
+ if (serverSet && this.tryApplyServerValue(index, incoming, oldValue)) {
171
+ return true;
172
+ }
173
+ // If value is a MultyxClientValue, set the value directly
174
+ if (this.list[index] instanceof _1.MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
175
+ const result = this.list[index].set(serverSet ? new utils_1.EditWrapper(incoming) : incoming);
176
+ this.enqueueEditCallbacks(index, oldValue);
177
+ return result;
170
178
  }
171
179
  // Attempting to edit property not editable to client
172
180
  if (!allowed) {
173
181
  if (this.multyx.options.verbose) {
174
- console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + index}' to ${value}`);
182
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + index}' to ${incoming}`);
175
183
  }
176
184
  return false;
177
185
  }
178
- this.list[index] = new ((0, router_1.default)(value))(this.multyx, serverSet ? new utils_1.EditWrapper(value) : value, [...this.propertyPath, index.toString()], this.editable);
179
- const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + index);
180
- if (this.multyx.events.has(propSymbol)) {
181
- this.multyx[utils_1.Done].push(...((_c = (_b = this.multyx.events.get(propSymbol)) === null || _b === void 0 ? void 0 : _b.map(e => () => e(this.list[index]))) !== null && _c !== void 0 ? _c : []));
182
- }
183
- // We have to push into queue, since object may not be fully created
184
- // and there may still be more updates to parse
185
- for (const listener of this.editCallbacks) {
186
- this.multyx[utils_1.Add](() => listener(index, this.get(index), oldValue));
187
- }
186
+ this.list[index] = new ((0, router_1.default)(incoming))(this.multyx, serverSet ? new utils_1.EditWrapper(incoming) : incoming, [...this.propertyPath, index.toString()], this.editable);
187
+ this.notifyIndexWaiters(index);
188
+ this.enqueueEditCallbacks(index, oldValue);
188
189
  return true;
189
190
  }
190
191
  delete(index, native = false) {
@@ -403,6 +404,51 @@ class MultyxClientList {
403
404
  values[i] = this.get(i);
404
405
  return values[Symbol.iterator]();
405
406
  }
407
+ hydrateFromServer(values) {
408
+ if (!Array.isArray(values))
409
+ return;
410
+ for (let i = 0; i < values.length; i++) {
411
+ this.set(i, new utils_1.EditWrapper(values[i]));
412
+ }
413
+ for (let i = values.length; i < this.length; i++) {
414
+ this.delete(i, true);
415
+ }
416
+ this.length = values.length;
417
+ }
418
+ tryApplyServerValue(index, incoming, oldValue) {
419
+ const current = this.list[index];
420
+ if (!current)
421
+ return false;
422
+ if (current instanceof _1.MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
423
+ current.set(new utils_1.EditWrapper(incoming));
424
+ this.enqueueEditCallbacks(index, oldValue);
425
+ return true;
426
+ }
427
+ const canHydrate = typeof (current === null || current === void 0 ? void 0 : current.hydrateFromServer) === 'function';
428
+ if (Array.isArray(incoming) && canHydrate && current.type === 'list') {
429
+ current.hydrateFromServer(incoming);
430
+ this.enqueueEditCallbacks(index, oldValue);
431
+ return true;
432
+ }
433
+ if (isPlainObject(incoming) && canHydrate && current.type === 'object') {
434
+ current.hydrateFromServer(incoming);
435
+ this.enqueueEditCallbacks(index, oldValue);
436
+ return true;
437
+ }
438
+ return false;
439
+ }
440
+ notifyIndexWaiters(index) {
441
+ var _b, _c;
442
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + index);
443
+ if (this.multyx.events.has(propSymbol)) {
444
+ this.multyx[utils_1.Done].push(...((_c = (_b = this.multyx.events.get(propSymbol)) === null || _b === void 0 ? void 0 : _b.map(e => () => e(this.list[index]))) !== null && _c !== void 0 ? _c : []));
445
+ }
446
+ }
447
+ enqueueEditCallbacks(index, oldValue) {
448
+ for (const listener of this.editCallbacks) {
449
+ this.multyx[utils_1.Add](() => listener(index, this.get(index), oldValue));
450
+ }
451
+ }
406
452
  }
407
453
  _a = Symbol.toPrimitive;
408
454
  exports.default = MultyxClientList;
@@ -4,6 +4,7 @@ import type Multyx from '../index';
4
4
  import { type MultyxClientList, type MultyxClientItem } from ".";
5
5
  import MultyxClientValue from "./value";
6
6
  export default class MultyxClientObject {
7
+ readonly type = "object";
7
8
  protected object: RawObject<MultyxClientItem>;
8
9
  private multyx;
9
10
  propertyPath: string[];
@@ -33,4 +34,7 @@ export default class MultyxClientObject {
33
34
  * @param constraints Packed constraints object mirroring MultyxClientObject shape
34
35
  */
35
36
  [Unpack](constraints: RawObject): void;
37
+ hydrateFromServer(value: RawObject): void;
38
+ private applyServerValue;
39
+ private notifyPropertyWaiters;
36
40
  }
@@ -8,6 +8,7 @@ const utils_1 = require("../utils");
8
8
  const _1 = require(".");
9
9
  const router_1 = __importDefault(require("./router"));
10
10
  const value_1 = __importDefault(require("./value"));
11
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
12
  class MultyxClientObject {
12
13
  get value() {
13
14
  const parsed = {};
@@ -33,6 +34,7 @@ class MultyxClientObject {
33
34
  (_a = this.get(updatePath[0])) === null || _a === void 0 ? void 0 : _a[utils_1.Edit](updatePath.slice(1), value);
34
35
  }
35
36
  constructor(multyx, object, propertyPath = [], editable) {
37
+ this.type = 'object';
36
38
  this.editCallbacks = [];
37
39
  this.object = {};
38
40
  this.propertyPath = propertyPath;
@@ -109,32 +111,30 @@ class MultyxClientObject {
109
111
  return next.set(path.slice(1), value);
110
112
  }
111
113
  set(property, value) {
112
- var _a, _b;
113
114
  if (Array.isArray(property))
114
115
  return this.recursiveSet(property, value);
115
116
  const serverSet = value instanceof utils_1.EditWrapper;
116
117
  const allowed = serverSet || this.editable;
117
- if (serverSet || (0, _1.IsMultyxClientItem)(value))
118
- value = value.value;
119
- if (value === undefined)
118
+ const incoming = (serverSet || (0, _1.IsMultyxClientItem)(value)) ? value.value : value;
119
+ if (incoming === undefined)
120
120
  return this.delete(property, serverSet);
121
+ if (serverSet && this.applyServerValue(property, incoming)) {
122
+ return true;
123
+ }
121
124
  // Only create new MultyxClientItem when needed
122
- if (this.object[property] instanceof value_1.default && typeof value != 'object') {
123
- return this.object[property].set(serverSet ? new utils_1.EditWrapper(value) : value);
125
+ if (this.object[property] instanceof value_1.default && (typeof incoming !== 'object' || incoming === null)) {
126
+ return this.object[property].set(serverSet ? new utils_1.EditWrapper(incoming) : incoming);
124
127
  }
125
128
  // Attempting to edit property not editable to client
126
129
  if (!allowed) {
127
130
  if (this.multyx.options.verbose) {
128
- console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${value}`);
131
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${incoming}`);
129
132
  }
130
133
  return false;
131
134
  }
132
135
  // Creating a new value
133
- this.object[property] = new ((0, router_1.default)(value))(this.multyx, serverSet ? new utils_1.EditWrapper(value) : value, [...this.propertyPath, property], this.editable);
134
- const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
135
- if (this.multyx.events.has(propSymbol)) {
136
- this.multyx[utils_1.Done].push(...((_b = (_a = this.multyx.events.get(propSymbol)) === null || _a === void 0 ? void 0 : _a.map(e => () => e(this.object[property]))) !== null && _b !== void 0 ? _b : []));
137
- }
136
+ this.object[property] = new ((0, router_1.default)(incoming))(this.multyx, serverSet ? new utils_1.EditWrapper(incoming) : incoming, [...this.propertyPath, property], this.editable);
137
+ this.notifyPropertyWaiters(property);
138
138
  return true;
139
139
  }
140
140
  delete(property, native = false) {
@@ -189,5 +189,43 @@ class MultyxClientObject {
189
189
  (_a = this.object[prop]) === null || _a === void 0 ? void 0 : _a[utils_1.Unpack](constraints[prop]);
190
190
  }
191
191
  }
192
+ hydrateFromServer(value) {
193
+ if (!isPlainObject(value))
194
+ return;
195
+ const remaining = new Set(Object.keys(this.object));
196
+ for (const [key, entry] of Object.entries(value)) {
197
+ remaining.delete(key);
198
+ this.set(key, new utils_1.EditWrapper(entry));
199
+ }
200
+ for (const key of remaining) {
201
+ this.delete(key, true);
202
+ }
203
+ }
204
+ applyServerValue(property, incoming) {
205
+ const current = this.object[property];
206
+ if (!current)
207
+ return false;
208
+ if (current instanceof value_1.default && (typeof incoming !== 'object' || incoming === null)) {
209
+ current.set(new utils_1.EditWrapper(incoming));
210
+ return true;
211
+ }
212
+ const canHydrate = typeof (current === null || current === void 0 ? void 0 : current.hydrateFromServer) === 'function';
213
+ if (Array.isArray(incoming) && canHydrate && current.type === 'list') {
214
+ current.hydrateFromServer(incoming);
215
+ return true;
216
+ }
217
+ if (isPlainObject(incoming) && canHydrate && current.type === 'object') {
218
+ current.hydrateFromServer(incoming);
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+ notifyPropertyWaiters(property) {
224
+ var _a, _b;
225
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
226
+ if (this.multyx.events.has(propSymbol)) {
227
+ this.multyx[utils_1.Done].push(...((_b = (_a = this.multyx.events.get(propSymbol)) === null || _a === void 0 ? void 0 : _a.map(e => () => e(this.object[property]))) !== null && _b !== void 0 ? _b : []));
228
+ }
229
+ }
192
230
  }
193
231
  exports.default = MultyxClientObject;
@@ -12,6 +12,10 @@ export default class MultyxClientValue {
12
12
  private readModifiers;
13
13
  private editCallbacks;
14
14
  get value(): Value;
15
+ private interpolationModifier?;
16
+ private latestSample?;
17
+ private previousSample?;
18
+ private interpolationFrameMs;
15
19
  set value(v: Value);
16
20
  addReadModifier(modifier: (value: Value) => Value): void;
17
21
  addEditCallback(callback: (value: Value, previousValue: Value) => void): void;
@@ -27,4 +31,10 @@ export default class MultyxClientValue {
27
31
  toString: () => string;
28
32
  valueOf: () => Value;
29
33
  [Symbol.toPrimitive]: () => Value;
34
+ Lerp(maxFrameDuration?: number): this;
35
+ PredictiveLerp(maxFrameDuration?: number): this;
36
+ private applyInterpolation;
37
+ private attachInterpolationModifier;
38
+ private captureSample;
39
+ private interpolateValue;
30
40
  }
@@ -3,12 +3,14 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const message_1 = require("../message");
5
5
  const utils_1 = require("../utils");
6
+ const DEFAULT_INTERPOLATION_FRAME_MS = 250;
6
7
  class MultyxClientValue {
7
8
  get value() {
8
9
  return this.readModifiers.reduce((value, modifier) => modifier(value), this._value);
9
10
  }
10
11
  set value(v) {
11
12
  this._value = v;
13
+ this.captureSample(v);
12
14
  }
13
15
  addReadModifier(modifier) {
14
16
  this.readModifiers.push(modifier);
@@ -25,6 +27,7 @@ class MultyxClientValue {
25
27
  var _b, _c;
26
28
  this.readModifiers = [];
27
29
  this.editCallbacks = [];
30
+ this.interpolationFrameMs = DEFAULT_INTERPOLATION_FRAME_MS;
28
31
  /* Native methods to allow MultyxValue to be treated as primitive */
29
32
  this.toString = () => this.value.toString();
30
33
  this.valueOf = () => this.value;
@@ -64,7 +67,7 @@ class MultyxClientValue {
64
67
  return false;
65
68
  }
66
69
  }
67
- if (this.value === nv) {
70
+ if (this._value === nv) {
68
71
  this.value = nv;
69
72
  return true;
70
73
  }
@@ -95,6 +98,57 @@ class MultyxClientValue {
95
98
  this.constraints[cname] = constraint;
96
99
  }
97
100
  }
101
+ Lerp(maxFrameDuration = DEFAULT_INTERPOLATION_FRAME_MS) {
102
+ return this.applyInterpolation('lerp', maxFrameDuration);
103
+ }
104
+ PredictiveLerp(maxFrameDuration = DEFAULT_INTERPOLATION_FRAME_MS) {
105
+ return this.applyInterpolation('predictive', maxFrameDuration);
106
+ }
107
+ applyInterpolation(mode, maxFrameDuration) {
108
+ if (typeof this._value !== 'number' || Number.isNaN(this._value)) {
109
+ throw new Error(`MultyxClientValue.${mode === 'lerp' ? 'Lerp' : 'PredictiveLerp'} can only be applied to numeric values`);
110
+ }
111
+ this.interpolationFrameMs = Math.max(1, maxFrameDuration);
112
+ this.attachInterpolationModifier(mode);
113
+ return this;
114
+ }
115
+ attachInterpolationModifier(mode) {
116
+ if (this.interpolationModifier) {
117
+ this.readModifiers = this.readModifiers.filter(fn => fn !== this.interpolationModifier);
118
+ }
119
+ this.interpolationModifier = (value) => this.interpolateValue(value, mode);
120
+ this.readModifiers.push(this.interpolationModifier);
121
+ }
122
+ captureSample(value) {
123
+ if (typeof value !== 'number' || Number.isNaN(value)) {
124
+ this.latestSample = undefined;
125
+ this.previousSample = undefined;
126
+ return;
127
+ }
128
+ const now = Date.now();
129
+ if (!this.latestSample) {
130
+ this.latestSample = { value, time: now };
131
+ return;
132
+ }
133
+ this.previousSample = Object.assign({}, this.latestSample);
134
+ this.latestSample = { value, time: now };
135
+ }
136
+ interpolateValue(baseValue, mode) {
137
+ if (typeof baseValue !== 'number' || !this.latestSample || !this.previousSample) {
138
+ return baseValue;
139
+ }
140
+ const durationRaw = this.latestSample.time - this.previousSample.time;
141
+ if (durationRaw <= 0)
142
+ return baseValue;
143
+ const duration = Math.max(1, Math.min(durationRaw, this.interpolationFrameMs));
144
+ const elapsed = Math.max(0, Math.min(Date.now() - this.latestSample.time, duration));
145
+ const ratio = duration === 0 ? 1 : elapsed / duration;
146
+ const delta = this.latestSample.value - this.previousSample.value;
147
+ if (mode === 'predictive') {
148
+ return this.latestSample.value + delta * ratio;
149
+ }
150
+ return this.previousSample.value + delta * ratio;
151
+ }
98
152
  }
99
153
  _a = Symbol.toPrimitive;
100
154
  exports.default = MultyxClientValue;
package/dist/message.d.ts CHANGED
@@ -9,8 +9,6 @@ export declare function UncompressUpdate(str: string): {
9
9
  name?: undefined;
10
10
  response?: undefined;
11
11
  uuid?: undefined;
12
- publicData?: undefined;
13
- clientUUID?: undefined;
14
12
  client?: undefined;
15
13
  tps?: undefined;
16
14
  constraintTable?: undefined;
@@ -27,8 +25,6 @@ export declare function UncompressUpdate(str: string): {
27
25
  name?: undefined;
28
26
  response?: undefined;
29
27
  uuid?: undefined;
30
- publicData?: undefined;
31
- clientUUID?: undefined;
32
28
  client?: undefined;
33
29
  tps?: undefined;
34
30
  constraintTable?: undefined;
@@ -45,8 +41,6 @@ export declare function UncompressUpdate(str: string): {
45
41
  property?: undefined;
46
42
  data?: undefined;
47
43
  uuid?: undefined;
48
- publicData?: undefined;
49
- clientUUID?: undefined;
50
44
  client?: undefined;
51
45
  tps?: undefined;
52
46
  constraintTable?: undefined;
@@ -56,15 +50,13 @@ export declare function UncompressUpdate(str: string): {
56
50
  } | {
57
51
  instruction: string;
58
52
  uuid: string;
59
- publicData: any;
53
+ data: any;
60
54
  team?: undefined;
61
55
  path?: undefined;
62
56
  value?: undefined;
63
57
  property?: undefined;
64
- data?: undefined;
65
58
  name?: undefined;
66
59
  response?: undefined;
67
- clientUUID?: undefined;
68
60
  client?: undefined;
69
61
  tps?: undefined;
70
62
  constraintTable?: undefined;
@@ -73,7 +65,7 @@ export declare function UncompressUpdate(str: string): {
73
65
  space?: undefined;
74
66
  } | {
75
67
  instruction: string;
76
- clientUUID: string;
68
+ client: string;
77
69
  team?: undefined;
78
70
  path?: undefined;
79
71
  value?: undefined;
@@ -82,8 +74,6 @@ export declare function UncompressUpdate(str: string): {
82
74
  name?: undefined;
83
75
  response?: undefined;
84
76
  uuid?: undefined;
85
- publicData?: undefined;
86
- client?: undefined;
87
77
  tps?: undefined;
88
78
  constraintTable?: undefined;
89
79
  clients?: undefined;
@@ -105,8 +95,6 @@ export declare function UncompressUpdate(str: string): {
105
95
  name?: undefined;
106
96
  response?: undefined;
107
97
  uuid?: undefined;
108
- publicData?: undefined;
109
- clientUUID?: undefined;
110
98
  } | undefined;
111
99
  export declare function CompressUpdate(update: Update): string;
112
100
  export declare class Message {
package/dist/message.js CHANGED
@@ -23,9 +23,9 @@ function UncompressUpdate(str) {
23
23
  if (instruction == '5')
24
24
  return { instruction: 'resp', name: specifier, response: data[0] };
25
25
  if (instruction == '6')
26
- return { instruction: 'conn', uuid: specifier, publicData: data[0] };
26
+ return { instruction: 'conn', uuid: specifier, data: data[0] };
27
27
  if (instruction == '7')
28
- return { instruction: 'dcon', clientUUID: specifier };
28
+ return { instruction: 'dcon', client: specifier };
29
29
  if (instruction == '8')
30
30
  return {
31
31
  instruction: 'init',
@@ -54,7 +54,7 @@ function CompressUpdate(update) {
54
54
  code = 2;
55
55
  pieces = [update.name, JSON.stringify(update.response)];
56
56
  }
57
- if (!pieces || !code)
57
+ if (!pieces || code === undefined)
58
58
  return '';
59
59
  let compressed = code.toString();
60
60
  for (let i = 0; i < pieces.length; i++) {
package/dist/utils.js CHANGED
@@ -29,33 +29,62 @@ exports.EditWrapper = EditWrapper;
29
29
  * ```
30
30
  */
31
31
  function Interpolate(object, property, interpolationCurve) {
32
+ if (!Array.isArray(interpolationCurve) || interpolationCurve.length === 0) {
33
+ throw new Error('Interpolation curve must contain at least one slice');
34
+ }
35
+ const curve = [...interpolationCurve].sort((a, b) => a.time - b.time);
36
+ const curveMaxTime = curve[curve.length - 1].time;
37
+ const usesNormalizedCurve = curveMaxTime <= 1;
38
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
32
39
  let start = { value: object[property], time: Date.now() };
33
40
  let end = { value: object[property], time: Date.now() };
34
41
  Object.defineProperty(object, property, {
42
+ configurable: true,
43
+ enumerable: true,
35
44
  get: () => {
36
- const time = end.time - start.time;
37
- let lower = interpolationCurve[0];
38
- let upper = interpolationCurve[0];
39
- for (const slice of interpolationCurve) {
40
- if (time > slice.time && slice.time > lower.time)
45
+ if (start.time === end.time)
46
+ return end.value;
47
+ const now = Date.now();
48
+ const duration = Math.max(end.time - start.time, 1);
49
+ const elapsed = Math.max(0, now - end.time);
50
+ const targetTime = usesNormalizedCurve
51
+ ? clamp(elapsed / duration, 0, curveMaxTime)
52
+ : clamp(elapsed, 0, curveMaxTime);
53
+ let lower = curve[0];
54
+ let upper = curve[curve.length - 1];
55
+ for (const slice of curve) {
56
+ if (slice.time <= targetTime) {
41
57
  lower = slice;
42
- if (time < slice.time && slice.time < upper.time)
43
- upper = slice;
58
+ continue;
59
+ }
60
+ upper = slice;
61
+ break;
62
+ }
63
+ let ratio;
64
+ if (upper.time === lower.time) {
65
+ ratio = lower.progress;
66
+ }
67
+ else {
68
+ const sliceTime = (targetTime - lower.time) / (upper.time - lower.time);
69
+ ratio = lower.progress + sliceTime * (upper.progress - lower.progress);
44
70
  }
45
- const sliceTime = (time - lower.time) / (upper.time - lower.time);
46
- const ratio = lower.progress + sliceTime * (upper.progress - lower.progress);
47
71
  if (Number.isNaN(ratio))
48
72
  return start.value;
49
- return end.value * ratio + start.value * (1 - ratio);
73
+ if (typeof start.value === 'number' && typeof end.value === 'number') {
74
+ return end.value * ratio + start.value * (1 - ratio);
75
+ }
76
+ return ratio >= 1 ? end.value : start.value;
50
77
  },
51
78
  set: (value) => {
79
+ const now = Date.now();
52
80
  // Don't lerp between edit requests sent in same frame
53
- if (Date.now() - end.time < 10) {
81
+ if (now - end.time < 10) {
54
82
  end.value = value;
83
+ end.time = now;
55
84
  return true;
56
85
  }
57
86
  start = Object.assign({}, end);
58
- end = { value, time: Date.now() };
87
+ end = { value, time: now };
59
88
  return true;
60
89
  }
61
90
  });
package/multyx.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Multyx=e():t.Multyx=e()}(self,(()=>(()=>{"use strict";var t={376:(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.Controller=void 0;const i=s(210);e.Controller=class{constructor(t){this.listening=new Set,this.ws=t,this.preventDefault=!1,this.keys={},this.mouse={x:NaN,y:NaN,down:!1,centerX:0,centerY:0,scaleX:1,scaleY:1},document.addEventListener("keydown",(t=>{this.preventDefault&&t.preventDefault;const e=t.key.toLowerCase();this.keys[e]&&this.listening.has("keyhold")&&this.relayInput("keyhold",{code:e}),this.keys[t.code]&&this.listening.has("keyhold")&&this.relayInput("keyhold",{code:t.code}),this.listening.has(e)&&!this.keys[e]&&this.relayInput("keydown",{code:t.key}),this.listening.has(t.code)&&!this.keys[t.code]&&this.relayInput("keydown",{code:t.code}),this.keys[e]=!0,this.keys[t.code]=!0})),document.addEventListener("keyup",(t=>{this.preventDefault&&t.preventDefault;const e=t.key.toLowerCase();delete this.keys[e],delete this.keys[t.code],this.listening.has(e)&&this.relayInput("keyup",{code:e}),this.listening.has(t.code)&&this.relayInput("keyup",{code:t.code})})),document.addEventListener("mousedown",(t=>{if(this.preventDefault&&t.preventDefault,this.mouseGetter){const t=this.mouseGetter();this.mouse.x=t.x,this.mouse.y=t.y}else this.mouse.x=(t.clientX-this.mouse.centerX)/this.mouse.scaleX,this.mouse.y=(t.clientY-this.mouse.centerY)/this.mouse.scaleY;this.mouse.down=!0,this.listening.has("mousedown")&&this.relayInput("mousedown",{x:this.mouse.x,y:this.mouse.y})})),document.addEventListener("mouseup",(t=>{if(this.preventDefault&&t.preventDefault,this.mouseGetter){const t=this.mouseGetter();this.mouse.x=t.x,this.mouse.y=t.y}else this.mouse.x=(t.clientX-this.mouse.centerX)/this.mouse.scaleX,this.mouse.y=(t.clientY-this.mouse.centerY)/this.mouse.scaleY;this.mouse.down=!1,this.listening.has("mouseup")&&this.relayInput("mouseup",{x:this.mouse.x,y:this.mouse.y})})),document.addEventListener("mousemove",(t=>{if(this.preventDefault&&t.preventDefault,this.mouseGetter){const t=this.mouseGetter();this.mouse.x=t.x,this.mouse.y=t.y}else this.mouse.x=(t.clientX-this.mouse.centerX)/this.mouse.scaleX,this.mouse.y=(t.clientY-this.mouse.centerY)/this.mouse.scaleY;this.listening.has("mousemove")&&this.relayInput("mousemove",{x:this.mouse.x,y:this.mouse.y})}))}mapCanvasPosition(t,e){const s="top"in e,i="bottom"in e,n="left"in e,o="right"in e,r=e.anchor,l=t.getBoundingClientRect(),h=(t,...e)=>{const s=t?"Cannot include value for ":"Must include value for ",i=1==e.length?e[0]:e.slice(0,-1).join(", ")+(t?" and ":" or ")+e.slice(-1)[0],n=r?" if anchoring at "+r:" if not anchoring";console.error(s+i+n)},a=l.width/l.height,u=l.height/l.width;if((Number.isNaN(a)||Number.isNaN(u))&&console.error("Canvas element bounding box is flat, canvas must be present on the screen"),r){if("center"==r){if(s&&i&&e.top!==-e.bottom||n&&o&&e.left!==-e.right)return h(!0,"top","bottom","left","right");s?(e.left=n?e.left:o?-e.right:-Math.abs(a*e.top),e.right=n?-e.left:o?e.right:Math.abs(a*e.top),e.bottom=-e.top):i?(e.left=n?e.left:o?-e.right:-Math.abs(a*e.bottom),e.right=n?-e.left:o?e.right:Math.abs(a*e.bottom),e.top=-e.bottom):n?(e.top=s?e.top:i?-e.bottom:-Math.abs(u*e.left),e.bottom=s?-e.top:i?e.bottom:Math.abs(u*e.left),e.right=-e.left):o&&(e.top=s?e.top:i?-e.bottom:-Math.abs(u*e.right),e.bottom=s?-e.top:i?e.bottom:Math.abs(u*e.right),e.left=-e.right)}else if("bottom"==r){if(!n&&!o&&!s)return h(!1,"left","right","top");if(e.bottom)return h(!0,"bottom");e.bottom=0,n?(e.top=Math.abs(u*e.left*2),e.right=-e.left):o?(e.top=Math.abs(u*e.right*2),e.left=-e.right):(e.left=-Math.abs(a*e.top/2),e.right=-e.left)}else if("top"==r){if(!n&&!o&&!i)return h(!1,"left","right","bottom");if(e.top)return h(!0,"top");e.top=0,n?(e.bottom=Math.abs(u*e.left*2),e.right=-e.left):o?(e.bottom=Math.abs(u*e.right*2),e.left=-e.right):(e.left=-Math.abs(a*e.bottom/2),e.right=-e.left)}else if("left"==r){if(!s&&!i&&!o)return h(!1,"top","bottom","right");if(n)return h(!0,"left");e.left=0,s?(e.right=-Math.abs(a*e.top*2),e.bottom=-e.top):i?(e.right=Math.abs(a*e.bottom*2),e.top=-e.bottom):(e.top=-Math.abs(u*e.right/2),e.bottom=-e.top)}else if("right"==r){if(!s&&!i&&!n)return h(!1,"top","bottom","left");if(o)return h(!0,"right");e.right=0,s?(e.left=-Math.abs(a*e.top*2),e.bottom=-e.top):i?(e.left=Math.abs(a*e.bottom*2),e.top=-e.bottom):(e.top=-Math.abs(u*e.right/2),e.bottom=-e.top)}else if("topleft"==r){if(!o&&!i)return h(!1,"right","bottom");if(n||s)return h(!0,"left","top");e.left=e.top=0,o?e.bottom=Math.abs(u*e.right):e.right=Math.abs(a*e.bottom)}else if("topright"==r){if(!n&&!i)return h(!1,"left","bottom");if(o||s)return h(!0,"right","top");e.right=e.top=0,n?e.bottom=Math.abs(u*e.left):e.left=Math.abs(a*e.bottom)}else if("bottomleft"==r){if(!o&&!s)return h(!1,"right","top");if(i||n)return h(!0,"bottom","left");e.left=e.bottom=0,o?e.top=Math.abs(u*e.right):e.right=Math.abs(a*e.top)}else if("bottomright"==r){if(!s&&!n)return h(!1,"top","left");if(o||i)return h(!0,"bottom","right");e.right=e.bottom=0,n?e.top=Math.abs(u*e.left):e.left=Math.abs(a*e.top)}}else{if(!s&&!i)return h(!1,"top","bottom");if(i?s||(e.top=e.bottom-t.height):e.bottom=e.top+t.height,!n&&!o)return h(!1,"left","right");o?n||(e.left=e.right-t.width):e.right=e.left+t.width}const c=t.getContext("2d");null==c||c.setTransform(1,0,0,1,0,0),t.width=Math.floor(Math.abs(e.right-e.left)),t.height=Math.floor(Math.abs(e.bottom-e.top)),e.right<e.left&&(null==c||c.scale(-1,1)),e.top>e.bottom&&(null==c||c.scale(1,-1)),null==c||c.translate(-e.left,-e.top)}mapMousePosition(t,e,s=document.body,i=1,n=i){const o=window.innerWidth/(s instanceof HTMLCanvasElement?s.width:s.clientWidth),r=window.innerHeight/(s instanceof HTMLCanvasElement?s.height:s.clientHeight),l=s.getBoundingClientRect();this.mouse.centerX=l.left+t*o,this.mouse.centerY=l.top+e*r,this.mouse.scaleX=i*o,this.mouse.scaleY=n*r}mapMouseToCanvas(t){const e=t.getContext("2d"),s=null==e?void 0:e.getTransform(),i=t.getBoundingClientRect(),n=i.width/t.width,o=i.height/t.height;this.mouse.centerX=i.left+(null==s?void 0:s.e)*n,this.mouse.centerY=i.top+(null==s?void 0:s.f)*o,this.mouse.scaleX=n*(null==s?void 0:s.a),this.mouse.scaleY=o*(null==s?void 0:s.d)}setMouseAs(t){this.mouseGetter=t}relayInput(t,e){if(1!==this.ws.readyState)throw new Error("Websocket connection is "+(2==this.ws.readyState?"closing":"closed"));this.ws.send(i.Message.Native(Object.assign({instruction:"input",input:t},e?{data:e}:{})))}}},34:function(t,e,s){var i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.MultyxClientValue=e.MultyxClientObject=e.MultyxClientList=void 0,e.IsMultyxClientItem=function(t){return t instanceof n.default||t instanceof o.default||t instanceof r.default};const n=i(s(70));e.MultyxClientList=n.default;const o=i(s(614));e.MultyxClientObject=o.default;const r=i(s(501));e.MultyxClientValue=r.default},70:function(t,e,s){var i,n=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});const o=s(34),r=s(787),l=n(s(735)),h=s(210);class a{addEditCallback(t){this.editCallbacks.push(t)}get value(){var t;const e=[];for(let s=0;s<this.length;s++)e[s]=null===(t=this.get(s))||void 0===t?void 0:t.value;return e}get length(){return this.list.length}set length(t){this.list.length=t}handleShiftOperation(t,e){const s=t>=0?e>=0?"right":"left":0==e?"reverse":e<0?"length":"unknown";switch(s){case"reverse":for(let t=0;t<Math.floor(this.length/2);t++){const e=this.list[t];this.list[t]=this.list[this.length-1-t],this.list[this.length-1-t]=e}break;case"left":for(let s=t;s<this.length;s++)s+e<0||(this.list[s+e]=this.list[s]);break;case"right":for(let s=this.length-1;s>=t;s--)this.list[s+e]=this.list[s];break;case"length":this.length+=e;break;default:this.multyx.options.verbose&&console.error("Unknown shift operation: "+s)}}constructor(t,e,s=[],n){this.editCallbacks=[],this.toString=()=>this.value.toString(),this.valueOf=()=>this.value,this[i]=()=>this.value,this.list=[],this.propertyPath=s,this.multyx=t,this.editable=n;const o=e instanceof r.EditWrapper;e instanceof a&&(e=e.value),e instanceof r.EditWrapper&&(e=e.value);for(let t=0;t<e.length;t++)this.set(t,o?new r.EditWrapper(e[t]):e[t]);return new Proxy(this,{has:(t,e)=>"number"==typeof e?t.has(e):e in t,get:(t,e)=>e in t?t[e]:(isNaN(parseInt(e))||(e=parseInt(e)),t.get(e)),set:(t,e,s)=>e in t?(t[e]=s,!0):!!t.set(e,s),deleteProperty:(t,e)=>"number"==typeof e&&t.delete(e)})}has(t){return t>=0&&t<this.length}get(t){if("number"==typeof t)return this.list[t];if(0==t.length)return this;if(1==t.length)return this.list[parseInt(t[0])];const e=this.list[parseInt(t[0])];return!e||e instanceof o.MultyxClientValue?void 0:e.get(t.slice(1))}recursiveSet(t,e){if(0==t.length)return this.multyx.options.verbose&&console.error(`Attempting to edit MultyxClientList with no path. Setting '${this.propertyPath.join(".")}' to ${e}`),!1;if("shift"==t[0]&&e instanceof r.EditWrapper)return this.handleShiftOperation(parseInt(t[1]),e.value),!0;if(1==t.length)return this.set(parseInt(t[0]),e);let s=this.get(parseInt(t[0]));return(s instanceof o.MultyxClientValue||null==s)&&(this.set(parseInt(t[0]),new r.EditWrapper({})),s=this.get(parseInt(t[0]))),s.set(t.slice(1),e)}set(t,e){var s,i;if(Array.isArray(t))return this.recursiveSet(t,e);const n=this.get(t),h=e instanceof r.EditWrapper,a=h||this.editable;if((h||(0,o.IsMultyxClientItem)(e))&&(e=e.value),void 0===e)return this.delete(t,h);if(this.list[t]instanceof o.MultyxClientValue&&"object"!=typeof e)return this.list[t].set(h?new r.EditWrapper(e):e);if(!a)return this.multyx.options.verbose&&console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join(".")+"."+t}' to ${e}`),!1;this.list[t]=new((0,l.default)(e))(this.multyx,h?new r.EditWrapper(e):e,[...this.propertyPath,t.toString()],this.editable);const u=Symbol.for("_"+this.propertyPath.join(".")+"."+t);this.multyx.events.has(u)&&this.multyx[r.Done].push(...null!==(i=null===(s=this.multyx.events.get(u))||void 0===s?void 0:s.map((e=>()=>e(this.list[t]))))&&void 0!==i?i:[]);for(const e of this.editCallbacks)this.multyx[r.Add]((()=>e(t,this.get(t),n)));return!0}delete(t,e=!1){const s=this.get(t);if("string"==typeof t&&(t=parseInt(t)),!this.editable&&!e)return this.multyx.options.verbose&&console.error(`Attempting to delete property that is not editable. Deleting '${this.propertyPath.join(".")+"."+t}'`),!1;delete this.list[t];for(const e of this.editCallbacks)this.multyx[r.Add]((()=>e(t,void 0,s)));return e||this.multyx.ws.send(h.Message.Native({instruction:"edit",path:[...this.propertyPath,t.toString()],value:void 0})),!0}await(t){if(this.has(t))return Promise.resolve(this.get(t));const e=Symbol.for("_"+this.propertyPath.join(".")+"."+t);return new Promise((t=>this.multyx.on(e,t)))}push(...t){for(const e of t)this.set(this.length,e);return this.length}pop(){if(0===this.length)return;const t=this.get(this.length);return this.delete(this.length),t}unshift(...t){for(let e=this.length-1;e>=0;e--)e>=t.length?this.set(e,this.get(e-t.length)):this.set(e,t[e]);return this.length}shift(){if(0==this.length)return;this.length--;const t=this.get(0);for(let t=0;t<this.length;t++)this.set(t,this.get(t+1));return t}slice(t,e){return this.list.slice(t,e)}splice(t,e,...s){return this.list.splice(t,null!=e?e:0,...s)}setSplice(t,e,...s){void 0===e&&(e=this.length-t);let i=s.length-e;if(i>0)for(let s=this.length-1;s>=t+e;s--)this.set(s+i,this.get(s));else if(i<0){for(let s=t+e;s<this.length;s++)this.set(s+i,this.get(s));const s=this.length;for(let t=s+i;t<s;t++)this.set(t,void 0)}for(let e=t;e<s.length;e++)this.set(e,s[e])}filter(t){return this.list.filter(((e,s)=>t(e,s,this)))}setFilter(t){const e=[];for(let s=0;s<this.length;s++)e.push(t(this.get(s),s,this));let s=0;for(let t=0;t<e.length;t++)e[t]&&s&&this.set(t-s,this.get(t)),e[t]||s--}map(t){const e=[];for(let s=0;s<this.length;s++)e.push(t(this.get(s),s,this));return e}flat(){return this.list.flat()}setFlat(){for(let t=0;t<this.length;t++){const e=this.get(t);if(e instanceof a)for(let s=0;s<e.length;s++)t++,this.set(t,e[s])}}reduce(t,e){for(let s=0;s<this.length;s++)e=t(e,this.get(s),s,this);return e}reduceRight(t,e){for(let s=this.length-1;s>=0;s--)e=t(e,this.get(s),s,this);return e}reverse(){let t=this.length-1;for(let e=0;e<t;e++){const s=this.get(e),i=this.get(t);this.set(e,i),this.set(t,s)}return this}forEach(t){for(let e=0;e<this.length;e++)t(this.get(e),e,this)}every(t){for(let e=0;e<this.length;e++)if(!t(this.get(e),e,this))return!1;return!0}some(t){for(let e=0;e<this.length;e++)if(t(this.get(e),e,this))return!0;return!1}find(t){for(let e=0;e<this.length;e++)if(t(this.get(e),e,this))return this.get(e)}findIndex(t){for(let e=0;e<this.length;e++)if(t(this.get(e),e,this))return e;return-1}entries(){const t=[];for(let e=0;e<this.length;e++)t.push([this.get(e),e]);return t}keys(){return Array(this.length).fill(0).map(((t,e)=>e))}[r.Edit](){}[r.Unpack](t){var e;for(let s=0;s<this.length;s++)null===(e=this.get(s))||void 0===e||e[r.Unpack](t[s])}[Symbol.iterator](){const t=[];for(let e=0;e<this.length;e++)t[e]=this.get(e);return t[Symbol.iterator]()}}i=Symbol.toPrimitive,e.default=a},614:function(t,e,s){var i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});const n=s(210),o=s(787),r=s(34),l=i(s(735)),h=i(s(501));class a{get value(){const t={};for(const e in this.object)t[e]=this.object[e];return t}addEditCallback(t){this.editCallbacks.push(t)}[o.Edit](t,e){var s;1!=t.length?(0==t.length&&this.multyx.options.verbose&&console.error("Update path is empty. Attempting to edit MultyxClientObject with no path."),this.has(t[0])||this.set(t[0],new o.EditWrapper({})),null===(s=this.get(t[0]))||void 0===s||s[o.Edit](t.slice(1),e)):this.set(t[0],new o.EditWrapper(e))}constructor(t,e,s=[],i){this.editCallbacks=[],this.object={},this.propertyPath=s,this.multyx=t,this.editable=i;const n=e instanceof o.EditWrapper;e instanceof a&&(e=e.value),e instanceof o.EditWrapper&&(e=e.value);for(const t in e)this.set(t,n?new o.EditWrapper(e[t]):e[t]);if(this.constructor===a)return new Proxy(this,{has:(t,e)=>t.has(e),get:(t,e)=>e in t?t[e]:t.get(e),set:(t,e,s)=>e in t?(t[e]=s,!0):t.set(e,s),deleteProperty:(t,e)=>t.delete(e,!1)})}has(t){return t in this.object}get(t){if("string"==typeof t)return this.object[t];if(0==t.length)return this;if(1==t.length)return this.object[t[0]];const e=this.object[t[0]];return!e||e instanceof h.default?void 0:e.get(t.slice(1))}recursiveSet(t,e){if(0==t.length)return this.multyx.options.verbose&&console.error(`Attempting to edit MultyxClientObject with no path. Setting '${this.propertyPath.join(".")}' to ${e}`),!1;if(1==t.length)return this.set(t[0],e);let s=this.get(t[0]);return(s instanceof h.default||null==s)&&(isNaN(parseInt(t[1]))?(this.set(t[0],new o.EditWrapper({})),s=this.get(t[0])):(this.set(t[0],new o.EditWrapper([])),s=this.get(t[0]))),s.set(t.slice(1),e)}set(t,e){var s,i;if(Array.isArray(t))return this.recursiveSet(t,e);const n=e instanceof o.EditWrapper,a=n||this.editable;if((n||(0,r.IsMultyxClientItem)(e))&&(e=e.value),void 0===e)return this.delete(t,n);if(this.object[t]instanceof h.default&&"object"!=typeof e)return this.object[t].set(n?new o.EditWrapper(e):e);if(!a)return this.multyx.options.verbose&&console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join(".")+"."+t}' to ${e}`),!1;this.object[t]=new((0,l.default)(e))(this.multyx,n?new o.EditWrapper(e):e,[...this.propertyPath,t],this.editable);const u=Symbol.for("_"+this.propertyPath.join(".")+"."+t);return this.multyx.events.has(u)&&this.multyx[o.Done].push(...null!==(i=null===(s=this.multyx.events.get(u))||void 0===s?void 0:s.map((e=>()=>e(this.object[t]))))&&void 0!==i?i:[]),!0}delete(t,e=!1){return this.editable||e?(delete this.object[t],e||this.multyx.ws.send(n.Message.Native({instruction:"edit",path:[...this.propertyPath,t],value:void 0})),!0):(this.multyx.options.verbose&&console.error(`Attempting to delete property that is not editable. Deleting '${this.propertyPath.join(".")+"."+t}'`),!1)}keys(){return Object.keys(this.object)}values(){return Object.values(this.object)}entries(){const t=[];for(let e in this.object)t.push([e,this.get(e)]);return t}await(t){if(this.has(t))return Promise.resolve(this.get(t));const e=Symbol.for("_"+this.propertyPath.join(".")+"."+t);return new Promise((t=>this.multyx.on(e,t)))}[o.Unpack](t){var e;for(const s in t)null===(e=this.object[s])||void 0===e||e[o.Unpack](t[s])}}e.default=a},735:(t,e,s)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default=function(t){return Array.isArray(t)?s(70).default:"object"==typeof t?s(614).default:s(501).default}},501:(t,e,s)=>{var i;Object.defineProperty(e,"__esModule",{value:!0});const n=s(210),o=s(787);class r{get value(){return this.readModifiers.reduce(((t,e)=>e(t)),this._value)}set value(t){this._value=t}addReadModifier(t){this.readModifiers.push(t)}addEditCallback(t){this.editCallbacks.push(t)}[o.Edit](t,e){0==t.length&&this.set(new o.EditWrapper(e))}constructor(t,e,s=[],n){var r,l;this.readModifiers=[],this.editCallbacks=[],this.toString=()=>this.value.toString(),this.valueOf=()=>this.value,this[i]=()=>this.value,this.propertyPath=s,this.editable=n,this.multyx=t,this.constraints={},this.set(e);const h=Symbol.for("_"+this.propertyPath.join("."));this.multyx.events.has(h)&&this.multyx[o.Done].push(...null!==(l=null===(r=this.multyx.events.get(h))||void 0===r?void 0:r.map((t=>()=>t(this.value))))&&void 0!==l?l:[])}set(t){if(t instanceof o.EditWrapper){const e=this.value;return this.value=t.value,this.editCallbacks.forEach((s=>s(t.value,e))),!0}if(!this.editable)return this.multyx.options.verbose&&console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join(".")}' to ${t}`),!1;let e=t;for(const s in this.constraints)if(e=(0,this.constraints[s])(e),null===e)return this.multyx.options.verbose&&console.error(`Attempting to set property that failed on constraint. Setting '${this.propertyPath.join(".")}' to ${t}, stopped by constraint '${s}'`),!1;return this.value===e?(this.value=e,!0):(this.value=e,this.multyx.ws.send(n.Message.Native({instruction:"edit",path:this.propertyPath,value:e})),!0)}bindElement(t){this.addEditCallback(((e,s)=>{e!==s&&(t.innerText=e.toString())}))}[o.Unpack](t){for(const[e,s]of Object.entries(t)){const t=(0,o.BuildConstraint)(e,s);t&&(this.constraints[e]=t)}}}i=Symbol.toPrimitive,e.default=r},210:(t,e)=>{function s(t){let e,s;if("edit"==t.instruction?(e=0,s=[t.path.join("."),JSON.stringify(t.value)]):"input"==t.instruction?(e=1,s=[t.input,JSON.stringify(t.data)]):"resp"==t.instruction&&(e=2,s=[t.name,JSON.stringify(t.response)]),!s||!e)return"";let i=e.toString();for(let t=0;t<s.length;t++)i+=s[t].replace(/;/g,";_"),t<s.length-1&&(i+=";,");return JSON.stringify([i])}Object.defineProperty(e,"__esModule",{value:!0}),e.Message=void 0,e.UncompressUpdate=function(t){const[e,...s]=t.split(/;,/),i=e[0],n=e.slice(1).replace(/;_/g,";"),o=s.map((t=>t.replace(/;_/g,";"))).map((t=>"undefined"==t?void 0:JSON.parse(t)));return"0"==i?{instruction:"edit",team:!1,path:n.split("."),value:o[0]}:"1"==i?{instruction:"edit",team:!0,path:n.split("."),value:o[0]}:"2"==i?{instruction:"self",property:"controller",data:JSON.parse(n)}:"3"==i?{instruction:"self",property:"uuid",data:JSON.parse(n)}:"4"==i?{instruction:"self",property:"constraint",data:JSON.parse(n)}:"9"==i?{instruction:"self",property:"space",data:JSON.parse(n)}:"5"==i?{instruction:"resp",name:n,response:o[0]}:"6"==i?{instruction:"conn",uuid:n,publicData:o[0]}:"7"==i?{instruction:"dcon",clientUUID:n}:"8"==i?{instruction:"init",client:JSON.parse(n),tps:o[0],constraintTable:o[1],clients:o[2],teams:o[3],space:o[4]}:void 0},e.CompressUpdate=s;class i{constructor(t,e,s=!1){this.name=t,this.data=e,this.time=Date.now(),this.native=s}static BundleOperations(t,e){return Array.isArray(e)||(e=[e]),JSON.stringify(new i("_",{operations:e,deltaTime:t}))}static Native(t){return s(t)}static Parse(t){var e,s;const n=JSON.parse(t);return Array.isArray(n)?new i("_",n,!0):new i(null!==(e=n.name)&&void 0!==e?e:"",null!==(s=n.data)&&void 0!==s?s:"",!1)}static Create(t,e){if(0==t.length)throw new Error("Multyx message cannot have empty name");if("_"==t[0]&&(t="_"+t),"function"==typeof e)throw new Error("Multyx data must be JSON storable");return JSON.stringify(new i(t,e))}}e.Message=i},944:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.DefaultOptions=void 0,e.DefaultOptions={port:8443,secure:!1,uri:"localhost",verbose:!1,logUpdateFrame:!1}},787:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.EditWrapper=e.Edit=e.Add=e.Done=e.Unpack=void 0,e.Interpolate=function(t,e,s){let i={value:t[e],time:Date.now()},n={value:t[e],time:Date.now()};Object.defineProperty(t,e,{get:()=>{const t=n.time-i.time;let e=s[0],o=s[0];for(const i of s)t>i.time&&i.time>e.time&&(e=i),t<i.time&&i.time<o.time&&(o=i);const r=(t-e.time)/(o.time-e.time),l=e.progress+r*(o.progress-e.progress);return Number.isNaN(l)?i.value:n.value*l+i.value*(1-l)},set:t=>Date.now()-n.time<10?(n.value=t,!0):(i=Object.assign({},n),n={value:t,time:Date.now()},!0)})},e.BuildConstraint=function(t,e){return"min"==t?t=>t>=e[0]?t:e[0]:"max"==t?t=>t<=e[0]?t:e[0]:"int"==t?t=>Math.floor(t):"ban"==t?t=>e.includes(t)?null:t:"disabled"==t?t=>e[0]?null:t:t=>t},e.Unpack=Symbol("unpack"),e.Done=Symbol("done"),e.Add=Symbol("add"),e.Edit=Symbol("edit"),e.EditWrapper=class{constructor(t){this.value=t}}}},e={};function s(i){var n=e[i];if(void 0!==n)return n.exports;var o=e[i]={exports:{}};return t[i].call(o.exports,o,o.exports,s),o.exports}var i={};return(()=>{var t,e=i;const n=s(210),o=s(787),r=s(376),l=s(34),h=s(944);class a{constructor(e={},s){var i;if(this[t]=[],this.options=Object.assign(Object.assign({},h.DefaultOptions),e),!this.options.uri)throw new Error("URI is required");const o=`ws${this.options.secure?"s":""}://${this.options.uri.split("/")[0]}:${this.options.port}/${null!==(i=this.options.uri.split("/")[1])&&void 0!==i?i:""}`;this.ws=new WebSocket(o),this.ping=0,this.space="default",this.events=new Map,this.self={},this.tps=0,this.all={},this.teams=new l.MultyxClientObject(this,{},[],!0),this.clients={},this.controller=new r.Controller(this.ws),null==s||s(),this.ws.onmessage=t=>{var e,s,i,o;const r=n.Message.Parse(t.data);this.ping=2*(Date.now()-r.time),r.native?(this.parseNativeEvent(r),null===(e=this.events.get(a.Native))||void 0===e||e.forEach((t=>t(r)))):(null===(s=this.events.get(r.name))||void 0===s||s.forEach((t=>{const e=t(r.data);void 0!==e&&this.send(r.name,e)})),null===(i=this.events.get(a.Custom))||void 0===i||i.forEach((t=>t(r)))),null===(o=this.events.get(a.Any))||void 0===o||o.forEach((t=>t(r)))}}on(t,e){var s;const i=null!==(s=this.events.get(t))&&void 0!==s?s:[];i.push(e),this.events.set(t,i)}send(t,e){"_"===t[0]&&(t="_"+t);const s={instruction:"resp",name:t,response:e};this.ws.send(n.Message.Native(s))}await(t,e){return this.send(t,e),new Promise((e=>this.events.set(Symbol.for("_"+t),[e])))}loop(t,e){if(e)this.on(a.Start,(()=>setInterval(t,Math.round(1e3/e))));else{const e=()=>{t(),requestAnimationFrame(e)};this.on(a.Start,(()=>requestAnimationFrame(e)))}}[(t=o.Done,o.Add)](t){this[o.Done].push(t)}parseNativeEvent(t){var e,s,i,r,h;t.data=t.data.map(n.UncompressUpdate),this.options.logUpdateFrame&&console.log(t.data);for(const n of t.data)switch(n.instruction){case"init":this.initialize(n);for(const t of null!==(e=this.events.get(a.Start))&&void 0!==e?e:[])this[o.Done].push((()=>t(n)));this.events.has(a.Start)&&(this.events.get(a.Start).length=0);break;case"edit":if(1==n.path.length)n.team?this.teams.set(n.path[0],new o.EditWrapper(n.value)):this.clients[n.path[0]]=new l.MultyxClientObject(this,new o.EditWrapper(n.value),[n.path[0]],!1);else{const t=n.team?this.teams.get(n.path[0]):this.clients[n.path[0]];if(!t)return;t.set(n.path.slice(1),new o.EditWrapper(n.value))}for(const t of null!==(s=this.events.get(a.Edit))&&void 0!==s?s:[])this[o.Done].push((()=>t(n)));break;case"self":this.parseSelf(n);break;case"conn":this.clients[n.uuid]=new l.MultyxClientObject(this,n.data,[n.uuid],!1);for(const t of null!==(i=this.events.get(a.Connection))&&void 0!==i?i:[])this[o.Done].push((()=>t(this.clients[n.uuid])));break;case"dcon":for(const t of null!==(r=this.events.get(a.Disconnect))&&void 0!==r?r:[]){const e=this.clients[n.client].value;this[o.Done].push((()=>t(e)))}delete this.clients[n.client];break;case"resp":{const t=null===(h=this.events.get(Symbol.for("_"+n.name)))||void 0===h?void 0:h[0];this.events.delete(Symbol.for("_"+n.name)),t&&this[o.Done].push((()=>t(n.response)));break}default:this.options.verbose&&console.error("Server error: Unknown native Multyx instruction")}this[o.Done].forEach((t=>t())),this[o.Done].length=0}initialize(t){this.tps=t.tps,this.uuid=t.client.uuid,this.joinTime=t.client.joinTime,this.controller.listening=new Set(t.client.controller);for(const e of Object.keys(t.teams))this.teams[e]=new o.EditWrapper(t.teams[e]);this.all=this.teams.all,this.clients={};for(const[e,s]of Object.entries(t.clients))e!=this.uuid&&(this.clients[e]=new l.MultyxClientObject(this,new o.EditWrapper(s),[e],!1));const e=new l.MultyxClientObject(this,new o.EditWrapper(t.client.self),[this.uuid],!0);this.self=e,this.clients[this.uuid]=e;for(const[e,s]of Object.entries(t.constraintTable))(this.uuid==e?this.self:this.teams[e])[o.Unpack](s)}parseSelf(t){if("controller"==t.property)this.controller.listening=new Set(t.data);else if("uuid"==t.property)this.uuid=t.data;else if("constraint"==t.property){let e=this.uuid==t.data.path[0]?this.self:this.teams[t.data.path[0]];for(const s of t.data.path.slice(1))e=null==e?void 0:e[s];if(void 0===e)return;e[o.Unpack]({[t.data.name]:t.data.args})}else"space"==t.property&&(this.space=t.data,this.updateSpace())}updateSpace(){"default"!=this.space?document.querySelectorAll("[data-multyx-space]").forEach((t=>{t.style.display=t.dataset.multyxSpace==this.space?"block":"none",t.style.pointerEvents=t.dataset.multyxSpace==this.space?"auto":"none"})):document.querySelectorAll("[data-multyx-space]").forEach((t=>{t.style.display="block",t.style.pointerEvents="auto"}))}}a.Start=Symbol("start"),a.Connection=Symbol("connection"),a.Disconnect=Symbol("disconnect"),a.Edit=Symbol("edit"),a.Native=Symbol("native"),a.Custom=Symbol("custom"),a.Any=Symbol("any"),e.default=a})(),i.default})()));
1
+ !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Multyx=e():t.Multyx=e()}(self,()=>(()=>{"use strict";var t={249:function(t,e,i){var s,n=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});const o=i(625),r=i(703),l=n(i(416)),a=i(449);class h{addEditCallback(t){this.editCallbacks.push(t)}get value(){var t;const e=[];for(let i=0;i<this.length;i++)e[i]=null===(t=this.get(i))||void 0===t?void 0:t.value;return e}get length(){return this.list.length}set length(t){this.list.length=t}handleShiftOperation(t,e){const i=t>=0?e>=0?"right":"left":0==e?"reverse":e<0?"length":"unknown";switch(i){case"reverse":for(let t=0;t<Math.floor(this.length/2);t++){const e=this.list[t];this.list[t]=this.list[this.length-1-t],this.list[this.length-1-t]=e}break;case"left":for(let i=t;i<this.length;i++)i+e<0||(this.list[i+e]=this.list[i]);break;case"right":for(let i=this.length-1;i>=t;i--)this.list[i+e]=this.list[i];break;case"length":this.length+=e;break;default:this.multyx.options.verbose&&console.error("Unknown shift operation: "+i)}}constructor(t,e,i=[],n){this.type="list",this.editCallbacks=[],this.toString=()=>this.value.toString(),this.valueOf=()=>this.value,this[s]=()=>this.value,this.list=[],this.propertyPath=i,this.multyx=t,this.editable=n;const o=e instanceof r.EditWrapper;e instanceof h&&(e=e.value),e instanceof r.EditWrapper&&(e=e.value);for(let t=0;t<e.length;t++)this.set(t,o?new r.EditWrapper(e[t]):e[t]);return new Proxy(this,{has:(t,e)=>"number"==typeof e?t.has(e):e in t,get:(t,e)=>e in t?t[e]:(isNaN(parseInt(e))||(e=parseInt(e)),t.get(e)),set:(t,e,i)=>e in t?(t[e]=i,!0):!!t.set(e,i),deleteProperty:(t,e)=>"number"==typeof e&&t.delete(e)})}has(t){return t>=0&&t<this.length}get(t){if("number"==typeof t)return this.list[t];if(0==t.length)return this;if(1==t.length)return this.list[parseInt(t[0])];const e=this.list[parseInt(t[0])];return!e||e instanceof o.MultyxClientValue?void 0:e.get(t.slice(1))}recursiveSet(t,e){if(0==t.length)return this.multyx.options.verbose&&console.error(`Attempting to edit MultyxClientList with no path. Setting '${this.propertyPath.join(".")}' to ${e}`),!1;if("shift"==t[0]&&e instanceof r.EditWrapper)return this.handleShiftOperation(parseInt(t[1]),e.value),!0;if(1==t.length)return this.set(parseInt(t[0]),e);let i=this.get(parseInt(t[0]));return(i instanceof o.MultyxClientValue||null==i)&&(this.set(parseInt(t[0]),new r.EditWrapper({})),i=this.get(parseInt(t[0]))),!(!i||i instanceof o.MultyxClientValue)&&i.set(t.slice(1),e)}set(t,e){if(Array.isArray(t))return this.recursiveSet(t,e);const i=this.get(t),s=e instanceof r.EditWrapper,n=s||this.editable,a=s||(0,o.IsMultyxClientItem)(e)?e.value:e;if(void 0===a)return this.delete(t,s);if(s&&this.tryApplyServerValue(t,a,i))return!0;if(this.list[t]instanceof o.MultyxClientValue&&("object"!=typeof a||null===a)){const e=this.list[t].set(s?new r.EditWrapper(a):a);return this.enqueueEditCallbacks(t,i),e}return n?(this.list[t]=new((0,l.default)(a))(this.multyx,s?new r.EditWrapper(a):a,[...this.propertyPath,t.toString()],this.editable),this.notifyIndexWaiters(t),this.enqueueEditCallbacks(t,i),!0):(this.multyx.options.verbose&&console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join(".")+"."+t}' to ${a}`),!1)}delete(t,e=!1){const i=this.get(t);if("string"==typeof t&&(t=parseInt(t)),!this.editable&&!e)return this.multyx.options.verbose&&console.error(`Attempting to delete property that is not editable. Deleting '${this.propertyPath.join(".")+"."+t}'`),!1;delete this.list[t];for(const e of this.editCallbacks)this.multyx[r.Add](()=>e(t,void 0,i));return e||this.multyx.ws.send(a.Message.Native({instruction:"edit",path:[...this.propertyPath,t.toString()],value:void 0})),!0}await(t){if(this.has(t))return Promise.resolve(this.get(t));const e=Symbol.for("_"+this.propertyPath.join(".")+"."+t);return new Promise(t=>this.multyx.on(e,t))}push(...t){for(const e of t)this.set(this.length,e);return this.length}pop(){if(0===this.length)return;const t=this.get(this.length);return this.delete(this.length),t}unshift(...t){for(let e=this.length-1;e>=0;e--)e>=t.length?this.set(e,this.get(e-t.length)):this.set(e,t[e]);return this.length}shift(){if(0==this.length)return;this.length--;const t=this.get(0);for(let t=0;t<this.length;t++)this.set(t,this.get(t+1));return t}slice(t,e){return this.list.slice(t,e)}splice(t,e,...i){return this.list.splice(t,null!=e?e:0,...i)}setSplice(t,e,...i){void 0===e&&(e=this.length-t);let s=i.length-e;if(s>0)for(let i=this.length-1;i>=t+e;i--)this.set(i+s,this.get(i));else if(s<0){for(let i=t+e;i<this.length;i++)this.set(i+s,this.get(i));const i=this.length;for(let t=i+s;t<i;t++)this.set(t,void 0)}for(let e=t;e<i.length;e++)this.set(e,i[e])}filter(t){return this.list.filter((e,i)=>t(e,i,this))}setFilter(t){const e=[];for(let i=0;i<this.length;i++)e.push(t(this.get(i),i,this));let i=0;for(let t=0;t<e.length;t++)e[t]&&i&&this.set(t-i,this.get(t)),e[t]||i--}map(t){const e=[];for(let i=0;i<this.length;i++)e.push(t(this.get(i),i,this));return e}flat(){return this.list.flat()}setFlat(){for(let t=0;t<this.length;t++){const e=this.get(t);if(e instanceof h)for(let i=0;i<e.length;i++)t++,this.set(t,e[i])}}reduce(t,e){for(let i=0;i<this.length;i++)e=t(e,this.get(i),i,this);return e}reduceRight(t,e){for(let i=this.length-1;i>=0;i--)e=t(e,this.get(i),i,this);return e}reverse(){let t=this.length-1;for(let e=0;e<t;e++){const i=this.get(e),s=this.get(t);this.set(e,s),this.set(t,i)}return this}forEach(t){for(let e=0;e<this.length;e++)t(this.get(e),e,this)}every(t){for(let e=0;e<this.length;e++)if(!t(this.get(e),e,this))return!1;return!0}some(t){for(let e=0;e<this.length;e++)if(t(this.get(e),e,this))return!0;return!1}find(t){for(let e=0;e<this.length;e++)if(t(this.get(e),e,this))return this.get(e)}findIndex(t){for(let e=0;e<this.length;e++)if(t(this.get(e),e,this))return e;return-1}entries(){const t=[];for(let e=0;e<this.length;e++)t.push([this.get(e),e]);return t}keys(){return Array(this.length).fill(0).map((t,e)=>e)}[r.Edit](){}[r.Unpack](t){var e;for(let i=0;i<this.length;i++)null===(e=this.get(i))||void 0===e||e[r.Unpack](t[i])}[Symbol.iterator](){const t=[];for(let e=0;e<this.length;e++)t[e]=this.get(e);return t[Symbol.iterator]()}hydrateFromServer(t){if(Array.isArray(t)){for(let e=0;e<t.length;e++)this.set(e,new r.EditWrapper(t[e]));for(let e=t.length;e<this.length;e++)this.delete(e,!0);this.length=t.length}}tryApplyServerValue(t,e,i){const s=this.list[t];if(!s)return!1;if(s instanceof o.MultyxClientValue&&("object"!=typeof e||null===e))return s.set(new r.EditWrapper(e)),this.enqueueEditCallbacks(t,i),!0;const n="function"==typeof(null==s?void 0:s.hydrateFromServer);return Array.isArray(e)&&n&&"list"===s.type?(s.hydrateFromServer(e),this.enqueueEditCallbacks(t,i),!0):!(null===(l=e)||"object"!=typeof l||Array.isArray(l)||!n||"object"!==s.type||(s.hydrateFromServer(e),this.enqueueEditCallbacks(t,i),0));var l}notifyIndexWaiters(t){var e,i;const s=Symbol.for("_"+this.propertyPath.join(".")+"."+t);this.multyx.events.has(s)&&this.multyx[r.Done].push(...null!==(i=null===(e=this.multyx.events.get(s))||void 0===e?void 0:e.map(e=>()=>e(this.list[t])))&&void 0!==i?i:[])}enqueueEditCallbacks(t,e){for(const i of this.editCallbacks)this.multyx[r.Add](()=>i(t,this.get(t),e))}}s=Symbol.toPrimitive,e.default=h},280:(t,e,i)=>{var s;Object.defineProperty(e,"__esModule",{value:!0});const n=i(449),o=i(703);class r{get value(){return this.readModifiers.reduce((t,e)=>e(t),this._value)}set value(t){this._value=t,this.captureSample(t)}addReadModifier(t){this.readModifiers.push(t)}addEditCallback(t){this.editCallbacks.push(t)}[o.Edit](t,e){0==t.length&&this.set(new o.EditWrapper(e))}constructor(t,e,i=[],n){var r,l;this.readModifiers=[],this.editCallbacks=[],this.interpolationFrameMs=250,this.toString=()=>this.value.toString(),this.valueOf=()=>this.value,this[s]=()=>this.value,this.propertyPath=i,this.editable=n,this.multyx=t,this.constraints={},this.set(e);const a=Symbol.for("_"+this.propertyPath.join("."));this.multyx.events.has(a)&&this.multyx[o.Done].push(...null!==(l=null===(r=this.multyx.events.get(a))||void 0===r?void 0:r.map(t=>()=>t(this.value)))&&void 0!==l?l:[])}set(t){if(t instanceof o.EditWrapper){const e=this.value;return this.value=t.value,this.editCallbacks.forEach(i=>i(t.value,e)),!0}if(!this.editable)return this.multyx.options.verbose&&console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join(".")}' to ${t}`),!1;let e=t;for(const i in this.constraints)if(e=(0,this.constraints[i])(e),null===e)return this.multyx.options.verbose&&console.error(`Attempting to set property that failed on constraint. Setting '${this.propertyPath.join(".")}' to ${t}, stopped by constraint '${i}'`),!1;return this._value===e?(this.value=e,!0):(this.value=e,this.multyx.ws.send(n.Message.Native({instruction:"edit",path:this.propertyPath,value:e})),!0)}bindElement(t){this.addEditCallback((e,i)=>{e!==i&&(t.innerText=e.toString())})}[o.Unpack](t){for(const[e,i]of Object.entries(t)){const t=(0,o.BuildConstraint)(e,i);t&&(this.constraints[e]=t)}}Lerp(t=250){return this.applyInterpolation("lerp",t)}PredictiveLerp(t=250){return this.applyInterpolation("predictive",t)}applyInterpolation(t,e){if("number"!=typeof this._value||Number.isNaN(this._value))throw new Error(`MultyxClientValue.${"lerp"===t?"Lerp":"PredictiveLerp"} can only be applied to numeric values`);return this.interpolationFrameMs=Math.max(1,e),this.attachInterpolationModifier(t),this}attachInterpolationModifier(t){this.interpolationModifier&&(this.readModifiers=this.readModifiers.filter(t=>t!==this.interpolationModifier)),this.interpolationModifier=e=>this.interpolateValue(e,t),this.readModifiers.push(this.interpolationModifier)}captureSample(t){if("number"!=typeof t||Number.isNaN(t))return this.latestSample=void 0,void(this.previousSample=void 0);const e=Date.now();this.latestSample?(this.previousSample=Object.assign({},this.latestSample),this.latestSample={value:t,time:e}):this.latestSample={value:t,time:e}}interpolateValue(t,e){if("number"!=typeof t||!this.latestSample||!this.previousSample)return t;const i=this.latestSample.time-this.previousSample.time;if(i<=0)return t;const s=Math.max(1,Math.min(i,this.interpolationFrameMs)),n=Math.max(0,Math.min(Date.now()-this.latestSample.time,s)),o=0===s?1:n/s,r=this.latestSample.value-this.previousSample.value;return"predictive"===e?this.latestSample.value+r*o:this.previousSample.value+r*o}}s=Symbol.toPrimitive,e.default=r},416:(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default=function(t){return Array.isArray(t)?i(249).default:"object"==typeof t?i(922).default:i(280).default}},449:(t,e)=>{function i(t){let e,i;if("edit"==t.instruction?(e=0,i=[t.path.join("."),JSON.stringify(t.value)]):"input"==t.instruction?(e=1,i=[t.input,JSON.stringify(t.data)]):"resp"==t.instruction&&(e=2,i=[t.name,JSON.stringify(t.response)]),!i||void 0===e)return"";let s=e.toString();for(let t=0;t<i.length;t++)s+=i[t].replace(/;/g,";_"),t<i.length-1&&(s+=";,");return JSON.stringify([s])}Object.defineProperty(e,"__esModule",{value:!0}),e.Message=void 0,e.UncompressUpdate=function(t){const[e,...i]=t.split(/;,/),s=e[0],n=e.slice(1).replace(/;_/g,";"),o=i.map(t=>t.replace(/;_/g,";")).map(t=>"undefined"==t?void 0:JSON.parse(t));return"0"==s?{instruction:"edit",team:!1,path:n.split("."),value:o[0]}:"1"==s?{instruction:"edit",team:!0,path:n.split("."),value:o[0]}:"2"==s?{instruction:"self",property:"controller",data:JSON.parse(n)}:"3"==s?{instruction:"self",property:"uuid",data:JSON.parse(n)}:"4"==s?{instruction:"self",property:"constraint",data:JSON.parse(n)}:"9"==s?{instruction:"self",property:"space",data:JSON.parse(n)}:"5"==s?{instruction:"resp",name:n,response:o[0]}:"6"==s?{instruction:"conn",uuid:n,data:o[0]}:"7"==s?{instruction:"dcon",client:n}:"8"==s?{instruction:"init",client:JSON.parse(n),tps:o[0],constraintTable:o[1],clients:o[2],teams:o[3],space:o[4]}:void 0},e.CompressUpdate=i;class s{constructor(t,e,i=!1){this.name=t,this.data=e,this.time=Date.now(),this.native=i}static BundleOperations(t,e){return Array.isArray(e)||(e=[e]),JSON.stringify(new s("_",{operations:e,deltaTime:t}))}static Native(t){return i(t)}static Parse(t){var e,i;const n=JSON.parse(t);return Array.isArray(n)?new s("_",n,!0):new s(null!==(e=n.name)&&void 0!==e?e:"",null!==(i=n.data)&&void 0!==i?i:"",!1)}static Create(t,e){if(0==t.length)throw new Error("Multyx message cannot have empty name");if("_"==t[0]&&(t="_"+t),"function"==typeof e)throw new Error("Multyx data must be JSON storable");return JSON.stringify(new s(t,e))}}e.Message=s},625:function(t,e,i){var s=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.MultyxClientValue=e.MultyxClientObject=e.MultyxClientList=void 0,e.IsMultyxClientItem=function(t){return t instanceof n.default||t instanceof o.default||t instanceof r.default};const n=s(i(249));e.MultyxClientList=n.default;const o=s(i(922));e.MultyxClientObject=o.default;const r=s(i(280));e.MultyxClientValue=r.default},703:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.EditWrapper=e.Edit=e.Add=e.Done=e.Unpack=void 0,e.Interpolate=function(t,e,i){if(!Array.isArray(i)||0===i.length)throw new Error("Interpolation curve must contain at least one slice");const s=[...i].sort((t,e)=>t.time-e.time),n=s[s.length-1].time,o=n<=1;let r={value:t[e],time:Date.now()},l={value:t[e],time:Date.now()};Object.defineProperty(t,e,{configurable:!0,enumerable:!0,get:()=>{if(r.time===l.time)return l.value;const t=Date.now(),e=Math.max(l.time-r.time,1),i=Math.max(0,t-l.time),a=(h=o?i/e:i,u=0,p=n,Math.min(Math.max(h,u),p));var h,u,p;let c,d=s[0],f=s[s.length-1];for(const t of s){if(!(t.time<=a)){f=t;break}d=t}if(f.time===d.time)c=d.progress;else{const t=(a-d.time)/(f.time-d.time);c=d.progress+t*(f.progress-d.progress)}return Number.isNaN(c)?r.value:"number"==typeof r.value&&"number"==typeof l.value?l.value*c+r.value*(1-c):c>=1?l.value:r.value},set:t=>{const e=Date.now();return e-l.time<10?(l.value=t,l.time=e,!0):(r=Object.assign({},l),l={value:t,time:e},!0)}})},e.BuildConstraint=function(t,e){return"min"==t?t=>t>=e[0]?t:e[0]:"max"==t?t=>t<=e[0]?t:e[0]:"int"==t?t=>Math.floor(t):"ban"==t?t=>e.includes(t)?null:t:"disabled"==t?t=>e[0]?null:t:t=>t},e.Unpack=Symbol("unpack"),e.Done=Symbol("done"),e.Add=Symbol("add"),e.Edit=Symbol("edit"),e.EditWrapper=class{constructor(t){this.value=t}}},832:(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.Controller=void 0;const s=i(449);e.Controller=class{constructor(t){this.listening=new Set,this.ws=t,this.preventDefault=!1,this.keys={},this.mouse={x:NaN,y:NaN,down:!1,centerX:0,centerY:0,scaleX:1,scaleY:1},document.addEventListener("keydown",t=>{this.preventDefault&&t.preventDefault();const e=t.key.toLowerCase();this.keys[e]&&this.listening.has("keyhold")&&this.relayInput("keyhold",{code:e}),this.keys[t.code]&&this.listening.has("keyhold")&&this.relayInput("keyhold",{code:t.code}),this.listening.has(e)&&!this.keys[e]&&this.relayInput("keydown",{code:t.key}),this.listening.has(t.code)&&!this.keys[t.code]&&this.relayInput("keydown",{code:t.code}),this.keys[e]=!0,this.keys[t.code]=!0}),document.addEventListener("keyup",t=>{this.preventDefault&&t.preventDefault();const e=t.key.toLowerCase();delete this.keys[e],delete this.keys[t.code],this.listening.has(e)&&this.relayInput("keyup",{code:e}),this.listening.has(t.code)&&this.relayInput("keyup",{code:t.code})}),document.addEventListener("mousedown",t=>{if(this.preventDefault&&t.preventDefault(),this.mouseGetter){const t=this.mouseGetter();this.mouse.x=t.x,this.mouse.y=t.y}else this.mouse.x=(t.clientX-this.mouse.centerX)/this.mouse.scaleX,this.mouse.y=(t.clientY-this.mouse.centerY)/this.mouse.scaleY;this.mouse.down=!0,this.listening.has("mousedown")&&this.relayInput("mousedown",{x:this.mouse.x,y:this.mouse.y})}),document.addEventListener("mouseup",t=>{if(this.preventDefault&&t.preventDefault(),this.mouseGetter){const t=this.mouseGetter();this.mouse.x=t.x,this.mouse.y=t.y}else this.mouse.x=(t.clientX-this.mouse.centerX)/this.mouse.scaleX,this.mouse.y=(t.clientY-this.mouse.centerY)/this.mouse.scaleY;this.mouse.down=!1,this.listening.has("mouseup")&&this.relayInput("mouseup",{x:this.mouse.x,y:this.mouse.y})}),document.addEventListener("mousemove",t=>{if(this.preventDefault&&t.preventDefault(),this.mouseGetter){const t=this.mouseGetter();this.mouse.x=t.x,this.mouse.y=t.y}else this.mouse.x=(t.clientX-this.mouse.centerX)/this.mouse.scaleX,this.mouse.y=(t.clientY-this.mouse.centerY)/this.mouse.scaleY;this.listening.has("mousemove")&&this.relayInput("mousemove",{x:this.mouse.x,y:this.mouse.y})})}mapCanvasPosition(t,e){const i="top"in e,s="bottom"in e,n="left"in e,o="right"in e,r=e.anchor,l=t.getBoundingClientRect(),a=(t,...e)=>{const i=t?"Cannot include value for ":"Must include value for ",s=1==e.length?e[0]:e.slice(0,-1).join(", ")+(t?" and ":" or ")+e.slice(-1)[0],n=r?" if anchoring at "+r:" if not anchoring";console.error(i+s+n)},h=l.width/l.height,u=l.height/l.width;if((Number.isNaN(h)||Number.isNaN(u))&&console.error("Canvas element bounding box is flat, canvas must be present on the screen"),r){if("center"==r){if(i&&s&&e.top!==-e.bottom||n&&o&&e.left!==-e.right)return a(!0,"top","bottom","left","right");i?(e.left=n?e.left:o?-e.right:-Math.abs(h*e.top),e.right=n?-e.left:o?e.right:Math.abs(h*e.top),e.bottom=-e.top):s?(e.left=n?e.left:o?-e.right:-Math.abs(h*e.bottom),e.right=n?-e.left:o?e.right:Math.abs(h*e.bottom),e.top=-e.bottom):n?(e.top=i?e.top:s?-e.bottom:-Math.abs(u*e.left),e.bottom=i?-e.top:s?e.bottom:Math.abs(u*e.left),e.right=-e.left):o&&(e.top=i?e.top:s?-e.bottom:-Math.abs(u*e.right),e.bottom=i?-e.top:s?e.bottom:Math.abs(u*e.right),e.left=-e.right)}else if("bottom"==r){if(!n&&!o&&!i)return a(!1,"left","right","top");if(e.bottom)return a(!0,"bottom");e.bottom=0,n?(e.top=Math.abs(u*e.left*2),e.right=-e.left):o?(e.top=Math.abs(u*e.right*2),e.left=-e.right):(e.left=-Math.abs(h*e.top/2),e.right=-e.left)}else if("top"==r){if(!n&&!o&&!s)return a(!1,"left","right","bottom");if(e.top)return a(!0,"top");e.top=0,n?(e.bottom=Math.abs(u*e.left*2),e.right=-e.left):o?(e.bottom=Math.abs(u*e.right*2),e.left=-e.right):(e.left=-Math.abs(h*e.bottom/2),e.right=-e.left)}else if("left"==r){if(!i&&!s&&!o)return a(!1,"top","bottom","right");if(n)return a(!0,"left");e.left=0,i?(e.right=-Math.abs(h*e.top*2),e.bottom=-e.top):s?(e.right=Math.abs(h*e.bottom*2),e.top=-e.bottom):(e.top=-Math.abs(u*e.right/2),e.bottom=-e.top)}else if("right"==r){if(!i&&!s&&!n)return a(!1,"top","bottom","left");if(o)return a(!0,"right");e.right=0,i?(e.left=-Math.abs(h*e.top*2),e.bottom=-e.top):s?(e.left=Math.abs(h*e.bottom*2),e.top=-e.bottom):(e.top=-Math.abs(u*e.right/2),e.bottom=-e.top)}else if("topleft"==r){if(!o&&!s)return a(!1,"right","bottom");if(n||i)return a(!0,"left","top");e.left=e.top=0,o?e.bottom=Math.abs(u*e.right):e.right=Math.abs(h*e.bottom)}else if("topright"==r){if(!n&&!s)return a(!1,"left","bottom");if(o||i)return a(!0,"right","top");e.right=e.top=0,n?e.bottom=Math.abs(u*e.left):e.left=Math.abs(h*e.bottom)}else if("bottomleft"==r){if(!o&&!i)return a(!1,"right","top");if(s||n)return a(!0,"bottom","left");e.left=e.bottom=0,o?e.top=Math.abs(u*e.right):e.right=Math.abs(h*e.top)}else if("bottomright"==r){if(!i&&!n)return a(!1,"top","left");if(o||s)return a(!0,"bottom","right");e.right=e.bottom=0,n?e.top=Math.abs(u*e.left):e.left=Math.abs(h*e.top)}}else{if(!i&&!s)return a(!1,"top","bottom");if(s?i||(e.top=e.bottom-t.height):e.bottom=e.top+t.height,!n&&!o)return a(!1,"left","right");o?n||(e.left=e.right-t.width):e.right=e.left+t.width}const p=t.getContext("2d");null==p||p.setTransform(1,0,0,1,0,0),t.width=Math.floor(Math.abs(e.right-e.left)),t.height=Math.floor(Math.abs(e.bottom-e.top)),e.right<e.left&&(null==p||p.scale(-1,1)),e.top>e.bottom&&(null==p||p.scale(1,-1)),null==p||p.translate(-e.left,-e.top)}mapMousePosition(t,e,i=document.body,s=1,n=s){const o=window.innerWidth/(i instanceof HTMLCanvasElement?i.width:i.clientWidth),r=window.innerHeight/(i instanceof HTMLCanvasElement?i.height:i.clientHeight),l=i.getBoundingClientRect();this.mouse.centerX=l.left+t*o,this.mouse.centerY=l.top+e*r,this.mouse.scaleX=s*o,this.mouse.scaleY=n*r}mapMouseToCanvas(t){const e=t.getContext("2d"),i=null==e?void 0:e.getTransform(),s=t.getBoundingClientRect(),n=s.width/t.width,o=s.height/t.height;this.mouse.centerX=s.left+(null==i?void 0:i.e)*n,this.mouse.centerY=s.top+(null==i?void 0:i.f)*o,this.mouse.scaleX=n*(null==i?void 0:i.a),this.mouse.scaleY=o*(null==i?void 0:i.d)}setMouseAs(t){this.mouseGetter=t}relayInput(t,e){if(1!==this.ws.readyState)throw new Error("Websocket connection is "+(2==this.ws.readyState?"closing":"closed"));this.ws.send(s.Message.Native(Object.assign({instruction:"input",input:t},e?{data:e}:{})))}}},922:function(t,e,i){var s=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0});const n=i(449),o=i(703),r=i(625),l=s(i(416)),a=s(i(280)),h=t=>null!==t&&"object"==typeof t&&!Array.isArray(t);class u{get value(){const t={};for(const e in this.object)t[e]=this.object[e];return t}addEditCallback(t){this.editCallbacks.push(t)}[o.Edit](t,e){var i;1!=t.length?(0==t.length&&this.multyx.options.verbose&&console.error("Update path is empty. Attempting to edit MultyxClientObject with no path."),this.has(t[0])||this.set(t[0],new o.EditWrapper({})),null===(i=this.get(t[0]))||void 0===i||i[o.Edit](t.slice(1),e)):this.set(t[0],new o.EditWrapper(e))}constructor(t,e,i=[],s){this.type="object",this.editCallbacks=[],this.object={},this.propertyPath=i,this.multyx=t,this.editable=s;const n=e instanceof o.EditWrapper;e instanceof u&&(e=e.value),e instanceof o.EditWrapper&&(e=e.value);for(const t in e)this.set(t,n?new o.EditWrapper(e[t]):e[t]);if(this.constructor===u)return new Proxy(this,{has:(t,e)=>t.has(e),get:(t,e)=>e in t?t[e]:t.get(e),set:(t,e,i)=>e in t?(t[e]=i,!0):t.set(e,i),deleteProperty:(t,e)=>t.delete(e,!1)})}has(t){return t in this.object}get(t){if("string"==typeof t)return this.object[t];if(0==t.length)return this;if(1==t.length)return this.object[t[0]];const e=this.object[t[0]];return!e||e instanceof a.default?void 0:e.get(t.slice(1))}recursiveSet(t,e){if(0==t.length)return this.multyx.options.verbose&&console.error(`Attempting to edit MultyxClientObject with no path. Setting '${this.propertyPath.join(".")}' to ${e}`),!1;if(1==t.length)return this.set(t[0],e);let i=this.get(t[0]);return(i instanceof a.default||null==i)&&(isNaN(parseInt(t[1]))?(this.set(t[0],new o.EditWrapper({})),i=this.get(t[0])):(this.set(t[0],new o.EditWrapper([])),i=this.get(t[0]))),i.set(t.slice(1),e)}set(t,e){if(Array.isArray(t))return this.recursiveSet(t,e);const i=e instanceof o.EditWrapper,s=i||this.editable,n=i||(0,r.IsMultyxClientItem)(e)?e.value:e;return void 0===n?this.delete(t,i):!(!i||!this.applyServerValue(t,n))||(this.object[t]instanceof a.default&&("object"!=typeof n||null===n)?this.object[t].set(i?new o.EditWrapper(n):n):s?(this.object[t]=new((0,l.default)(n))(this.multyx,i?new o.EditWrapper(n):n,[...this.propertyPath,t],this.editable),this.notifyPropertyWaiters(t),!0):(this.multyx.options.verbose&&console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join(".")+"."+t}' to ${n}`),!1))}delete(t,e=!1){return this.editable||e?(delete this.object[t],e||this.multyx.ws.send(n.Message.Native({instruction:"edit",path:[...this.propertyPath,t],value:void 0})),!0):(this.multyx.options.verbose&&console.error(`Attempting to delete property that is not editable. Deleting '${this.propertyPath.join(".")+"."+t}'`),!1)}keys(){return Object.keys(this.object)}values(){return Object.values(this.object)}entries(){const t=[];for(let e in this.object)t.push([e,this.get(e)]);return t}await(t){if(this.has(t))return Promise.resolve(this.get(t));const e=Symbol.for("_"+this.propertyPath.join(".")+"."+t);return new Promise(t=>this.multyx.on(e,t))}[o.Unpack](t){var e;for(const i in t)null===(e=this.object[i])||void 0===e||e[o.Unpack](t[i])}hydrateFromServer(t){if(!h(t))return;const e=new Set(Object.keys(this.object));for(const[i,s]of Object.entries(t))e.delete(i),this.set(i,new o.EditWrapper(s));for(const t of e)this.delete(t,!0)}applyServerValue(t,e){const i=this.object[t];if(!i)return!1;if(i instanceof a.default&&("object"!=typeof e||null===e))return i.set(new o.EditWrapper(e)),!0;const s="function"==typeof(null==i?void 0:i.hydrateFromServer);return(Array.isArray(e)&&s&&"list"===i.type||!(!h(e)||!s||"object"!==i.type))&&(i.hydrateFromServer(e),!0)}notifyPropertyWaiters(t){var e,i;const s=Symbol.for("_"+this.propertyPath.join(".")+"."+t);this.multyx.events.has(s)&&this.multyx[o.Done].push(...null!==(i=null===(e=this.multyx.events.get(s))||void 0===e?void 0:e.map(e=>()=>e(this.object[t])))&&void 0!==i?i:[])}}e.default=u},960:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.DefaultOptions=void 0,e.DefaultOptions={port:8443,secure:!1,uri:"localhost",verbose:!1,logUpdateFrame:!1}}},e={};function i(s){var n=e[s];if(void 0!==n)return n.exports;var o=e[s]={exports:{}};return t[s].call(o.exports,o,o.exports,i),o.exports}var s={};return(()=>{var t,e=s;const n=i(449),o=i(703),r=i(832),l=i(625),a=i(960);class h{constructor(e={},i){var s;if(this[t]=[],this.options=Object.assign(Object.assign({},a.DefaultOptions),e),!this.options.uri)throw new Error("URI is required");const o=`ws${this.options.secure?"s":""}://${this.options.uri.split("/")[0]}:${this.options.port}/${null!==(s=this.options.uri.split("/")[1])&&void 0!==s?s:""}`;this.ws=new WebSocket(o),this.ping=0,this.space="default",this.events=new Map,this.self={},this.tps=0,this.all={},this.teams=new l.MultyxClientObject(this,{},[],!0),this.clients={},this.controller=new r.Controller(this.ws),null==i||i(),this.ws.onmessage=t=>{var e,i,s,o;const r=n.Message.Parse(t.data);this.ping=2*(Date.now()-r.time),r.native?(this.parseNativeEvent(r),null===(e=this.events.get(h.Native))||void 0===e||e.forEach(t=>t(r))):(null===(i=this.events.get(r.name))||void 0===i||i.forEach(t=>{const e=t(r.data);void 0!==e&&this.send(r.name,e)}),null===(s=this.events.get(h.Custom))||void 0===s||s.forEach(t=>t(r))),null===(o=this.events.get(h.Any))||void 0===o||o.forEach(t=>t(r))}}on(t,e){var i;const s=null!==(i=this.events.get(t))&&void 0!==i?i:[];s.push(e),this.events.set(t,s)}send(t,e){"_"===t[0]&&(t="_"+t);const i={instruction:"resp",name:t,response:e};this.ws.send(n.Message.Native(i))}await(t,e){return this.send(t,e),new Promise(e=>this.events.set(Symbol.for("_"+t),[e]))}loop(t,e){if(e)this.on(h.Start,()=>setInterval(t,Math.round(1e3/e)));else{const e=()=>{t(),requestAnimationFrame(e)};this.on(h.Start,()=>requestAnimationFrame(e))}}[(t=o.Done,o.Add)](t){this[o.Done].push(t)}parseNativeEvent(t){var e,i,s,r,a;t.data=t.data.map(n.UncompressUpdate),this.options.logUpdateFrame&&console.log(t.data);for(const n of t.data)switch(n.instruction){case"init":this.initialize(n);for(const t of null!==(e=this.events.get(h.Start))&&void 0!==e?e:[])this[o.Done].push(()=>t(n));this.events.has(h.Start)&&(this.events.get(h.Start).length=0);break;case"edit":if(1==n.path.length)n.team?this.teams.set(n.path[0],new o.EditWrapper(n.value)):this.clients[n.path[0]]=new l.MultyxClientObject(this,new o.EditWrapper(n.value),[n.path[0]],!1);else{const t=n.team?this.teams.get(n.path[0]):this.clients[n.path[0]];if(!t)return;t.set(n.path.slice(1),new o.EditWrapper(n.value))}for(const t of null!==(i=this.events.get(h.Edit))&&void 0!==i?i:[])this[o.Done].push(()=>t(n));break;case"self":this.parseSelf(n);break;case"conn":this.clients[n.uuid]=new l.MultyxClientObject(this,n.data,[n.uuid],!1);for(const t of null!==(s=this.events.get(h.Connection))&&void 0!==s?s:[])this[o.Done].push(()=>t(this.clients[n.uuid]));break;case"dcon":for(const t of null!==(r=this.events.get(h.Disconnect))&&void 0!==r?r:[]){const e=this.clients[n.client].value;this[o.Done].push(()=>t(e))}delete this.clients[n.client];break;case"resp":{const t=null===(a=this.events.get(Symbol.for("_"+n.name)))||void 0===a?void 0:a[0];this.events.delete(Symbol.for("_"+n.name)),t&&this[o.Done].push(()=>t(n.response));break}default:this.options.verbose&&console.error("Server error: Unknown native Multyx instruction")}this[o.Done].forEach(t=>t()),this[o.Done].length=0}initialize(t){this.tps=t.tps,this.uuid=t.client.uuid,this.joinTime=t.client.joinTime,this.controller.listening=new Set(t.client.controller);for(const e of Object.keys(t.teams))this.teams[e]=new o.EditWrapper(t.teams[e]);this.all=this.teams.all,this.clients={};for(const[e,i]of Object.entries(t.clients))e!=this.uuid&&(this.clients[e]=new l.MultyxClientObject(this,new o.EditWrapper(i),[e],!1));const e=new l.MultyxClientObject(this,new o.EditWrapper(t.client.self),[this.uuid],!0);this.self=e,this.clients[this.uuid]=e;for(const[e,i]of Object.entries(t.constraintTable))(this.uuid==e?this.self:this.teams[e])[o.Unpack](i)}parseSelf(t){if("controller"==t.property)this.controller.listening=new Set(t.data);else if("uuid"==t.property)this.uuid=t.data;else if("constraint"==t.property){let e=this.uuid==t.data.path[0]?this.self:this.teams[t.data.path[0]];for(const i of t.data.path.slice(1))e=null==e?void 0:e[i];if(void 0===e)return;e[o.Unpack]({[t.data.name]:t.data.args})}else"space"==t.property&&(this.space=t.data,this.updateSpace())}updateSpace(){"default"!=this.space?document.querySelectorAll("[data-multyx-space]").forEach(t=>{t.style.display=t.dataset.multyxSpace==this.space?"block":"none",t.style.pointerEvents=t.dataset.multyxSpace==this.space?"auto":"none"}):document.querySelectorAll("[data-multyx-space]").forEach(t=>{t.style.display="block",t.style.pointerEvents="auto"})}}h.Start=Symbol("start"),h.Connection=Symbol("connection"),h.Disconnect=Symbol("disconnect"),h.Edit=Symbol("edit"),h.Native=Symbol("native"),h.Custom=Symbol("custom"),h.Any=Symbol("any"),h.Interpolate=o.Interpolate,e.default=h})(),s.default})());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multyx-client",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Framework designed to simplify the creation of multiplayer browser games by addressing the complexities of managing server-client communication, shared state, and input handling",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/controller.ts CHANGED
@@ -32,7 +32,7 @@ export class Controller {
32
32
  };
33
33
 
34
34
  document.addEventListener('keydown', e => {
35
- if(this.preventDefault) e.preventDefault;
35
+ if(this.preventDefault) e.preventDefault();
36
36
 
37
37
  const key = e.key.toLowerCase();
38
38
 
@@ -57,7 +57,7 @@ export class Controller {
57
57
 
58
58
  });
59
59
  document.addEventListener('keyup', e => {
60
- if(this.preventDefault) e.preventDefault;
60
+ if(this.preventDefault) e.preventDefault();
61
61
 
62
62
  const key = e.key.toLowerCase();
63
63
 
@@ -69,7 +69,7 @@ export class Controller {
69
69
 
70
70
  // Mouse input events
71
71
  document.addEventListener('mousedown', e => {
72
- if(this.preventDefault) e.preventDefault;
72
+ if(this.preventDefault) e.preventDefault();
73
73
 
74
74
  if(this.mouseGetter) {
75
75
  const mouse = this.mouseGetter();
@@ -86,7 +86,7 @@ export class Controller {
86
86
  });
87
87
  });
88
88
  document.addEventListener('mouseup', e => {
89
- if(this.preventDefault) e.preventDefault;
89
+ if(this.preventDefault) e.preventDefault();
90
90
 
91
91
  if(this.mouseGetter) {
92
92
  const mouse = this.mouseGetter();
@@ -103,7 +103,7 @@ export class Controller {
103
103
  });
104
104
  });
105
105
  document.addEventListener('mousemove', e => {
106
- if(this.preventDefault) e.preventDefault;
106
+ if(this.preventDefault) e.preventDefault();
107
107
 
108
108
  if(this.mouseGetter) {
109
109
  const mouse = this.mouseGetter();
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Message, UncompressUpdate } from "./message";
2
- import { Unpack, EditWrapper, Add, Edit, Done } from './utils';
2
+ import { Unpack, EditWrapper, Add, Done, Interpolate } from './utils';
3
3
  import { RawObject, ResponseUpdate } from "./types";
4
4
  import { Controller } from "./controller";
5
- import { MultyxClientObject, MultyxClientValue } from "./items";
5
+ import { MultyxClientObject } from "./items";
6
6
  import { DefaultOptions, Options } from "./options";
7
7
  export default class Multyx {
8
8
  ws: WebSocket;
@@ -31,6 +31,8 @@ export default class Multyx {
31
31
  static Custom = Symbol('custom');
32
32
  static Any = Symbol('any');
33
33
 
34
+ static Interpolate = Interpolate;
35
+
34
36
  constructor(options: Options = {}, callback?: () => void) {
35
37
  this.options = { ...DefaultOptions, ...options };
36
38
 
package/src/items/list.ts CHANGED
@@ -4,7 +4,10 @@ import { Add, Done, Edit, EditWrapper, Unpack } from '../utils';
4
4
  import MultyxClientItemRouter from './router';
5
5
  import { Message } from '../message';
6
6
 
7
+ const isPlainObject = (value: any) => value !== null && typeof value === 'object' && !Array.isArray(value);
8
+
7
9
  export default class MultyxClientList {
10
+ readonly type = 'list';
8
11
  protected list: MultyxClientItem[];
9
12
  private multyx: Multyx;
10
13
  propertyPath: string[];
@@ -157,9 +160,14 @@ export default class MultyxClientList {
157
160
  let next = this.get(parseInt(path[0]));
158
161
  if(next instanceof MultyxClientValue || next == undefined) {
159
162
  this.set(parseInt(path[0]), new EditWrapper({}));
160
- next = this.get(parseInt(path[0])) as MultyxClientObject;
163
+ next = this.get(parseInt(path[0]));
164
+ }
165
+
166
+ if(!next || next instanceof MultyxClientValue) {
167
+ return false;
161
168
  }
162
- return next.set(path.slice(1), value);
169
+
170
+ return (next as MultyxClientObject | MultyxClientList).set(path.slice(1), value);
163
171
  }
164
172
 
165
173
  set(index: number | string[], value: any): boolean {
@@ -169,41 +177,37 @@ export default class MultyxClientList {
169
177
 
170
178
  const serverSet = value instanceof EditWrapper;
171
179
  const allowed = serverSet || this.editable;
172
- if(serverSet || IsMultyxClientItem(value)) value = value.value;
173
- if(value === undefined) return this.delete(index, serverSet);
180
+ const incoming = (serverSet || IsMultyxClientItem(value)) ? value.value : value;
181
+ if(incoming === undefined) return this.delete(index, serverSet);
182
+
183
+ if(serverSet && this.tryApplyServerValue(index, incoming, oldValue)) {
184
+ return true;
185
+ }
174
186
 
175
- // If value is a MultyxClientValue, set the value
176
- if(this.list[index] instanceof MultyxClientValue && typeof value != 'object') {
177
- return this.list[index].set(serverSet ? new EditWrapper(value) : value);
187
+ // If value is a MultyxClientValue, set the value directly
188
+ if(this.list[index] instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
189
+ const result = this.list[index].set(serverSet ? new EditWrapper(incoming) : incoming);
190
+ this.enqueueEditCallbacks(index, oldValue);
191
+ return result;
178
192
  }
179
193
 
180
194
  // Attempting to edit property not editable to client
181
195
  if(!allowed) {
182
196
  if(this.multyx.options.verbose) {
183
- console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + index}' to ${value}`);
197
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + index}' to ${incoming}`);
184
198
  }
185
199
  return false;
186
200
  }
187
201
 
188
- this.list[index] = new (MultyxClientItemRouter(value))(
202
+ this.list[index] = new (MultyxClientItemRouter(incoming))(
189
203
  this.multyx,
190
- serverSet ? new EditWrapper(value) : value,
204
+ serverSet ? new EditWrapper(incoming) : incoming,
191
205
  [...this.propertyPath, index.toString()],
192
206
  this.editable
193
207
  );
194
208
 
195
- const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + index);
196
- if(this.multyx.events.has(propSymbol)) {
197
- this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
198
- () => e(this.list[index])
199
- ) ?? []));
200
- }
201
-
202
- // We have to push into queue, since object may not be fully created
203
- // and there may still be more updates to parse
204
- for(const listener of this.editCallbacks) {
205
- this.multyx[Add](() => listener(index, this.get(index), oldValue));
206
- }
209
+ this.notifyIndexWaiters(index);
210
+ this.enqueueEditCallbacks(index, oldValue);
207
211
 
208
212
  return true;
209
213
  }
@@ -452,4 +456,56 @@ export default class MultyxClientList {
452
456
  toString = () => this.value.toString();
453
457
  valueOf = () => this.value;
454
458
  [Symbol.toPrimitive] = () => this.value;
459
+
460
+ hydrateFromServer(values: any[]) {
461
+ if(!Array.isArray(values)) return;
462
+ for(let i=0; i<values.length; i++) {
463
+ this.set(i, new EditWrapper(values[i]));
464
+ }
465
+ for(let i=values.length; i<this.length; i++) {
466
+ this.delete(i, true);
467
+ }
468
+ this.length = values.length;
469
+ }
470
+
471
+ private tryApplyServerValue(index: number, incoming: any, oldValue: MultyxClientItem | undefined) {
472
+ const current = this.list[index];
473
+ if(!current) return false;
474
+
475
+ if(current instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
476
+ current.set(new EditWrapper(incoming));
477
+ this.enqueueEditCallbacks(index, oldValue);
478
+ return true;
479
+ }
480
+
481
+ const canHydrate = typeof (current as any)?.hydrateFromServer === 'function';
482
+ if(Array.isArray(incoming) && canHydrate && (current as any).type === 'list') {
483
+ (current as any).hydrateFromServer(incoming);
484
+ this.enqueueEditCallbacks(index, oldValue);
485
+ return true;
486
+ }
487
+
488
+ if(isPlainObject(incoming) && canHydrate && (current as any).type === 'object') {
489
+ (current as any).hydrateFromServer(incoming);
490
+ this.enqueueEditCallbacks(index, oldValue);
491
+ return true;
492
+ }
493
+
494
+ return false;
495
+ }
496
+
497
+ private notifyIndexWaiters(index: number) {
498
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + index);
499
+ if(this.multyx.events.has(propSymbol)) {
500
+ this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
501
+ () => e(this.list[index])
502
+ ) ?? []));
503
+ }
504
+ }
505
+
506
+ private enqueueEditCallbacks(index: number, oldValue: MultyxClientItem | undefined) {
507
+ for(const listener of this.editCallbacks) {
508
+ this.multyx[Add](() => listener(index, this.get(index), oldValue));
509
+ }
510
+ }
455
511
  }
@@ -7,7 +7,10 @@ import { IsMultyxClientItem, type MultyxClientList, type MultyxClientItem } from
7
7
  import MultyxClientItemRouter from "./router";
8
8
  import MultyxClientValue from "./value";
9
9
 
10
+ const isPlainObject = (value: any) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+
10
12
  export default class MultyxClientObject {
13
+ readonly type = 'object';
11
14
  protected object: RawObject<MultyxClientItem>;
12
15
  private multyx: Multyx;
13
16
  propertyPath: string[];
@@ -124,36 +127,35 @@ export default class MultyxClientObject {
124
127
 
125
128
  const serverSet = value instanceof EditWrapper;
126
129
  const allowed = serverSet || this.editable;
127
- if(serverSet || IsMultyxClientItem(value)) value = value.value;
128
- if(value === undefined) return this.delete(property, serverSet);
130
+ const incoming = (serverSet || IsMultyxClientItem(value)) ? value.value : value;
131
+ if(incoming === undefined) return this.delete(property, serverSet);
132
+
133
+ if(serverSet && this.applyServerValue(property, incoming)) {
134
+ return true;
135
+ }
129
136
 
130
137
  // Only create new MultyxClientItem when needed
131
- if(this.object[property] instanceof MultyxClientValue && typeof value != 'object') {
132
- return this.object[property].set(serverSet ? new EditWrapper(value) : value);
138
+ if(this.object[property] instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
139
+ return this.object[property].set(serverSet ? new EditWrapper(incoming) : incoming);
133
140
  }
134
141
 
135
142
  // Attempting to edit property not editable to client
136
143
  if(!allowed) {
137
144
  if(this.multyx.options.verbose) {
138
- console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${value}`);
145
+ console.error(`Attempting to set property that is not editable. Setting '${this.propertyPath.join('.') + '.' + property}' to ${incoming}`);
139
146
  }
140
147
  return false;
141
148
  }
142
149
 
143
150
  // Creating a new value
144
- this.object[property] = new (MultyxClientItemRouter(value))(
151
+ this.object[property] = new (MultyxClientItemRouter(incoming))(
145
152
  this.multyx,
146
- serverSet ? new EditWrapper(value) : value,
153
+ serverSet ? new EditWrapper(incoming) : incoming,
147
154
  [...this.propertyPath, property],
148
155
  this.editable
149
156
  );
150
157
 
151
- const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
152
- if(this.multyx.events.has(propSymbol)) {
153
- this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
154
- () => e(this.object[property])
155
- ) ?? []));
156
- }
158
+ this.notifyPropertyWaiters(property);
157
159
 
158
160
  return true;
159
161
  }
@@ -216,4 +218,48 @@ export default class MultyxClientObject {
216
218
  this.object[prop]?.[Unpack](constraints[prop]);
217
219
  }
218
220
  }
221
+
222
+ hydrateFromServer(value: RawObject) {
223
+ if(!isPlainObject(value)) return;
224
+ const remaining = new Set(Object.keys(this.object));
225
+ for(const [key, entry] of Object.entries(value)) {
226
+ remaining.delete(key);
227
+ this.set(key, new EditWrapper(entry));
228
+ }
229
+ for(const key of remaining) {
230
+ this.delete(key, true);
231
+ }
232
+ }
233
+
234
+ private applyServerValue(property: string, incoming: any) {
235
+ const current = this.object[property];
236
+ if(!current) return false;
237
+
238
+ if(current instanceof MultyxClientValue && (typeof incoming !== 'object' || incoming === null)) {
239
+ current.set(new EditWrapper(incoming));
240
+ return true;
241
+ }
242
+
243
+ const canHydrate = typeof (current as any)?.hydrateFromServer === 'function';
244
+ if(Array.isArray(incoming) && canHydrate && (current as any).type === 'list') {
245
+ (current as any).hydrateFromServer(incoming);
246
+ return true;
247
+ }
248
+
249
+ if(isPlainObject(incoming) && canHydrate && (current as any).type === 'object') {
250
+ (current as any).hydrateFromServer(incoming);
251
+ return true;
252
+ }
253
+
254
+ return false;
255
+ }
256
+
257
+ private notifyPropertyWaiters(property: string) {
258
+ const propSymbol = Symbol.for("_" + this.propertyPath.join('.') + '.' + property);
259
+ if(this.multyx.events.has(propSymbol)) {
260
+ this.multyx[Done].push(...(this.multyx.events.get(propSymbol)?.map(e =>
261
+ () => e(this.object[property])
262
+ ) ?? []));
263
+ }
264
+ }
219
265
  }
@@ -3,6 +3,10 @@ import { Message } from "../message";
3
3
  import { Constraint, RawObject, Value } from "../types";
4
4
  import { BuildConstraint, Done, Edit, EditWrapper, Unpack } from '../utils';
5
5
 
6
+ type InterpolationMode = 'lerp' | 'predictive';
7
+ type NumericSample = { value: number, time: number };
8
+ const DEFAULT_INTERPOLATION_FRAME_MS = 250;
9
+
6
10
  export default class MultyxClientValue {
7
11
  private _value: Value;
8
12
  private multyx: Multyx;
@@ -17,8 +21,14 @@ export default class MultyxClientValue {
17
21
  return this.readModifiers.reduce((value, modifier) => modifier(value), this._value);
18
22
  }
19
23
 
24
+ private interpolationModifier?: (value: Value) => Value;
25
+ private latestSample?: NumericSample;
26
+ private previousSample?: NumericSample;
27
+ private interpolationFrameMs: number = DEFAULT_INTERPOLATION_FRAME_MS;
28
+
20
29
  set value(v) {
21
30
  this._value = v;
31
+ this.captureSample(v);
22
32
  }
23
33
 
24
34
  addReadModifier(modifier: (value: Value) => Value) {
@@ -79,7 +89,7 @@ export default class MultyxClientValue {
79
89
  }
80
90
  }
81
91
 
82
- if(this.value === nv) {
92
+ if(this._value === nv) {
83
93
  this.value = nv;
84
94
  return true;
85
95
  }
@@ -118,4 +128,68 @@ export default class MultyxClientValue {
118
128
  toString = () => this.value.toString();
119
129
  valueOf = () => this.value;
120
130
  [Symbol.toPrimitive] = () => this.value;
131
+
132
+ Lerp(maxFrameDuration: number = DEFAULT_INTERPOLATION_FRAME_MS) {
133
+ return this.applyInterpolation('lerp', maxFrameDuration);
134
+ }
135
+
136
+ PredictiveLerp(maxFrameDuration: number = DEFAULT_INTERPOLATION_FRAME_MS) {
137
+ return this.applyInterpolation('predictive', maxFrameDuration);
138
+ }
139
+
140
+ private applyInterpolation(mode: InterpolationMode, maxFrameDuration: number) {
141
+ if(typeof this._value !== 'number' || Number.isNaN(this._value)) {
142
+ throw new Error(`MultyxClientValue.${mode === 'lerp' ? 'Lerp' : 'PredictiveLerp'} can only be applied to numeric values`);
143
+ }
144
+
145
+ this.interpolationFrameMs = Math.max(1, maxFrameDuration);
146
+ this.attachInterpolationModifier(mode);
147
+ return this;
148
+ }
149
+
150
+ private attachInterpolationModifier(mode: InterpolationMode) {
151
+ if(this.interpolationModifier) {
152
+ this.readModifiers = this.readModifiers.filter(fn => fn !== this.interpolationModifier);
153
+ }
154
+
155
+ this.interpolationModifier = (value: Value) => this.interpolateValue(value, mode);
156
+ this.readModifiers.push(this.interpolationModifier);
157
+ }
158
+
159
+ private captureSample(value: Value) {
160
+ if(typeof value !== 'number' || Number.isNaN(value)) {
161
+ this.latestSample = undefined;
162
+ this.previousSample = undefined;
163
+ return;
164
+ }
165
+
166
+ const now = Date.now();
167
+ if(!this.latestSample) {
168
+ this.latestSample = { value, time: now };
169
+ return;
170
+ }
171
+
172
+ this.previousSample = { ...this.latestSample };
173
+ this.latestSample = { value, time: now };
174
+ }
175
+
176
+ private interpolateValue(baseValue: Value, mode: InterpolationMode): Value {
177
+ if(typeof baseValue !== 'number' || !this.latestSample || !this.previousSample) {
178
+ return baseValue;
179
+ }
180
+
181
+ const durationRaw = this.latestSample.time - this.previousSample.time;
182
+ if(durationRaw <= 0) return baseValue;
183
+
184
+ const duration = Math.max(1, Math.min(durationRaw, this.interpolationFrameMs));
185
+ const elapsed = Math.max(0, Math.min(Date.now() - this.latestSample.time, duration));
186
+ const ratio = duration === 0 ? 1 : elapsed / duration;
187
+ const delta = this.latestSample.value - this.previousSample.value;
188
+
189
+ if(mode === 'predictive') {
190
+ return this.latestSample.value + delta * ratio;
191
+ }
192
+
193
+ return this.previousSample.value + delta * ratio;
194
+ }
121
195
  }
package/src/message.ts CHANGED
@@ -15,8 +15,8 @@ export function UncompressUpdate(str: string) {
15
15
  if(instruction == '9') return { instruction: 'self', property: "space", data: JSON.parse(specifier) };
16
16
 
17
17
  if(instruction == '5') return { instruction: 'resp', name: specifier, response: data[0] };
18
- if(instruction == '6') return { instruction: 'conn', uuid: specifier, publicData: data[0] };
19
- if(instruction == '7') return { instruction: 'dcon', clientUUID: specifier };
18
+ if(instruction == '6') return { instruction: 'conn', uuid: specifier, data: data[0] };
19
+ if(instruction == '7') return { instruction: 'dcon', client: specifier };
20
20
 
21
21
  if(instruction == '8') return {
22
22
  instruction: 'init',
@@ -45,7 +45,7 @@ export function CompressUpdate(update: Update) {
45
45
  pieces = [update.name, JSON.stringify(update.response)];
46
46
  }
47
47
 
48
- if(!pieces || !code) return '';
48
+ if(!pieces || code === undefined) return '';
49
49
  let compressed = code.toString();
50
50
  for(let i = 0; i < pieces.length; i++) {
51
51
  compressed += pieces[i].replace(/;/g, ';_');
package/src/utils.ts CHANGED
@@ -36,34 +36,65 @@ export function Interpolate(
36
36
  progress: number,
37
37
  }[]
38
38
  ) {
39
+ if(!Array.isArray(interpolationCurve) || interpolationCurve.length === 0) {
40
+ throw new Error('Interpolation curve must contain at least one slice');
41
+ }
42
+
43
+ const curve = [...interpolationCurve].sort((a, b) => a.time - b.time);
44
+ const curveMaxTime = curve[curve.length - 1].time;
45
+ const usesNormalizedCurve = curveMaxTime <= 1;
46
+ const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
47
+
39
48
  let start = { value: object[property], time: Date.now() };
40
49
  let end = { value: object[property], time: Date.now() };
41
50
 
42
51
  Object.defineProperty(object, property, {
52
+ configurable: true,
53
+ enumerable: true,
43
54
  get: () => {
44
- const time = end.time - start.time;
45
- let lower = interpolationCurve[0];
46
- let upper = interpolationCurve[0];
55
+ if(start.time === end.time) return end.value;
56
+ const now = Date.now();
57
+ const duration = Math.max(end.time - start.time, 1);
58
+ const elapsed = Math.max(0, now - end.time);
59
+ const targetTime = usesNormalizedCurve
60
+ ? clamp(elapsed / duration, 0, curveMaxTime)
61
+ : clamp(elapsed, 0, curveMaxTime);
47
62
 
48
- for(const slice of interpolationCurve) {
49
- if(time > slice.time && slice.time > lower.time) lower = slice;
50
- if(time < slice.time && slice.time < upper.time) upper = slice;
63
+ let lower = curve[0];
64
+ let upper = curve[curve.length - 1];
65
+ for(const slice of curve) {
66
+ if(slice.time <= targetTime) {
67
+ lower = slice;
68
+ continue;
69
+ }
70
+ upper = slice;
71
+ break;
51
72
  }
52
73
 
53
- const sliceTime = (time - lower.time) / (upper.time - lower.time);
54
- const ratio = lower.progress + sliceTime * (upper.progress - lower.progress);
74
+ let ratio: number;
75
+ if(upper.time === lower.time) {
76
+ ratio = lower.progress;
77
+ } else {
78
+ const sliceTime = (targetTime - lower.time) / (upper.time - lower.time);
79
+ ratio = lower.progress + sliceTime * (upper.progress - lower.progress);
80
+ }
55
81
 
56
82
  if(Number.isNaN(ratio)) return start.value;
57
- return end.value * ratio + start.value * (1 - ratio);
83
+ if(typeof start.value === 'number' && typeof end.value === 'number') {
84
+ return end.value * ratio + start.value * (1 - ratio);
85
+ }
86
+ return ratio >= 1 ? end.value : start.value;
58
87
  },
59
88
  set: (value) => {
89
+ const now = Date.now();
60
90
  // Don't lerp between edit requests sent in same frame
61
- if(Date.now() - end.time < 10) {
91
+ if(now - end.time < 10) {
62
92
  end.value = value;
93
+ end.time = now;
63
94
  return true;
64
95
  }
65
96
  start = { ...end };
66
- end = { value, time: Date.now() }
97
+ end = { value, time: now };
67
98
  return true;
68
99
  }
69
100
  });