@bloopjs/bloop 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jsr.json +1 -1
- package/package.json +6 -2
- package/src/bloop.ts +239 -0
- package/src/context.ts +6 -68
- package/src/events.ts +25 -0
- package/src/mod.ts +3 -200
- package/src/runtime.ts +273 -0
- package/src/system.ts +5 -5
- package/src/util.ts +23 -0
- package/test/inputs.test.ts +131 -66
- package/test/runtime.test.ts +298 -0
- package/test/tape.test.ts +57 -9
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_WASM_URL,
|
|
3
|
+
type EnginePointer,
|
|
4
|
+
type Key,
|
|
5
|
+
keyToKeyCode,
|
|
6
|
+
type MouseButton,
|
|
7
|
+
mouseButtonToMouseButtonCode,
|
|
8
|
+
SNAPSHOT_HEADER_ENGINE_LEN_OFFSET,
|
|
9
|
+
SNAPSHOT_HEADER_LEN,
|
|
10
|
+
SNAPSHOT_HEADER_USER_LEN_OFFSET,
|
|
11
|
+
TimeContext,
|
|
12
|
+
type WasmEngine,
|
|
13
|
+
} from "@bloopjs/engine";
|
|
14
|
+
import { assert } from "./util";
|
|
15
|
+
|
|
16
|
+
export type MountOpts = {
|
|
17
|
+
wasmUrl?: URL;
|
|
18
|
+
/**
|
|
19
|
+
* A callback function to run logic for a given frame
|
|
20
|
+
*/
|
|
21
|
+
systemsCallback: (system_handle: number, ptr: EnginePointer) => void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sets buffer to the latest engine memory buffer
|
|
25
|
+
*
|
|
26
|
+
* Note that if the engine wasm memory grows, all dataviews into the memory must be updated
|
|
27
|
+
*/
|
|
28
|
+
setBuffer: (buffer: ArrayBuffer) => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether to start recording immediately upon mount
|
|
32
|
+
* Defaults to true
|
|
33
|
+
*/
|
|
34
|
+
startRecording?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optional hook to serialize some data when snapshotting
|
|
38
|
+
*/
|
|
39
|
+
serialize?: SerializeFn;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional hook to deserialize some data when restoring
|
|
43
|
+
*/
|
|
44
|
+
deserialize?: DeserializeFn;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type SerializeFn = () => {
|
|
48
|
+
size: number;
|
|
49
|
+
write(buffer: ArrayBufferLike, ptr: EnginePointer): void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DeserializeFn = (
|
|
53
|
+
buffer: ArrayBufferLike,
|
|
54
|
+
ptr: EnginePointer,
|
|
55
|
+
size: number,
|
|
56
|
+
) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The runtime is a portable runtime that is responsible for:
|
|
60
|
+
*
|
|
61
|
+
* * Moving the engine forward and backward in time
|
|
62
|
+
* * Maintaining a js-friendly view of engine memory
|
|
63
|
+
*/
|
|
64
|
+
export class Runtime {
|
|
65
|
+
wasm: WasmEngine;
|
|
66
|
+
#memory: WebAssembly.Memory;
|
|
67
|
+
#time: TimeContext;
|
|
68
|
+
#serialize?: SerializeFn;
|
|
69
|
+
constructor(
|
|
70
|
+
wasm: WasmEngine,
|
|
71
|
+
memory: WebAssembly.Memory,
|
|
72
|
+
opts?: { serialize?: SerializeFn },
|
|
73
|
+
) {
|
|
74
|
+
this.wasm = wasm;
|
|
75
|
+
this.#memory = memory;
|
|
76
|
+
this.#time = new TimeContext(
|
|
77
|
+
new DataView(this.#memory.buffer, this.wasm.get_time_ctx()),
|
|
78
|
+
);
|
|
79
|
+
this.#serialize = opts?.serialize;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
step(ms?: number) {
|
|
83
|
+
this.wasm.step(ms ?? 16);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stepBack() {
|
|
87
|
+
if (this.time.frame === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.seek(this.time.frame - 1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
seek(frame: number) {
|
|
94
|
+
assert(
|
|
95
|
+
this.hasHistory,
|
|
96
|
+
"Not recording or playing back, can't seek to frame",
|
|
97
|
+
);
|
|
98
|
+
this.wasm.seek(frame);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
record() {
|
|
102
|
+
const serializer = this.#serialize ? this.#serialize() : null;
|
|
103
|
+
const size = serializer ? serializer.size : 0;
|
|
104
|
+
this.wasm.start_recording(size, 1024);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
snapshot(): Uint8Array<ArrayBuffer> {
|
|
108
|
+
const serializer = this.#serialize ? this.#serialize() : null;
|
|
109
|
+
const size = serializer ? serializer.size : 0;
|
|
110
|
+
|
|
111
|
+
const ptr = this.wasm.take_snapshot(size);
|
|
112
|
+
const header = new Uint32Array(this.#memory.buffer, ptr, 4);
|
|
113
|
+
const userLenIndex =
|
|
114
|
+
SNAPSHOT_HEADER_USER_LEN_OFFSET / Uint32Array.BYTES_PER_ELEMENT;
|
|
115
|
+
const engineLenIndex =
|
|
116
|
+
SNAPSHOT_HEADER_ENGINE_LEN_OFFSET / Uint32Array.BYTES_PER_ELEMENT;
|
|
117
|
+
assert(header[userLenIndex], `header user length missing`);
|
|
118
|
+
assert(header[engineLenIndex], `header engine length missing`);
|
|
119
|
+
const length = header[userLenIndex] + header[engineLenIndex];
|
|
120
|
+
const memoryView = new Uint8Array(this.#memory.buffer, ptr, length);
|
|
121
|
+
|
|
122
|
+
const copy = new Uint8Array(length);
|
|
123
|
+
copy.set(memoryView);
|
|
124
|
+
|
|
125
|
+
return copy;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
restore(snapshot: Uint8Array) {
|
|
129
|
+
const dataPtr = this.wasm.alloc(snapshot.byteLength);
|
|
130
|
+
assert(
|
|
131
|
+
dataPtr > 0,
|
|
132
|
+
`failed to allocate ${snapshot.byteLength} bytes for snapshot restore, pointer=${dataPtr}`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// copy snapshot into wasm memory
|
|
136
|
+
const memoryView = new Uint8Array(
|
|
137
|
+
this.#memory.buffer,
|
|
138
|
+
dataPtr,
|
|
139
|
+
snapshot.byteLength,
|
|
140
|
+
);
|
|
141
|
+
memoryView.set(snapshot);
|
|
142
|
+
|
|
143
|
+
// restore the snapshot
|
|
144
|
+
this.wasm.restore(dataPtr);
|
|
145
|
+
|
|
146
|
+
// free the allocated memory
|
|
147
|
+
this.wasm.free(dataPtr, snapshot.byteLength);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get time(): TimeContext {
|
|
151
|
+
if (
|
|
152
|
+
!this.#time.dataView ||
|
|
153
|
+
this.#time.dataView.buffer !== this.#memory.buffer
|
|
154
|
+
) {
|
|
155
|
+
// update the data view to the latest memory buffer
|
|
156
|
+
this.#time.dataView = new DataView(
|
|
157
|
+
this.#memory.buffer,
|
|
158
|
+
this.wasm.get_time_ctx(),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return this.#time;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get buffer(): ArrayBuffer {
|
|
165
|
+
return this.#memory.buffer;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get isRecording(): boolean {
|
|
169
|
+
return this.wasm.is_recording();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get isReplaying(): boolean {
|
|
173
|
+
return this.wasm.is_replaying();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get hasHistory(): boolean {
|
|
177
|
+
return this.isRecording || this.isReplaying;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
emit = {
|
|
181
|
+
keydown: (key: Key): void => {
|
|
182
|
+
this.wasm.emit_keydown(keyToKeyCode(key));
|
|
183
|
+
},
|
|
184
|
+
keyup: (key: Key): void => {
|
|
185
|
+
this.wasm.emit_keyup(keyToKeyCode(key));
|
|
186
|
+
},
|
|
187
|
+
mousemove: (x: number, y: number): void => {
|
|
188
|
+
this.wasm.emit_mousemove(x, y);
|
|
189
|
+
},
|
|
190
|
+
mousedown: (button: MouseButton): void => {
|
|
191
|
+
this.wasm.emit_mousedown(mouseButtonToMouseButtonCode(button));
|
|
192
|
+
},
|
|
193
|
+
mouseup: (button: MouseButton): void => {
|
|
194
|
+
this.wasm.emit_mouseup(mouseButtonToMouseButtonCode(button));
|
|
195
|
+
},
|
|
196
|
+
mousewheel: (x: number, y: number): void => {
|
|
197
|
+
this.wasm.emit_mousewheel(x, y);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type MountResult = {
|
|
203
|
+
runtime: Runtime;
|
|
204
|
+
wasm: WasmEngine;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export async function mount(opts: MountOpts): Promise<MountResult> {
|
|
208
|
+
if (
|
|
209
|
+
(opts.serialize && !opts.deserialize) ||
|
|
210
|
+
(!opts.serialize && opts.deserialize)
|
|
211
|
+
) {
|
|
212
|
+
throw new Error("Snapshot and restore hooks must be provided together");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// https://github.com/oven-sh/bun/issues/12434
|
|
216
|
+
const bytes = await Bun.file(opts.wasmUrl ?? DEFAULT_WASM_URL).arrayBuffer();
|
|
217
|
+
|
|
218
|
+
// 1mb to 64mb
|
|
219
|
+
// use bun check:wasm to find initial memory page size
|
|
220
|
+
const memory = new WebAssembly.Memory({ initial: 17, maximum: 1000 });
|
|
221
|
+
const wasmInstantiatedSource = await WebAssembly.instantiate(bytes, {
|
|
222
|
+
env: {
|
|
223
|
+
memory,
|
|
224
|
+
__cb: function (system_handle: number, ptr: number) {
|
|
225
|
+
opts.setBuffer(memory.buffer);
|
|
226
|
+
opts.systemsCallback(system_handle, ptr);
|
|
227
|
+
},
|
|
228
|
+
console_log: function (ptr: EnginePointer, len: number) {
|
|
229
|
+
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
|
230
|
+
const string = new TextDecoder("utf-8").decode(bytes);
|
|
231
|
+
console.log(string);
|
|
232
|
+
},
|
|
233
|
+
user_data_len: function () {
|
|
234
|
+
const serializer = opts.serialize ? opts.serialize() : null;
|
|
235
|
+
return serializer ? serializer.size : 0;
|
|
236
|
+
},
|
|
237
|
+
user_data_serialize: function (ptr: EnginePointer, len: number) {
|
|
238
|
+
if (!opts.serialize) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const serializer = opts.serialize();
|
|
242
|
+
if (len !== serializer.size) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`user_data_write length mismatch, expected=${serializer.size} got=${len}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
serializer.write(memory.buffer, ptr);
|
|
248
|
+
},
|
|
249
|
+
user_data_deserialize: function (ptr: EnginePointer, len: number) {
|
|
250
|
+
if (!opts.deserialize) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
opts.deserialize(memory.buffer, ptr, len);
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const wasm = wasmInstantiatedSource.instance.exports as WasmEngine;
|
|
258
|
+
|
|
259
|
+
wasm.initialize();
|
|
260
|
+
|
|
261
|
+
const runtime = new Runtime(wasm, memory, {
|
|
262
|
+
serialize: opts.serialize,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (opts.startRecording ?? true) {
|
|
266
|
+
runtime.record();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
runtime,
|
|
271
|
+
wasm,
|
|
272
|
+
};
|
|
273
|
+
}
|
package/src/system.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import type { Context } from "./context";
|
|
2
|
+
import type { BloopSchema } from "./data/schema";
|
|
1
3
|
import type {
|
|
2
4
|
KeyEvent,
|
|
3
5
|
MouseButtonEvent,
|
|
4
|
-
|
|
6
|
+
MouseMoveEvent,
|
|
5
7
|
MouseWheelEvent,
|
|
6
|
-
} from "
|
|
7
|
-
import type { Context } from "./context";
|
|
8
|
-
import type { BloopSchema } from "./data/schema";
|
|
8
|
+
} from "./events";
|
|
9
9
|
|
|
10
10
|
export type System<GS extends BloopSchema = BloopSchema> = {
|
|
11
11
|
label?: string;
|
|
@@ -38,7 +38,7 @@ export type System<GS extends BloopSchema = BloopSchema> = {
|
|
|
38
38
|
|
|
39
39
|
mousemove?: (
|
|
40
40
|
context: Context<GS> & {
|
|
41
|
-
event:
|
|
41
|
+
event: MouseMoveEvent;
|
|
42
42
|
},
|
|
43
43
|
) => void;
|
|
44
44
|
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function toHexString(dataView: DataView, length?: number): string {
|
|
2
|
+
length ??= dataView.byteLength;
|
|
3
|
+
let hexString = "";
|
|
4
|
+
for (let i = 0; i < length; i++) {
|
|
5
|
+
const byte = dataView.getUint8(i);
|
|
6
|
+
hexString += `${byte.toString(16).padStart(2, "0")} `;
|
|
7
|
+
}
|
|
8
|
+
return hexString.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assert(condition: any, message?: string): asserts condition {
|
|
12
|
+
if (condition == null || condition === false) {
|
|
13
|
+
throw new Error(message ?? "Assertion failed");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function unwrap<T>(
|
|
18
|
+
value: T | null | undefined,
|
|
19
|
+
message?: string,
|
|
20
|
+
): NonNullable<T> {
|
|
21
|
+
assert(value != null, message ?? `Unwrap failed: value is ${value}`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
package/test/inputs.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Bloop } from "../src/
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { Key, MouseButton } from "@bloopjs/engine";
|
|
3
|
+
import { Bloop } from "../src/bloop";
|
|
4
|
+
import { mount } from "../src/runtime";
|
|
4
5
|
|
|
5
|
-
describe("
|
|
6
|
+
describe("inputs", () => {
|
|
6
7
|
it("runs a single system", async () => {
|
|
7
8
|
const bloop = Bloop.create();
|
|
8
9
|
let count = 0;
|
|
@@ -19,7 +20,26 @@ describe("loop", () => {
|
|
|
19
20
|
expect(count).toEqual(1);
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
it("
|
|
23
|
+
it("routes keydown event", async () => {
|
|
24
|
+
const bloop = Bloop.create();
|
|
25
|
+
let receivedKey: Key | null = null;
|
|
26
|
+
let called = false;
|
|
27
|
+
bloop.system("input", {
|
|
28
|
+
keydown({ event }) {
|
|
29
|
+
receivedKey = event.key;
|
|
30
|
+
called = true;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { runtime } = await mount(bloop);
|
|
35
|
+
runtime.emit.keydown("Space");
|
|
36
|
+
runtime.step();
|
|
37
|
+
|
|
38
|
+
expect(called).toBe(true);
|
|
39
|
+
expect(receivedKey!).toEqual("Space");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("routes all input events", async () => {
|
|
23
43
|
const bloop = Bloop.create();
|
|
24
44
|
|
|
25
45
|
const events = {
|
|
@@ -53,12 +73,12 @@ describe("loop", () => {
|
|
|
53
73
|
},
|
|
54
74
|
});
|
|
55
75
|
|
|
56
|
-
const { runtime
|
|
76
|
+
const { runtime } = await mount(bloop);
|
|
57
77
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
runtime.emit.keydown("Space");
|
|
79
|
+
runtime.emit.mousedown("Left");
|
|
80
|
+
runtime.emit.mousemove(100, 150);
|
|
81
|
+
runtime.emit.mousewheel(1, 2);
|
|
62
82
|
|
|
63
83
|
runtime.step();
|
|
64
84
|
|
|
@@ -67,9 +87,9 @@ describe("loop", () => {
|
|
|
67
87
|
expect(events.mousedown).toEqual("Left");
|
|
68
88
|
expect(events.mousewheel).toEqual({ x: 1, y: 2 });
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
runtime.emit.keyup("Space");
|
|
91
|
+
runtime.emit.mouseup("Left");
|
|
92
|
+
runtime.emit.mousemove(3, 4);
|
|
73
93
|
|
|
74
94
|
runtime.step();
|
|
75
95
|
|
|
@@ -78,82 +98,127 @@ describe("loop", () => {
|
|
|
78
98
|
expect(events.mousemove).toEqual({ x: 3, y: 4 });
|
|
79
99
|
});
|
|
80
100
|
|
|
81
|
-
it("
|
|
101
|
+
it("exposes keyboard context", async () => {
|
|
82
102
|
const bloop = Bloop.create({
|
|
83
103
|
bag: {
|
|
84
|
-
|
|
104
|
+
down: null as boolean | null,
|
|
105
|
+
held: null as boolean | null,
|
|
106
|
+
up: null as boolean | null,
|
|
85
107
|
},
|
|
86
108
|
});
|
|
87
109
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
mousedown: null as boolean | null,
|
|
94
|
-
mouseup: null as boolean | null,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
bloop.system("input snapshots", {
|
|
98
|
-
update({ inputs }) {
|
|
99
|
-
events.keydown = inputs.keys.space.down;
|
|
100
|
-
events.keyheld = inputs.keys.space.held;
|
|
101
|
-
events.keyup = inputs.keys.space.up;
|
|
102
|
-
|
|
103
|
-
events.mousedown = inputs.mouse.left.down;
|
|
104
|
-
events.mouseheld = inputs.mouse.left.held;
|
|
105
|
-
events.mouseup = inputs.mouse.left.up;
|
|
110
|
+
bloop.system("key state", {
|
|
111
|
+
update({ inputs, bag }) {
|
|
112
|
+
bag.down = inputs.keys.backquote.down;
|
|
113
|
+
bag.held = inputs.keys.backquote.held;
|
|
114
|
+
bag.up = inputs.keys.backquote.up;
|
|
106
115
|
},
|
|
107
116
|
});
|
|
108
117
|
|
|
109
|
-
const { runtime
|
|
118
|
+
const { runtime } = await mount(bloop);
|
|
110
119
|
|
|
111
120
|
// Initial state
|
|
112
121
|
runtime.step();
|
|
113
|
-
expect(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
mousedown: false,
|
|
118
|
-
mouseheld: false,
|
|
119
|
-
mouseup: false,
|
|
122
|
+
expect(bloop.bag).toEqual({
|
|
123
|
+
down: false,
|
|
124
|
+
held: false,
|
|
125
|
+
up: false,
|
|
120
126
|
});
|
|
121
127
|
|
|
122
128
|
// down and held are both true on the first frame of a key down
|
|
123
|
-
|
|
124
|
-
emitter.mousedown("Left");
|
|
129
|
+
runtime.emit.keydown("Backquote");
|
|
125
130
|
runtime.step();
|
|
126
|
-
expect(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
mousedown: true,
|
|
131
|
-
mouseheld: true,
|
|
132
|
-
mouseup: false,
|
|
131
|
+
expect(bloop.bag).toEqual({
|
|
132
|
+
down: true,
|
|
133
|
+
held: true,
|
|
134
|
+
up: false,
|
|
133
135
|
});
|
|
134
136
|
|
|
135
137
|
// held remains true, down goes false
|
|
136
138
|
runtime.step();
|
|
137
|
-
expect(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
mousedown: false,
|
|
142
|
-
mouseheld: true,
|
|
143
|
-
mouseup: false,
|
|
139
|
+
expect(bloop.bag).toEqual({
|
|
140
|
+
down: false,
|
|
141
|
+
held: true,
|
|
142
|
+
up: false,
|
|
144
143
|
});
|
|
145
144
|
|
|
146
145
|
// on key up, up is true, held and down are false
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
runtime.emit.keyup("Backquote");
|
|
147
|
+
runtime.step();
|
|
148
|
+
expect(bloop.bag).toEqual({
|
|
149
|
+
down: false,
|
|
150
|
+
held: false,
|
|
151
|
+
up: true,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("exposes mouse context", async () => {
|
|
156
|
+
const bloop = Bloop.create({
|
|
157
|
+
bag: {
|
|
158
|
+
down: null as boolean | null,
|
|
159
|
+
held: null as boolean | null,
|
|
160
|
+
up: null as boolean | null,
|
|
161
|
+
position: null as { x: number; y: number } | null,
|
|
162
|
+
wheel: null as { x: number; y: number } | null,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
bloop.system("mouse state", {
|
|
167
|
+
update({ inputs, bag }) {
|
|
168
|
+
bag.down = inputs.mouse.left.down;
|
|
169
|
+
bag.held = inputs.mouse.left.held;
|
|
170
|
+
bag.up = inputs.mouse.left.up;
|
|
171
|
+
bag.position = { x: inputs.mouse.x, y: inputs.mouse.y };
|
|
172
|
+
bag.wheel = inputs.mouse.wheel;
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const { runtime } = await mount(bloop);
|
|
177
|
+
|
|
178
|
+
// Initial state
|
|
179
|
+
runtime.step();
|
|
180
|
+
expect(bloop.bag).toEqual({
|
|
181
|
+
down: false,
|
|
182
|
+
held: false,
|
|
183
|
+
up: false,
|
|
184
|
+
position: { x: 0, y: 0 },
|
|
185
|
+
wheel: { x: 0, y: 0 },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// down and held are both true on the first frame of a key down
|
|
189
|
+
runtime.emit.mousedown("Left");
|
|
190
|
+
runtime.step();
|
|
191
|
+
expect(bloop.bag).toMatchObject({
|
|
192
|
+
down: true,
|
|
193
|
+
held: true,
|
|
194
|
+
up: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
runtime.emit.mousemove(123, 456);
|
|
198
|
+
runtime.step();
|
|
199
|
+
expect(bloop.bag).toMatchObject({
|
|
200
|
+
down: false,
|
|
201
|
+
held: true,
|
|
202
|
+
up: false,
|
|
203
|
+
position: { x: 123, y: 456 },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
runtime.emit.mousewheel(5, -3);
|
|
207
|
+
runtime.step();
|
|
208
|
+
expect(bloop.bag).toMatchObject({
|
|
209
|
+
down: false,
|
|
210
|
+
held: true,
|
|
211
|
+
up: false,
|
|
212
|
+
position: { x: 123, y: 456 },
|
|
213
|
+
wheel: { x: 5, y: -3 },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
runtime.emit.mouseup("Left");
|
|
149
217
|
runtime.step();
|
|
150
|
-
expect(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
mousedown: false,
|
|
155
|
-
mouseheld: false,
|
|
156
|
-
mouseup: true,
|
|
218
|
+
expect(bloop.bag).toMatchObject({
|
|
219
|
+
down: false,
|
|
220
|
+
held: false,
|
|
221
|
+
up: true,
|
|
157
222
|
});
|
|
158
223
|
});
|
|
159
224
|
|