Skip to content

Commit 894a759

Browse files
authored
chore: implement missing UTs & rework DI/adapters (#62)
* chore: implement missing UTs & rework DI/adapters * refactor(engine/controls): add UT & use DI/adapters
1 parent 764e4c0 commit 894a759

26 files changed

+1497
-132
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Place all constants directly **beneath the imports** under a `// CONSTANTS` com
6969
* Cover edge cases and error handling.
7070
* **Never** modify production code solely to ease testing.
7171
* For browser‑like APIs, mock the DOM with **`happy-dom`**.
72+
* Always try to type variables such as mocks.
7273

7374
## 6  User Interaction (for Copilot / LLM)
7475

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface CanvasAdapter {
2+
requestFullscreen(): void;
3+
exitFullscreen(): void;
4+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Import Internal Dependencies
2+
import type {
3+
EventTargetAdapter,
4+
EventTargetListener
5+
} from "./eventTarget.js";
6+
7+
export interface DocumentAdapter extends EventTargetAdapter {
8+
fullscreenElement?: Element | null;
9+
10+
exitFullscreen(): void;
11+
}
12+
13+
export class BrowserDocumentAdapter implements DocumentAdapter {
14+
addEventListener(
15+
type: string,
16+
listener: EventTargetListener
17+
) {
18+
document.addEventListener(type, listener);
19+
}
20+
21+
removeEventListener(
22+
type: string,
23+
listener: EventTargetListener
24+
) {
25+
document.removeEventListener(type, listener);
26+
}
27+
28+
exitFullscreen() {
29+
if (document.fullscreenElement) {
30+
document.exitFullscreen();
31+
}
32+
}
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type EventTargetListener = (...args: any[]) => void | boolean;
2+
3+
export interface EventTargetAdapter {
4+
addEventListener(
5+
type: string,
6+
listener: EventTargetListener,
7+
options?: boolean | AddEventListenerOptions
8+
): void;
9+
removeEventListener(
10+
type: string,
11+
listener: EventTargetListener,
12+
options?: boolean | AddEventListenerOptions
13+
): void;
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Import Internal Dependencies
2+
import {
3+
GameInstance
4+
} from "../systems/GameInstance.js";
5+
6+
export interface GlobalsAdapter {
7+
setGame(instance: GameInstance<any>): void;
8+
}
9+
10+
export class BrowserGlobalsAdapter implements GlobalsAdapter {
11+
setGame(
12+
instance: GameInstance<any>
13+
) {
14+
globalThis.game = instance;
15+
}
16+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from "./canvas.js";
2+
export * from "./document.js";
3+
export * from "./eventTarget.js";
4+
export * from "./window.js";
5+
export * from "./navigator.js";
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface NavigatorAdapter {
2+
getGamepads(): (Gamepad | null)[];
3+
vibrate(pattern: VibratePattern): boolean;
4+
}
5+
6+
export class BrowserNavigatorAdapter implements NavigatorAdapter {
7+
getGamepads(): (Gamepad | null)[] {
8+
return navigator.getGamepads();
9+
}
10+
11+
vibrate(
12+
pattern: VibratePattern
13+
): boolean {
14+
return navigator.vibrate(pattern);
15+
}
16+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Import Internal Dependencies
2+
import type {
3+
EventTargetAdapter,
4+
EventTargetListener
5+
} from "./eventTarget.js";
6+
import {
7+
type NavigatorAdapter,
8+
BrowserNavigatorAdapter
9+
} from "./navigator.js";
10+
11+
export interface WindowAdapter extends EventTargetAdapter {
12+
onbeforeunload?: ((this: Window, ev: BeforeUnloadEvent) => any) | null;
13+
14+
navigator: NavigatorAdapter;
15+
}
16+
17+
export class BrowserWindowAdapter implements WindowAdapter {
18+
navigator: NavigatorAdapter = new BrowserNavigatorAdapter();
19+
20+
addEventListener(
21+
type: string,
22+
listener: EventTargetListener
23+
) {
24+
window.addEventListener(type, listener);
25+
}
26+
27+
removeEventListener(
28+
type: string,
29+
listener: EventTargetListener
30+
) {
31+
window.removeEventListener(type, listener);
32+
}
33+
}

packages/engine/src/controls/ControlTarget.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/engine/src/controls/Input.class.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import * as THREE from "three";
33
import { EventEmitter } from "@posva/event-emitter";
44

55
// Import Internal Dependencies
6-
import * as targets from "./targets/index.js";
6+
import * as sources from "./targets/index.js";
7+
import {
8+
BrowserWindowAdapter,
9+
type WindowAdapter
10+
} from "../adapters/index.js";
11+
import type {
12+
InputControl
13+
} from "./types.js";
714

815
export type { MouseEventButton } from "./targets/Mouse.class.js";
916

@@ -16,24 +23,26 @@ export interface InputOptions {
1623
* @default false
1724
*/
1825
enableOnExit?: boolean;
26+
windowAdapter?: WindowAdapter;
1927
}
2028

2129
export type InputMouseAction =
2230
| number
23-
| keyof typeof targets.MouseEventButton
31+
| keyof typeof sources.MouseEventButton
2432
| "ANY"
2533
| "NONE";
2634

2735
export type InputKeyboardAction = string | "ANY" | "NONE";
2836

2937
export class Input extends EventEmitter<InputEvents> {
3038
#canvas: HTMLCanvasElement;
39+
#windowAdapter: WindowAdapter;
3140

32-
mouse: targets.Mouse;
33-
touchpad: targets.Touchpad;
34-
gamepad: targets.Gamepad;
35-
fullscreen: targets.Fullscreen;
36-
keyboard: targets.Keyboard;
41+
mouse: sources.Mouse;
42+
touchpad: sources.Touchpad;
43+
gamepad: sources.Gamepad;
44+
fullscreen: sources.Fullscreen;
45+
keyboard: sources.Keyboard;
3746

3847
exited = false;
3948

@@ -43,26 +52,36 @@ export class Input extends EventEmitter<InputEvents> {
4352
) {
4453
super();
4554
const {
46-
enableOnExit = false
55+
enableOnExit = false,
56+
windowAdapter = new BrowserWindowAdapter()
4757
} = options;
4858

4959
this.#canvas = canvas;
50-
const fullscreen = new targets.Fullscreen(canvas);
51-
this.mouse = new targets.Mouse(canvas, {
60+
this.#windowAdapter = windowAdapter;
61+
const fullscreen = new sources.Fullscreen({
62+
canvas
63+
});
64+
this.mouse = new sources.Mouse({
65+
canvas,
5266
mouseDownCallback: () => fullscreen.onMouseDown(),
5367
mouseUpCallback: () => fullscreen.onMouseUp()
5468
});
5569
this.fullscreen = fullscreen;
56-
this.touchpad = new targets.Touchpad(this.mouse);
57-
this.gamepad = new targets.Gamepad();
58-
this.keyboard = new targets.Keyboard();
70+
this.touchpad = new sources.Touchpad({
71+
canvas,
72+
mouse: this.mouse
73+
});
74+
this.gamepad = new sources.Gamepad({
75+
navigatorAdapter: this.#windowAdapter.navigator
76+
});
77+
this.keyboard = new sources.Keyboard();
5978

6079
if (enableOnExit) {
61-
window.onbeforeunload = this.doExitCallback;
80+
this.#windowAdapter.onbeforeunload = this.doExitCallback;
6281
}
6382
}
6483

65-
#targets() {
84+
#sourceInputs(): InputControl[] {
6685
return [
6786
this.mouse,
6887
this.touchpad,
@@ -72,17 +91,15 @@ export class Input extends EventEmitter<InputEvents> {
7291
}
7392

7493
connect() {
75-
this.#targets()
76-
.forEach((subscriber) => subscriber.connect());
77-
this.fullscreen?.connect();
78-
window.addEventListener("blur", this.onBlur);
94+
[...this.#sourceInputs(), this.fullscreen]
95+
.forEach((subscriber) => subscriber.connect?.());
96+
this.#windowAdapter.addEventListener("blur", this.onBlur);
7997
}
8098

8199
disconnect() {
82-
this.#targets()
83-
.forEach((subscriber) => subscriber.disconnect());
84-
this.fullscreen?.disconnect();
85-
window.removeEventListener("blur", this.onBlur);
100+
[...this.#sourceInputs(), this.fullscreen]
101+
.forEach((subscriber) => subscriber.disconnect?.());
102+
this.#windowAdapter.removeEventListener("blur", this.onBlur);
86103
}
87104

88105
enterFullscreen() {
@@ -94,8 +111,8 @@ export class Input extends EventEmitter<InputEvents> {
94111
}
95112

96113
update() {
97-
this.#targets()
98-
.forEach((subscriber) => subscriber?.update());
114+
this.#sourceInputs()
115+
.forEach((subscriber) => subscriber.update());
99116
}
100117

101118
getScreenSize() {
@@ -150,7 +167,7 @@ export class Input extends EventEmitter<InputEvents> {
150167
return this.mouse.buttonsDown.length === 0;
151168
}
152169

153-
const index = typeof action === "number" ? action : targets.MouseEventButton[action];
170+
const index = typeof action === "number" ? action : sources.MouseEventButton[action];
154171

155172
return this.mouse.buttonsDown[index];
156173
}
@@ -165,7 +182,7 @@ export class Input extends EventEmitter<InputEvents> {
165182
return this.mouse.buttons.every((button) => !button.wasJustReleased);
166183
}
167184

168-
const index = typeof action === "number" ? action : targets.MouseEventButton[action];
185+
const index = typeof action === "number" ? action : sources.MouseEventButton[action];
169186

170187
return this.mouse.buttons[index]?.wasJustReleased ?? false;
171188
}
@@ -179,7 +196,7 @@ export class Input extends EventEmitter<InputEvents> {
179196
if (action === "NONE") {
180197
return this.mouse.buttons.every((button) => !button.wasJustPressed);
181198
}
182-
const index = typeof action === "number" ? action : targets.MouseEventButton[action];
199+
const index = typeof action === "number" ? action : sources.MouseEventButton[action];
183200

184201
return this.mouse.buttons[index]?.wasJustPressed ?? false;
185202
}
@@ -232,7 +249,7 @@ export class Input extends EventEmitter<InputEvents> {
232249
vibrate(
233250
pattern: VibratePattern
234251
): void {
235-
window.navigator.vibrate(pattern);
252+
this.#windowAdapter.navigator.vibrate(pattern);
236253
}
237254

238255
isKeyDown(
@@ -392,7 +409,7 @@ export class Input extends EventEmitter<InputEvents> {
392409
}
393410

394411
private onBlur = () => {
395-
this.#targets()
412+
this.#sourceInputs()
396413
.forEach((subscriber) => subscriber.reset());
397414
};
398415

0 commit comments

Comments
 (0)