@vibedgames/gamepad 0.1.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physical.d.ts","sourceRoot":"","sources":["../src/physical.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;sEACsE;AACtE,MAAM,MAAM,SAAS,GACjB,GAAG,GACH,GAAG,GACH,GAAG,GACH,GAAG,GACH,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,QAAQ,GACR,OAAO,GACP,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CAAC;AA8CZ,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC,CAAC;IAC1D,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,aAAa,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAC5C,CAAC;AAyBF;wDACwD;AACxD,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiD;IAC1E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAa;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsC;IAE3D,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,IAAI,CAAwB;IACpC,OAAO,CAAC,QAAQ,CAAwB;IACxC,oDAAoD;IACpD,OAAO,CAAC,MAAM,CAAgC;IAE9C,YAAY,OAAO,GAAE,sBAA2B,EAO/C;IAED,2EAA2E;IAC3E,MAAM,IAAI,IAAI,CA+Bb;IAED,qDAAqD;IACrD,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,mEAAmE;IACnE,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEhC;IAED,4EAA4E;IAC5E,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG/B;IAED,gFAAgF;IAChF,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGhC;IAED,+EAA+E;IAC/E,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAErC;IAED;;;;OAIG;IACH,QAAQ,CAAC,IAAI,GAAE,MAAM,GAAG,OAAgB,GAAG,UAAU,CAoBpD;IAED,2EAA2E;IAC3E,OAAO,IAAI,IAAI,CAMd;IAED,OAAO,CAAC,OAAO;CAKhB"}
@@ -0,0 +1,201 @@
1
+ const BUTTON_INDEX = {
2
+ a: 0,
3
+ b: 1,
4
+ x: 2,
5
+ y: 3,
6
+ lb: 4,
7
+ rb: 5,
8
+ lt: 6,
9
+ rt: 7,
10
+ select: 8,
11
+ start: 9,
12
+ ls: 10,
13
+ rs: 11,
14
+ up: 12,
15
+ down: 13,
16
+ left: 14,
17
+ right: 15,
18
+ };
19
+ const PAD_BUTTONS = [
20
+ "a",
21
+ "b",
22
+ "x",
23
+ "y",
24
+ "lb",
25
+ "rb",
26
+ "lt",
27
+ "rt",
28
+ "select",
29
+ "start",
30
+ "ls",
31
+ "rs",
32
+ "up",
33
+ "down",
34
+ "left",
35
+ "right",
36
+ ];
37
+ const PAD_BUTTON_NAMES = new Set(PAD_BUTTONS);
38
+ function isPadButton(id) {
39
+ return PAD_BUTTON_NAMES.has(id);
40
+ }
41
+ const DEFAULT_STICK_DEAD_ZONE = 0.15;
42
+ const DEFAULT_TRIGGER_THRESHOLD = 0.05;
43
+ const IDLE_STICK = {
44
+ active: false,
45
+ anchorX: 0,
46
+ anchorY: 0,
47
+ curX: 0,
48
+ curY: 0,
49
+ dx: 0,
50
+ dy: 0,
51
+ distance: 0,
52
+ angle: 0,
53
+ magnitude: 0,
54
+ inDeadZone: true,
55
+ };
56
+ function defaultPoll() {
57
+ return typeof navigator !== "undefined" && typeof navigator.getGamepads === "function"
58
+ ? navigator.getGamepads()
59
+ : [];
60
+ }
61
+ /** True if any physical pad is currently connected — e.g. to decide whether
62
+ * controller rows belong on an instructions screen. */
63
+ export function isPadConnected() {
64
+ return defaultPoll().some((pad) => pad?.connected ?? false);
65
+ }
66
+ /**
67
+ * Physical controller input, API-mirroring {@link VirtualGamepad} so a game's
68
+ * read path works for both: `isButtonDown(id)` / `justPressed(id)` /
69
+ * `getStick()`. Poll-based — call `update()` once per frame; edges are
70
+ * published per update, matching the virtual pad's `nextFrame()` semantics.
71
+ * The first connected pad wins. No DOM, no engine, no listeners.
72
+ *
73
+ * ```ts
74
+ * const pad = new PhysicalGamepad({ bindings: { jump: ["a"], dash: ["b", "rb"] } });
75
+ * // in your game loop:
76
+ * pad.update();
77
+ * const dir = stickDirection4(pad.getStick());
78
+ * if (pad.justPressed("jump")) jump();
79
+ * ```
80
+ */
81
+ export class PhysicalGamepad {
82
+ bindings;
83
+ stickDeadZone;
84
+ triggerThreshold;
85
+ onConnect;
86
+ onDisconnect;
87
+ poll;
88
+ wasConnected = false;
89
+ axes = [];
90
+ down = new Set();
91
+ prevDown = new Set();
92
+ /** Raw analog values (triggers), by button name. */
93
+ values = new Map();
94
+ constructor(options = {}) {
95
+ this.bindings = options.bindings ?? {};
96
+ this.stickDeadZone = options.stickDeadZone ?? DEFAULT_STICK_DEAD_ZONE;
97
+ this.triggerThreshold = options.triggerThreshold ?? DEFAULT_TRIGGER_THRESHOLD;
98
+ this.onConnect = options.onConnect;
99
+ this.onDisconnect = options.onDisconnect;
100
+ this.poll = options.poll ?? defaultPoll;
101
+ }
102
+ /** Poll the first connected pad and publish press edges for this frame. */
103
+ update() {
104
+ let pad = null;
105
+ for (const candidate of this.poll()) {
106
+ if (candidate?.connected) {
107
+ pad = candidate;
108
+ break;
109
+ }
110
+ }
111
+ const connected = pad !== null;
112
+ if (connected !== this.wasConnected) {
113
+ this.wasConnected = connected;
114
+ if (connected)
115
+ this.onConnect?.();
116
+ else
117
+ this.onDisconnect?.();
118
+ }
119
+ // Swap, then rebuild — prevDown keeps last frame's state for edge reads.
120
+ [this.prevDown, this.down] = [this.down, this.prevDown];
121
+ this.down.clear();
122
+ this.values.clear();
123
+ this.axes = pad ? [...pad.axes] : [];
124
+ if (!pad)
125
+ return;
126
+ for (const name of PAD_BUTTONS) {
127
+ const button = pad.buttons[BUTTON_INDEX[name]];
128
+ if (!button)
129
+ continue;
130
+ this.values.set(name, button.value);
131
+ // Triggers report analog values; some pads never set `pressed` for a
132
+ // light pull, so a value past the threshold also counts as down.
133
+ if (button.pressed || button.value > this.triggerThreshold)
134
+ this.down.add(name);
135
+ }
136
+ }
137
+ /** A pad was connected as of the last `update()`. */
138
+ get connected() {
139
+ return this.wasConnected;
140
+ }
141
+ /** `id` is a binding action id or a raw {@link PadButton} name. */
142
+ isButtonDown(id) {
143
+ return this.resolve(id).some((b) => this.down.has(b));
144
+ }
145
+ /** Action went up→down since the previous `update()` (any bound button). */
146
+ justPressed(id) {
147
+ const buttons = this.resolve(id);
148
+ return buttons.some((b) => this.down.has(b)) && !buttons.some((b) => this.prevDown.has(b));
149
+ }
150
+ /** Action went down→up since the previous `update()` (all bound buttons up). */
151
+ justReleased(id) {
152
+ const buttons = this.resolve(id);
153
+ return buttons.some((b) => this.prevDown.has(b)) && !buttons.some((b) => this.down.has(b));
154
+ }
155
+ /** Raw analog value 0–1 (triggers are the interesting case). 0 when absent. */
156
+ buttonValue(button) {
157
+ return this.values.get(button) ?? 0;
158
+ }
159
+ /**
160
+ * A stick reading in {@link StickState} form (axes 0/1 left, 2/3 right;
161
+ * y-down matches screen space), so `stickDirection4/8` work unchanged.
162
+ * `active` while a pad is connected; dead-zoned like the virtual stick.
163
+ */
164
+ getStick(side = "left") {
165
+ if (!this.wasConnected)
166
+ return { ...IDLE_STICK };
167
+ const base = side === "left" ? 0 : 2;
168
+ const dx = this.axes[base] ?? 0;
169
+ const dy = this.axes[base + 1] ?? 0;
170
+ const distance = Math.hypot(dx, dy);
171
+ const span = Math.max(1e-6, 1 - this.stickDeadZone);
172
+ return {
173
+ active: true,
174
+ anchorX: 0,
175
+ anchorY: 0,
176
+ curX: dx,
177
+ curY: dy,
178
+ dx,
179
+ dy,
180
+ distance,
181
+ angle: Math.atan2(dy, dx),
182
+ magnitude: Math.min(1, Math.max(0, (distance - this.stickDeadZone) / span)),
183
+ inDeadZone: distance <= this.stickDeadZone,
184
+ };
185
+ }
186
+ /** Release all state (the class holds no listeners — this just clears). */
187
+ destroy() {
188
+ this.down.clear();
189
+ this.prevDown.clear();
190
+ this.values.clear();
191
+ this.axes = [];
192
+ this.wasConnected = false;
193
+ }
194
+ resolve(id) {
195
+ const bound = this.bindings[id];
196
+ if (bound)
197
+ return bound;
198
+ return isPadButton(id) ? [id] : [];
199
+ }
200
+ }
201
+ //# sourceMappingURL=physical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physical.js","sourceRoot":"","sources":["../src/physical.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Inset, VisibilityPolicy } from "./types";
2
+ /**
3
+ * Read the device safe-area insets (`env(safe-area-inset-*)`) via a hidden
4
+ * probe element, for feeding `VirtualGamepad.setViewport`. Returns zeros when
5
+ * there is no DOM or no notch. Requires `viewport-fit=cover` in the page's
6
+ * viewport meta tag to report non-zero values on iOS.
7
+ */
8
+ export declare function safeAreaInset(): Inset;
9
+ /** Whether the overlay should render before any touch has been seen. */
10
+ export declare function preShow(policy: VisibilityPolicy): boolean;
11
+ //# sourceMappingURL=safe-area.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-area.d.ts","sourceRoot":"","sources":["../src/safe-area.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAMvD;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,KAAK,CAiBrC;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAOzD"}
@@ -0,0 +1,37 @@
1
+ const ZERO = { top: 0, right: 0, bottom: 0, left: 0 };
2
+ let probe = null;
3
+ /**
4
+ * Read the device safe-area insets (`env(safe-area-inset-*)`) via a hidden
5
+ * probe element, for feeding `VirtualGamepad.setViewport`. Returns zeros when
6
+ * there is no DOM or no notch. Requires `viewport-fit=cover` in the page's
7
+ * viewport meta tag to report non-zero values on iOS.
8
+ */
9
+ export function safeAreaInset() {
10
+ if (typeof document === "undefined" || !document.body)
11
+ return ZERO;
12
+ if (!probe || !probe.isConnected) {
13
+ probe = document.createElement("div");
14
+ probe.style.cssText =
15
+ "position:fixed;top:0;left:0;visibility:hidden;pointer-events:none;" +
16
+ "padding:env(safe-area-inset-top) env(safe-area-inset-right) " +
17
+ "env(safe-area-inset-bottom) env(safe-area-inset-left);";
18
+ document.body.appendChild(probe);
19
+ }
20
+ const s = getComputedStyle(probe);
21
+ return {
22
+ top: Number.parseFloat(s.paddingTop) || 0,
23
+ right: Number.parseFloat(s.paddingRight) || 0,
24
+ bottom: Number.parseFloat(s.paddingBottom) || 0,
25
+ left: Number.parseFloat(s.paddingLeft) || 0,
26
+ };
27
+ }
28
+ /** Whether the overlay should render before any touch has been seen. */
29
+ export function preShow(policy) {
30
+ if (policy === "always")
31
+ return true;
32
+ if (policy !== "coarse")
33
+ return false;
34
+ return (typeof window !== "undefined" &&
35
+ (window.matchMedia("(pointer: coarse)").matches || "ontouchstart" in window));
36
+ }
37
+ //# sourceMappingURL=safe-area.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-area.js","sourceRoot":"","sources":["../src/safe-area.ts"],"names":[],"mappings":"AAEA,MAAM,IAAI,GAAU,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAE7D,IAAI,KAAK,GAAuB,IAAI,CAAC;AAErC;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QACjC,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACtC,KAAK,CAAC,KAAK,CAAC,OAAO;YACjB,oEAAoE;gBACpE,8DAA8D;gBAC9D,wDAAwD,CAAC;QAC3D,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,CAAC,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QACzC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC;QAC7C,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC;QAC/C,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,OAAO,CAAC,MAAwB;IAC9C,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,CACL,OAAO,MAAM,KAAK,WAAW;QAC7B,CAAC,MAAM,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC,OAAO,IAAI,cAAc,IAAI,MAAM,CAAC,CAC7E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,105 @@
1
+ /** A 2D point / vector in screen-space pixels. */
2
+ export type Vec2 = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ /** Device safe-area insets (notch / home indicator), screen-space px. */
7
+ export type Inset = {
8
+ top: number;
9
+ right: number;
10
+ bottom: number;
11
+ left: number;
12
+ };
13
+ /** Current canvas size + safe-area insets, used to re-anchor fixed buttons on
14
+ * resize. Position resolvers should keep bottom/side-anchored buttons clear
15
+ * of `inset` (zeros on devices without notches). */
16
+ export type Viewport = {
17
+ width: number;
18
+ height: number;
19
+ inset: Inset;
20
+ };
21
+ /** When the overlay is shown: after the first touch (default), pre-shown on
22
+ * coarse-pointer devices so buttons are discoverable, or always. */
23
+ export type VisibilityPolicy = "touch" | "coarse" | "always";
24
+ /** Tuning for the floating analog stick. All distances are screen-space px. */
25
+ export type StickOptions = {
26
+ /** Drag distance from the anchor that maps to full magnitude (1.0). */
27
+ radius?: number;
28
+ /** No magnitude / no aim within this drag of the anchor — a parked thumb
29
+ * doesn't jitter the heading. */
30
+ deadZone?: number;
31
+ /** Visual knob (inner puck) radius, used by renderers. */
32
+ knobRadius?: number;
33
+ };
34
+ /** A button on the virtual gamepad. */
35
+ export type ButtonOptions = {
36
+ /** Stable id you read back with `isButtonDown(id)`. */
37
+ id: string;
38
+ /** Fixed on-screen button: resolves a center given the current viewport, so
39
+ * the button re-anchors on resize. Omit `position` to make this a "rest"
40
+ * button that captures ANY touch not on the stick or another fixed button
41
+ * (the "any other finger fires" model). */
42
+ position?: (viewport: Viewport) => Vec2;
43
+ /** Hit-test + render radius for a fixed button. Ignored by rest buttons. */
44
+ radius?: number;
45
+ /** Optional glyph/label a custom renderer may draw. */
46
+ label?: string;
47
+ };
48
+ export type VirtualGamepadOptions = {
49
+ /** Floating analog stick config, or `false` to disable the stick entirely. */
50
+ stick?: StickOptions | false;
51
+ /** Action buttons (fixed or "rest"). Evaluated in order on each touch — put
52
+ * the most specific fixed buttons first; a "rest" button (if any) is the
53
+ * fallback for touches that hit nothing else. */
54
+ buttons?: ButtonOptions[];
55
+ /** Edge callback the first frame a button goes from up→down (e.g. place a
56
+ * bomb on tap). For held input, read `isButtonDown` each frame instead. */
57
+ onButtonDown?: (id: string) => void;
58
+ /** Edge callback when the last finger leaves a button (down→up). */
59
+ onButtonUp?: (id: string) => void;
60
+ };
61
+ /** A read-only snapshot of the stick. Fields collapse to 0 when idle. */
62
+ export type StickState = {
63
+ /** A finger is on the stick. */
64
+ active: boolean;
65
+ /** Where the finger first landed. */
66
+ anchorX: number;
67
+ anchorY: number;
68
+ /** Current finger position. */
69
+ curX: number;
70
+ curY: number;
71
+ /** Current minus anchor. */
72
+ dx: number;
73
+ dy: number;
74
+ /** Raw drag distance from the anchor (un-clamped). */
75
+ distance: number;
76
+ /** Drag heading in radians (`atan2(dy, dx)`, screen-space y-down); 0 idle. */
77
+ angle: number;
78
+ /** 0–1 thrust after the dead zone, clamped at `radius`. */
79
+ magnitude: number;
80
+ /** Drag is within the dead zone — don't re-aim or thrust this frame. */
81
+ inDeadZone: boolean;
82
+ };
83
+ /** Resolved stick tuning (defaults applied), for renderers. */
84
+ export type StickGeometry = {
85
+ radius: number;
86
+ deadZone: number;
87
+ knobRadius: number;
88
+ };
89
+ /** Resolved button geometry + state, for renderers. */
90
+ export type ButtonLayout = {
91
+ id: string;
92
+ /** Center (0,0 for rest buttons, which have no fixed position). */
93
+ x: number;
94
+ y: number;
95
+ radius: number;
96
+ label?: string;
97
+ pressed: boolean;
98
+ /** Rest buttons have no fixed position — renderers skip them. */
99
+ rest: boolean;
100
+ };
101
+ /** Four-way snapped direction (screen-space: +y is down). */
102
+ export type Dir4 = "up" | "down" | "left" | "right";
103
+ /** Eight-way snapped direction. */
104
+ export type Dir8 = Dir4 | "up-left" | "up-right" | "down-left" | "down-right";
105
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@vibedgames/gamepad",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "On-screen touch controls for browser games — framework-agnostic core + DOM and Phaser adapters",
6
+ "keywords": [
7
+ "gamedev",
8
+ "gamepad",
9
+ "joystick",
10
+ "mobile",
11
+ "phaser",
12
+ "touch",
13
+ "virtual-controller"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/kyh/vibedgames",
19
+ "directory": "packages/gamepad"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./dom": {
31
+ "types": "./dist/dom.d.ts",
32
+ "default": "./dist/dom.js"
33
+ },
34
+ "./phaser": {
35
+ "types": "./dist/phaser.d.ts",
36
+ "default": "./dist/phaser.js"
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@kyh/tsconfig": "^1.1.14",
41
+ "phaser": "^4.2.1",
42
+ "typescript": "^7.0.2"
43
+ },
44
+ "peerDependencies": {
45
+ "phaser": ">=3"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "phaser": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "tsc",
54
+ "clean": "rm -rf .cache .turbo dist node_modules",
55
+ "typecheck": "tsc --noEmit"
56
+ }
57
+ }