emeraldengine 2.2.1 → 3.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.
- package/README.md +2198 -968
- package/dist/types/index.d.ts +75 -32
- package/dist/types/src/Animator.d.ts +50 -0
- package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
- package/dist/types/src/Camera.d.ts +122 -0
- package/dist/types/src/CameraController.d.ts +107 -0
- package/dist/types/src/CanvasText.d.ts +91 -0
- package/dist/types/src/CollisionLayers.d.ts +58 -0
- package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
- package/dist/types/src/Coroutine.d.ts +111 -0
- package/dist/types/src/DebugOverlay.d.ts +86 -0
- package/dist/types/src/Drawable.d.ts +271 -0
- package/dist/types/src/Easing.d.ts +22 -0
- package/dist/types/src/Emerald.d.ts +420 -0
- package/dist/types/src/EmeraldDB.d.ts +159 -0
- package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
- package/dist/types/src/GLUtils.d.ts +4 -0
- package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
- package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
- package/dist/types/src/Interpolator.d.ts +66 -0
- package/dist/types/src/Material.d.ts +87 -0
- package/dist/types/src/MathUtils.d.ts +81 -0
- package/dist/types/src/ParticleEmitter.d.ts +131 -0
- package/dist/types/src/Physics.d.ts +337 -0
- package/dist/types/src/Pool.d.ts +53 -0
- package/dist/types/src/PostEffects.d.ts +68 -0
- package/dist/types/src/PostProcessor.d.ts +124 -0
- package/dist/types/src/RenderTarget.d.ts +56 -0
- package/dist/types/src/Scene.d.ts +62 -0
- package/dist/types/src/ScreenEffects.d.ts +111 -0
- package/dist/types/src/Serializer.d.ts +86 -0
- package/dist/types/src/Shaders.d.ts +2 -0
- package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
- package/dist/types/src/SpatialGrid.d.ts +50 -0
- package/dist/types/src/SpriteBatch.d.ts +89 -0
- package/dist/types/src/StateMachine.d.ts +59 -0
- package/dist/types/src/Storage.d.ts +89 -0
- package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
- package/dist/types/src/TextureAtlas.d.ts +55 -0
- package/dist/types/src/Tilemap.d.ts +91 -0
- package/dist/types/src/Time.d.ts +53 -0
- package/dist/types/src/Timer.d.ts +54 -0
- package/dist/types/src/Transform.d.ts +94 -0
- package/dist/types/src/Tween.d.ts +81 -0
- package/dist/types/src/UI.d.ts +121 -0
- package/dist/types/src/components/Behaviour.d.ts +71 -0
- package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
- package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
- package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
- package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
- package/dist/types/src/components/Collider.d.ts +102 -0
- package/dist/types/src/components/GameObject.d.ts +135 -0
- package/dist/types/src/components/PolygonCollider.d.ts +33 -0
- package/dist/types/src/components/RigidBody.d.ts +456 -0
- package/dist/types/src/importers/Aseprite.d.ts +79 -0
- package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
- package/dist/types/src/importers/TiledMap.d.ts +62 -0
- package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
- package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
- package/dist/types/src/managers/AssetManager.d.ts +116 -0
- package/dist/types/src/managers/AudioManager.d.ts +259 -0
- package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
- package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
- package/dist/types/src/managers/GLManager.d.ts +84 -0
- package/dist/types/src/managers/GLState.d.ts +37 -0
- package/dist/types/src/managers/Gamepad.d.ts +102 -0
- package/dist/types/src/managers/IDManager.d.ts +31 -0
- package/dist/types/src/managers/InputManager.d.ts +381 -0
- package/dist/types/src/managers/NetworkManager.d.ts +93 -0
- package/dist/types/src/managers/RenderStats.d.ts +34 -0
- package/dist/types/src/managers/SceneManager.d.ts +44 -0
- package/dist/types/src/managers/ShaderManager.d.ts +55 -0
- package/dist/types/src/managers/TextureManager.d.ts +102 -0
- package/dist/types/src/particlesystem/Particle.d.ts +64 -0
- package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
- package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
- package/dist/types/src/physics/AABB.d.ts +92 -0
- package/dist/types/src/physics/Body.d.ts +435 -0
- package/dist/types/src/physics/BodyType.d.ts +6 -0
- package/dist/types/src/physics/BroadPhase.d.ts +210 -0
- package/dist/types/src/physics/Collision.d.ts +102 -0
- package/dist/types/src/physics/Contact.d.ts +206 -0
- package/dist/types/src/physics/ContactSolver.d.ts +108 -0
- package/dist/types/src/physics/Distance.d.ts +54 -0
- package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
- package/dist/types/src/physics/Fixture.d.ts +221 -0
- package/dist/types/src/physics/Island.d.ts +52 -0
- package/dist/types/src/physics/Joint.d.ts +59 -0
- package/dist/types/src/physics/Math2D.d.ts +371 -0
- package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
- package/dist/types/src/physics/Settings.d.ts +22 -0
- package/dist/types/src/physics/Shapes.d.ts +207 -0
- package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
- package/dist/types/src/physics/World.d.ts +274 -0
- package/dist/types/src/physics/index.d.ts +34 -0
- package/index.js +78 -0
- package/package.json +73 -60
- package/src/Animator.js +95 -0
- package/src/BitmapText.js +6 -5
- package/src/Camera.js +183 -0
- package/src/CameraController.js +192 -0
- package/src/CanvasText.js +281 -0
- package/src/CollisionLayers.js +86 -0
- package/src/Color.js +26 -18
- package/src/Coroutine.js +259 -0
- package/src/DebugOverlay.js +246 -0
- package/src/Drawable.js +842 -582
- package/src/Easing.js +57 -0
- package/src/Emerald.js +1150 -459
- package/src/EmeraldDB.js +328 -0
- package/src/FPSCounter.js +43 -43
- package/src/GLUtils.js +60 -67
- package/src/Instance.js +41 -5
- package/src/InstancedTexture.js +308 -127
- package/src/Interpolator.js +124 -0
- package/src/Material.js +202 -0
- package/src/MathUtils.js +134 -0
- package/src/ParticleEmitter.js +284 -0
- package/src/Physics.js +431 -40
- package/src/Pool.js +85 -0
- package/src/PostEffects.js +296 -0
- package/src/PostProcessor.js +304 -0
- package/src/RenderTarget.js +134 -0
- package/src/Scene.js +115 -83
- package/src/ScreenEffects.js +266 -0
- package/src/Serializer.js +131 -0
- package/src/Shaders.js +150 -165
- package/src/Shapes.js +118 -129
- package/src/SpatialGrid.js +111 -0
- package/src/SpriteBatch.js +299 -0
- package/src/StateMachine.js +82 -0
- package/src/Storage.js +175 -47
- package/src/Texture.js +58 -67
- package/src/TextureAtlas.js +96 -0
- package/src/Tilemap.js +274 -0
- package/src/Time.js +51 -6
- package/src/Timer.js +99 -0
- package/src/Transform.js +100 -7
- package/src/Tween.js +160 -0
- package/src/UI.js +394 -0
- package/src/components/Behaviour.js +90 -0
- package/src/components/BoxCollider.js +29 -12
- package/src/components/BoxColliderDebug.js +3 -4
- package/src/components/CircleCollider.js +26 -12
- package/src/components/CircleColliderDebug.js +25 -24
- package/src/components/Collider.js +150 -34
- package/src/components/GameObject.js +130 -21
- package/src/components/PolygonCollider.js +55 -0
- package/src/components/RigidBody.js +556 -8
- package/src/importers/Aseprite.js +142 -0
- package/src/importers/ForgeLevel.js +581 -0
- package/src/importers/TiledMap.js +158 -0
- package/src/lights/DirectionalLight.js +6 -15
- package/src/lights/PointLight.js +4 -4
- package/src/managers/AssetManager.js +239 -0
- package/src/managers/AudioManager.js +565 -146
- package/src/managers/EventManager.js +488 -477
- package/src/managers/GLManager.js +57 -0
- package/src/managers/GLState.js +70 -0
- package/src/managers/Gamepad.js +126 -0
- package/src/managers/IDManager.js +24 -2
- package/src/managers/InputManager.js +905 -0
- package/src/managers/NetworkManager.js +178 -0
- package/src/managers/RenderStats.js +34 -0
- package/src/managers/SceneManager.js +30 -0
- package/src/managers/ShaderManager.js +0 -2
- package/src/managers/TextureManager.js +218 -0
- package/src/particlesystem/Particle.js +82 -7
- package/src/particlesystem/ParticleSettings.js +21 -3
- package/src/particlesystem/Particles.js +80 -31
- package/src/physics/AABB.js +207 -0
- package/src/physics/Body.js +862 -0
- package/src/physics/BodyType.js +16 -0
- package/src/physics/BroadPhase.js +641 -0
- package/src/physics/Collision.js +534 -0
- package/src/physics/Contact.js +500 -0
- package/src/physics/ContactSolver.js +526 -0
- package/src/physics/Distance.js +403 -0
- package/src/physics/DistanceJoint.js +227 -0
- package/src/physics/Fixture.js +346 -0
- package/src/physics/Island.js +203 -0
- package/src/physics/Joint.js +78 -0
- package/src/physics/Math2D.js +573 -0
- package/src/physics/RevoluteJoint.js +278 -0
- package/src/physics/Settings.js +78 -0
- package/src/physics/Shapes.js +549 -0
- package/src/physics/TimeOfImpact.js +87 -0
- package/src/physics/World.js +731 -0
- package/src/physics/index.js +79 -0
- package/dist/types/Drawable.d.ts +0 -157
- package/dist/types/Emerald.d.ts +0 -73
- package/dist/types/GLUtils.d.ts +0 -6
- package/dist/types/Physics.d.ts +0 -139
- package/dist/types/Scene.d.ts +0 -39
- package/dist/types/Shaders.d.ts +0 -4
- package/dist/types/Storage.d.ts +0 -46
- package/dist/types/Time.d.ts +0 -22
- package/dist/types/Transform.d.ts +0 -41
- package/dist/types/components/BoxColliderDebug.d.ts +0 -19
- package/dist/types/components/CircleColliderDebug.d.ts +0 -19
- package/dist/types/components/Collider.d.ts +0 -53
- package/dist/types/components/GameObject.d.ts +0 -72
- package/dist/types/components/RigidBody.d.ts +0 -104
- package/dist/types/managers/AudioManager.d.ts +0 -60
- package/dist/types/managers/GLManager.d.ts +0 -47
- package/dist/types/managers/IDManager.d.ts +0 -21
- package/dist/types/managers/SceneManager.d.ts +0 -22
- package/dist/types/particlesystem/Particle.d.ts +0 -42
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical W3C "standard gamepad" button layout, with vendor-neutral
|
|
3
|
+
* (south/east/west/north) and vendor (Xbox a/b/x/y, PlayStation
|
|
4
|
+
* cross/circle/square/triangle) aliases all pointing at the same index. This is
|
|
5
|
+
* the table used when a pad reports `mapping === "standard"` (Xbox pads, most
|
|
6
|
+
* controllers, and anything routed through Steam Input / XInput).
|
|
7
|
+
*/
|
|
8
|
+
const STANDARD_BUTTONS = {
|
|
9
|
+
south: 0,
|
|
10
|
+
a: 0,
|
|
11
|
+
cross: 0,
|
|
12
|
+
east: 1,
|
|
13
|
+
b: 1,
|
|
14
|
+
circle: 1,
|
|
15
|
+
west: 2,
|
|
16
|
+
x: 2,
|
|
17
|
+
square: 2,
|
|
18
|
+
north: 3,
|
|
19
|
+
y: 3,
|
|
20
|
+
triangle: 3,
|
|
21
|
+
l1: 4,
|
|
22
|
+
lb: 4,
|
|
23
|
+
leftShoulder: 4,
|
|
24
|
+
r1: 5,
|
|
25
|
+
rb: 5,
|
|
26
|
+
rightShoulder: 5,
|
|
27
|
+
l2: 6,
|
|
28
|
+
lt: 6,
|
|
29
|
+
leftTrigger: 6,
|
|
30
|
+
r2: 7,
|
|
31
|
+
rt: 7,
|
|
32
|
+
rightTrigger: 7,
|
|
33
|
+
select: 8,
|
|
34
|
+
back: 8,
|
|
35
|
+
view: 8,
|
|
36
|
+
share: 8,
|
|
37
|
+
start: 9,
|
|
38
|
+
menu: 9,
|
|
39
|
+
options: 9,
|
|
40
|
+
l3: 10,
|
|
41
|
+
leftStick: 10,
|
|
42
|
+
r3: 11,
|
|
43
|
+
rightStick: 11,
|
|
44
|
+
up: 12,
|
|
45
|
+
dpadUp: 12,
|
|
46
|
+
down: 13,
|
|
47
|
+
dpadDown: 13,
|
|
48
|
+
left: 14,
|
|
49
|
+
dpadLeft: 14,
|
|
50
|
+
right: 15,
|
|
51
|
+
dpadRight: 15,
|
|
52
|
+
home: 16,
|
|
53
|
+
guide: 16,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const STANDARD_AXES = { leftX: 0, leftY: 1, rightX: 2, rightY: 3 };
|
|
57
|
+
|
|
58
|
+
const CUSTOM_MAPPINGS = [];
|
|
59
|
+
|
|
60
|
+
const BUILTIN_MAPPINGS = [
|
|
61
|
+
{
|
|
62
|
+
match: "logitech dual action",
|
|
63
|
+
buttons: {
|
|
64
|
+
west: 0,
|
|
65
|
+
x: 0,
|
|
66
|
+
square: 0,
|
|
67
|
+
south: 1,
|
|
68
|
+
a: 1,
|
|
69
|
+
cross: 1,
|
|
70
|
+
east: 2,
|
|
71
|
+
b: 2,
|
|
72
|
+
circle: 2,
|
|
73
|
+
north: 3,
|
|
74
|
+
y: 3,
|
|
75
|
+
triangle: 3,
|
|
76
|
+
l1: 4,
|
|
77
|
+
lb: 4,
|
|
78
|
+
r1: 5,
|
|
79
|
+
rb: 5,
|
|
80
|
+
l2: 6,
|
|
81
|
+
lt: 6,
|
|
82
|
+
r2: 7,
|
|
83
|
+
rt: 7,
|
|
84
|
+
select: 8,
|
|
85
|
+
back: 8,
|
|
86
|
+
start: 9,
|
|
87
|
+
l3: 10,
|
|
88
|
+
r3: 11,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
match: "twin usb",
|
|
93
|
+
buttons: {
|
|
94
|
+
north: 0,
|
|
95
|
+
y: 0,
|
|
96
|
+
triangle: 0,
|
|
97
|
+
east: 1,
|
|
98
|
+
b: 1,
|
|
99
|
+
circle: 1,
|
|
100
|
+
south: 2,
|
|
101
|
+
a: 2,
|
|
102
|
+
cross: 2,
|
|
103
|
+
west: 3,
|
|
104
|
+
x: 3,
|
|
105
|
+
square: 3,
|
|
106
|
+
l2: 4,
|
|
107
|
+
lt: 4,
|
|
108
|
+
r2: 5,
|
|
109
|
+
rt: 5,
|
|
110
|
+
l1: 6,
|
|
111
|
+
lb: 6,
|
|
112
|
+
r1: 7,
|
|
113
|
+
rb: 7,
|
|
114
|
+
select: 8,
|
|
115
|
+
back: 8,
|
|
116
|
+
start: 9,
|
|
117
|
+
l3: 10,
|
|
118
|
+
r3: 11,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
match: "8bitdo",
|
|
123
|
+
buttons: {
|
|
124
|
+
east: 0,
|
|
125
|
+
b: 0,
|
|
126
|
+
circle: 0,
|
|
127
|
+
south: 1,
|
|
128
|
+
a: 1,
|
|
129
|
+
cross: 1,
|
|
130
|
+
north: 3,
|
|
131
|
+
y: 3,
|
|
132
|
+
triangle: 3,
|
|
133
|
+
west: 4,
|
|
134
|
+
x: 4,
|
|
135
|
+
square: 4,
|
|
136
|
+
l1: 6,
|
|
137
|
+
lb: 6,
|
|
138
|
+
r1: 7,
|
|
139
|
+
rb: 7,
|
|
140
|
+
l2: 8,
|
|
141
|
+
lt: 8,
|
|
142
|
+
r2: 9,
|
|
143
|
+
rt: 9,
|
|
144
|
+
select: 10,
|
|
145
|
+
back: 10,
|
|
146
|
+
start: 11,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const STICK_DIRECTIONS = {
|
|
152
|
+
leftStickLeft: ["leftX", -1],
|
|
153
|
+
leftStickRight: ["leftX", 1],
|
|
154
|
+
leftStickUp: ["leftY", -1],
|
|
155
|
+
leftStickDown: ["leftY", 1],
|
|
156
|
+
rightStickLeft: ["rightX", -1],
|
|
157
|
+
rightStickRight: ["rightX", 1],
|
|
158
|
+
rightStickUp: ["rightY", -1],
|
|
159
|
+
rightStickDown: ["rightY", 1],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for (const b of BUILTIN_MAPPINGS) {
|
|
163
|
+
CUSTOM_MAPPINGS.push({
|
|
164
|
+
test: (id) => (id || "").toLowerCase().includes(b.match),
|
|
165
|
+
buttons: { ...STANDARD_BUTTONS, ...b.buttons },
|
|
166
|
+
axes: { ...STANDARD_AXES, ...(b.axes || {}) },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @class InputManager
|
|
172
|
+
* @description Unified, pollable input: keyboard, mouse, touch, and full
|
|
173
|
+
* gamepad support (analog sticks/triggers, semantic button names, rumble,
|
|
174
|
+
* connect events, and per-controller mapping), with rebindable named actions.
|
|
175
|
+
* Call `update()` once per frame so `justPressed`/`justReleased` edge queries
|
|
176
|
+
* work for every device, including the gamepad.
|
|
177
|
+
*
|
|
178
|
+
* Gamepad tokens (usable anywhere a key token is, including in mapAction and
|
|
179
|
+
* justPressed); `<i>` is the pad index:
|
|
180
|
+
* "pad:<i>:south" / "pad:<i>:a" - face buttons (also east/b, west/x, north/y)
|
|
181
|
+
* "pad:<i>:l1" / "pad:<i>:r2" ... - shoulders / triggers
|
|
182
|
+
* "pad:<i>:start" / "pad:<i>:select" - center buttons
|
|
183
|
+
* "pad:<i>:dpadLeft" ... - d-pad (works via buttons or a hat axis)
|
|
184
|
+
* "pad:<i>:<n>" - raw button index (mapping-independent)
|
|
185
|
+
* "pad:<i>:axis<n>+" / "axis<n>-" - analog axis past the deadzone
|
|
186
|
+
* "pad:<i>:leftStickUp" / "Down" / "Left" / "Right" - stick as a d-pad
|
|
187
|
+
* "pad:<i>:rightStickUp" ... - (threshold: stickPressThreshold)
|
|
188
|
+
* "gamepad:<n>" - legacy: raw button <n> on pad 0
|
|
189
|
+
* Names resolve through the active mapping, so "pad:0:south" is the bottom face
|
|
190
|
+
* button regardless of whether the pad reports Xbox or PlayStation ordering.
|
|
191
|
+
*
|
|
192
|
+
* Typing these by hand means remembering the exact spelling of every button
|
|
193
|
+
* name above; {@link Gamepad} builds the same strings from named constants
|
|
194
|
+
* instead (`Gamepad.get(0).key(Gamepad.SOUTH)` === "pad:0:south"), which most
|
|
195
|
+
* editors will autocomplete the way an enum's members would.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* const input = new InputManager();
|
|
199
|
+
* input.mapAction("jump", ["Space", " ", "pad:0:south"]);
|
|
200
|
+
* input.mapAction("left", ["a", "ArrowLeft", "pad:0:dpadLeft"]);
|
|
201
|
+
* input.onGamepadConnected((info) => console.log("pad:", info.id, info.mapping));
|
|
202
|
+
* // in the loop:
|
|
203
|
+
* if (input.justPressed("jump")) { player.jump(); input.rumble(0, { duration: 120 }); }
|
|
204
|
+
* const { x } = input.getGamepadStick("left"); // analog, deadzoned
|
|
205
|
+
* input.update();
|
|
206
|
+
*/
|
|
207
|
+
class InputManager {
|
|
208
|
+
/**
|
|
209
|
+
* @method registerGamepadMapping
|
|
210
|
+
* @description Registers a custom button/axis mapping for controllers whose
|
|
211
|
+
* `mapping` is not "standard" (so their raw button indices differ). Match by
|
|
212
|
+
* a substring of `gamepad.id` (case-insensitive), a RegExp, or a predicate.
|
|
213
|
+
* @param {string|RegExp|Function} match - id matcher
|
|
214
|
+
* @param {Object} mapping - { buttons:{name:index}, axes:{name:index} };
|
|
215
|
+
* merged over the standard table, so you only specify what differs.
|
|
216
|
+
*/
|
|
217
|
+
static registerGamepadMapping(match, mapping = {}) {
|
|
218
|
+
const test =
|
|
219
|
+
typeof match === "function"
|
|
220
|
+
? match
|
|
221
|
+
: match instanceof RegExp
|
|
222
|
+
? (id) => match.test(id)
|
|
223
|
+
: (id) =>
|
|
224
|
+
(id || "").toLowerCase().includes(String(match).toLowerCase());
|
|
225
|
+
CUSTOM_MAPPINGS.unshift({
|
|
226
|
+
test,
|
|
227
|
+
buttons: { ...STANDARD_BUTTONS, ...(mapping.buttons || {}) },
|
|
228
|
+
axes: { ...STANDARD_AXES, ...(mapping.axes || {}) },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
constructor(options = {}) {
|
|
233
|
+
this.target =
|
|
234
|
+
options.target || (typeof window !== "undefined" ? window : null);
|
|
235
|
+
this.actions = new Map();
|
|
236
|
+
this.down = new Set();
|
|
237
|
+
this.prevDown = new Set();
|
|
238
|
+
this.mouse = { x: 0, y: 0, buttons: new Set() };
|
|
239
|
+
this.touches = [];
|
|
240
|
+
this.gamepads = [];
|
|
241
|
+
|
|
242
|
+
this.gamepadDeadzone = options.gamepadDeadzone ?? 0.3;
|
|
243
|
+
this.gamepadCurve = options.gamepadCurve ?? 1;
|
|
244
|
+
this.stickPressThreshold = options.stickPressThreshold ?? 0.5;
|
|
245
|
+
/** @private */
|
|
246
|
+
this._gamepadTokens = new Set();
|
|
247
|
+
/** @private */
|
|
248
|
+
this._connectHandlers = [];
|
|
249
|
+
/** @private */
|
|
250
|
+
this._disconnectHandlers = [];
|
|
251
|
+
/** @private */
|
|
252
|
+
this._connected = new Map();
|
|
253
|
+
/**
|
|
254
|
+
* Which device last produced real input: "keyboard", "mouse",
|
|
255
|
+
* "gamepad", "touch", or null before anything has happened yet. See
|
|
256
|
+
* {@link getLastActiveDevice}.
|
|
257
|
+
* @private
|
|
258
|
+
*/
|
|
259
|
+
this._lastActiveDevice = null;
|
|
260
|
+
/** Pending {@link identifyButton} calls, resolved from `update()`. @private */
|
|
261
|
+
this._buttonWaiters = [];
|
|
262
|
+
|
|
263
|
+
/** @private */
|
|
264
|
+
this._onKeyDown = (e) => {
|
|
265
|
+
this.down.add(this._normKey(e.key));
|
|
266
|
+
this._lastActiveDevice = "keyboard";
|
|
267
|
+
};
|
|
268
|
+
/** @private */
|
|
269
|
+
this._onKeyUp = (e) => this.down.delete(this._normKey(e.key));
|
|
270
|
+
/** @private */
|
|
271
|
+
this._onMouseDown = (e) => {
|
|
272
|
+
this.down.add(`mouse:${e.button}`);
|
|
273
|
+
this.mouse.buttons.add(e.button);
|
|
274
|
+
this._lastActiveDevice = "mouse";
|
|
275
|
+
};
|
|
276
|
+
/** @private */
|
|
277
|
+
this._onMouseUp = (e) => {
|
|
278
|
+
this.down.delete(`mouse:${e.button}`);
|
|
279
|
+
this.mouse.buttons.delete(e.button);
|
|
280
|
+
};
|
|
281
|
+
/** @private */
|
|
282
|
+
this._onMouseMove = (e) => {
|
|
283
|
+
this.mouse.x = e.clientX;
|
|
284
|
+
this.mouse.y = e.clientY;
|
|
285
|
+
};
|
|
286
|
+
/** @private */
|
|
287
|
+
this._onTouch = (e) => {
|
|
288
|
+
this.touches = Array.from(e.touches || []).map((t) => ({
|
|
289
|
+
x: t.clientX,
|
|
290
|
+
y: t.clientY,
|
|
291
|
+
id: t.identifier,
|
|
292
|
+
}));
|
|
293
|
+
this._lastActiveDevice = "touch";
|
|
294
|
+
};
|
|
295
|
+
/** @private */
|
|
296
|
+
this._onGamepadConnected = (e) => {
|
|
297
|
+
const gp = e.gamepad;
|
|
298
|
+
if (!gp) return;
|
|
299
|
+
const info = this._infoFromPad(gp);
|
|
300
|
+
this._connected.set(gp.index, info);
|
|
301
|
+
for (const h of this._connectHandlers) h(info);
|
|
302
|
+
};
|
|
303
|
+
/** @private */
|
|
304
|
+
this._onGamepadDisconnected = (e) => {
|
|
305
|
+
const gp = e.gamepad;
|
|
306
|
+
if (!gp) return;
|
|
307
|
+
const info = this._connected.get(gp.index) || this._infoFromPad(gp);
|
|
308
|
+
this._connected.delete(gp.index);
|
|
309
|
+
for (const h of this._disconnectHandlers) h(info);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (this.target) {
|
|
313
|
+
this.target.addEventListener("keydown", this._onKeyDown);
|
|
314
|
+
this.target.addEventListener("keyup", this._onKeyUp);
|
|
315
|
+
this.target.addEventListener("mousedown", this._onMouseDown);
|
|
316
|
+
this.target.addEventListener("mouseup", this._onMouseUp);
|
|
317
|
+
this.target.addEventListener("mousemove", this._onMouseMove);
|
|
318
|
+
this.target.addEventListener("touchstart", this._onTouch);
|
|
319
|
+
this.target.addEventListener("touchmove", this._onTouch);
|
|
320
|
+
this.target.addEventListener("touchend", this._onTouch);
|
|
321
|
+
this.target.addEventListener(
|
|
322
|
+
"gamepadconnected",
|
|
323
|
+
this._onGamepadConnected
|
|
324
|
+
);
|
|
325
|
+
this.target.addEventListener(
|
|
326
|
+
"gamepaddisconnected",
|
|
327
|
+
this._onGamepadDisconnected
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @private */
|
|
333
|
+
_normKey(key) {
|
|
334
|
+
return key && key.length <= 1 ? key.toLowerCase() : key;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @method mapAction
|
|
339
|
+
* @description Binds an action name to one or more input tokens (keys,
|
|
340
|
+
* "mouse:0", or any gamepad token above).
|
|
341
|
+
* @param {string} name - Action name
|
|
342
|
+
* @param {string[]} tokens - Input tokens
|
|
343
|
+
* @returns {InputManager} - this
|
|
344
|
+
*/
|
|
345
|
+
mapAction(name, tokens) {
|
|
346
|
+
this.actions.set(
|
|
347
|
+
name,
|
|
348
|
+
tokens.map((t) => this._normKey(t))
|
|
349
|
+
);
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @private */
|
|
354
|
+
_tokensFor(actionOrToken) {
|
|
355
|
+
return this.actions.get(actionOrToken) || [this._normKey(actionOrToken)];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** @private */
|
|
359
|
+
_tokenDown(token) {
|
|
360
|
+
if (this.down.has(token)) return true;
|
|
361
|
+
if (this._gamepadTokens.has(token)) return true;
|
|
362
|
+
if (token.startsWith("gamepad:")) {
|
|
363
|
+
const idx = parseInt(token.slice(8), 10);
|
|
364
|
+
const gp = this.gamepads[0];
|
|
365
|
+
return !!(gp && gp.buttons[idx] && gp.buttons[idx].pressed);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @method isDown
|
|
372
|
+
* @description Whether the action (or raw token) is currently held.
|
|
373
|
+
*/
|
|
374
|
+
isDown(actionOrToken) {
|
|
375
|
+
return this._tokensFor(actionOrToken).some((t) => this._tokenDown(t));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @method justPressed
|
|
380
|
+
* @description Whether the action became pressed this frame.
|
|
381
|
+
*/
|
|
382
|
+
justPressed(actionOrToken) {
|
|
383
|
+
return this._tokensFor(actionOrToken).some(
|
|
384
|
+
(t) => this.down.has(t) && !this.prevDown.has(t)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @method justReleased
|
|
390
|
+
* @description Whether the action was released this frame.
|
|
391
|
+
*/
|
|
392
|
+
justReleased(actionOrToken) {
|
|
393
|
+
return this._tokensFor(actionOrToken).some(
|
|
394
|
+
(t) => !this.down.has(t) && this.prevDown.has(t)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @method getAxis
|
|
400
|
+
* @description Returns -1/0/+1 based on two opposing actions.
|
|
401
|
+
*/
|
|
402
|
+
getAxis(negative, positive) {
|
|
403
|
+
return (this.isDown(positive) ? 1 : 0) - (this.isDown(negative) ? 1 : 0);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @method getGamepad
|
|
408
|
+
* @description Returns the polled gamepad at an index (or null).
|
|
409
|
+
*/
|
|
410
|
+
getGamepad(index = 0) {
|
|
411
|
+
return this.gamepads[index] || null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @method setGamepadDeadzone
|
|
416
|
+
* @description Sets the analog deadzone (0..1) for axes and trigger buttons.
|
|
417
|
+
* @returns {InputManager} - this
|
|
418
|
+
*/
|
|
419
|
+
setGamepadDeadzone(dz) {
|
|
420
|
+
this.gamepadDeadzone = dz;
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @method setGamepadCurve
|
|
426
|
+
* @description Sets the response-curve exponent applied to deadzoned analog
|
|
427
|
+
* values: 1 = linear (default), >1 = softer near center for fine aiming.
|
|
428
|
+
* @returns {InputManager} - this
|
|
429
|
+
*/
|
|
430
|
+
setGamepadCurve(exp) {
|
|
431
|
+
this.gamepadCurve = exp;
|
|
432
|
+
return this;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* @method _rescaleAxis
|
|
437
|
+
* @description Deadzone + rescale + curve for a single axis value. Unlike a
|
|
438
|
+
* hard cutoff, the output ramps smoothly from 0 at the deadzone edge to ±1
|
|
439
|
+
* at full deflection (no jump, and the full range stays reachable).
|
|
440
|
+
* @private
|
|
441
|
+
*/
|
|
442
|
+
_rescaleAxis(v, dz = this.gamepadDeadzone, curve = this.gamepadCurve) {
|
|
443
|
+
const a = Math.abs(v);
|
|
444
|
+
if (a < dz) return 0;
|
|
445
|
+
let t = Math.min(1, (a - dz) / (1 - dz || 1));
|
|
446
|
+
if (curve !== 1) t = Math.pow(t, curve);
|
|
447
|
+
return v < 0 ? -t : t;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @method _buttonMapFor
|
|
452
|
+
* @description Resolves the button-name → index table for a gamepad: a
|
|
453
|
+
* registered custom mapping (matched by id) wins; otherwise the standard
|
|
454
|
+
* table is used (also as the best-effort fallback for unknown non-standard
|
|
455
|
+
* pads).
|
|
456
|
+
* @private
|
|
457
|
+
*/
|
|
458
|
+
_buttonMapFor(gp) {
|
|
459
|
+
if (gp.mapping !== "standard") {
|
|
460
|
+
const id = gp.id || "";
|
|
461
|
+
for (const m of CUSTOM_MAPPINGS) {
|
|
462
|
+
if (m.test(id)) return m.buttons;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return STANDARD_BUTTONS;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* @method getGamepadAxis
|
|
470
|
+
* @description Returns a deadzoned analog axis value (-1..1). The value is
|
|
471
|
+
* RESCALED past the deadzone (0 at the edge, ±1 at full deflection) and runs
|
|
472
|
+
* through the response curve, so there is no jump at the threshold. Standard
|
|
473
|
+
* mapping: axis 0/1 = left stick X/Y, 2/3 = right stick X/Y.
|
|
474
|
+
* @param {number} axisIndex
|
|
475
|
+
* @param {number} [padIndex=0]
|
|
476
|
+
* @returns {number}
|
|
477
|
+
*/
|
|
478
|
+
getGamepadAxis(axisIndex, padIndex = 0) {
|
|
479
|
+
const gp = this.gamepads[padIndex];
|
|
480
|
+
if (!gp || !gp.axes) return 0;
|
|
481
|
+
return this._rescaleAxis(gp.axes[axisIndex] || 0);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @method getGamepadStick
|
|
486
|
+
* @description Returns the left or right stick with a RADIAL deadzone: the
|
|
487
|
+
* deadzone applies to the stick's distance from center (not per axis), so
|
|
488
|
+
* diagonals aren't clipped square and direction is preserved exactly. The
|
|
489
|
+
* magnitude is rescaled to use the full 0..1 range and shaped by the
|
|
490
|
+
* response curve.
|
|
491
|
+
* @param {"left"|"right"} [side="left"]
|
|
492
|
+
* @param {number} [padIndex=0]
|
|
493
|
+
* @param {Object} [opts] - { deadzone, curve, invertY } overrides. invertY
|
|
494
|
+
* flips the browser's y-down convention so up = +1.
|
|
495
|
+
* @returns {{x:number, y:number, magnitude:number, angle:number}}
|
|
496
|
+
*/
|
|
497
|
+
getGamepadStick(side = "left", padIndex = 0, opts = {}) {
|
|
498
|
+
const gp = this.gamepads[padIndex];
|
|
499
|
+
if (!gp || !gp.axes) return { x: 0, y: 0, magnitude: 0, angle: 0 };
|
|
500
|
+
const map = this._axisMapFor(gp);
|
|
501
|
+
const ax = side === "right" ? map.rightX : map.leftX;
|
|
502
|
+
const ay = side === "right" ? map.rightY : map.leftY;
|
|
503
|
+
const rx = gp.axes[ax] || 0;
|
|
504
|
+
const ry = gp.axes[ay] || 0;
|
|
505
|
+
const dz = opts.deadzone ?? this.gamepadDeadzone;
|
|
506
|
+
const curve = opts.curve ?? this.gamepadCurve;
|
|
507
|
+
|
|
508
|
+
const mag = Math.hypot(rx, ry);
|
|
509
|
+
if (mag < dz) return { x: 0, y: 0, magnitude: 0, angle: 0 };
|
|
510
|
+
let t = Math.min(1, (mag - dz) / (1 - dz || 1));
|
|
511
|
+
if (curve !== 1) t = Math.pow(t, curve);
|
|
512
|
+
const s = t / mag;
|
|
513
|
+
const x = rx * s;
|
|
514
|
+
const y = (opts.invertY ? -ry : ry) * s;
|
|
515
|
+
return { x, y, magnitude: t, angle: Math.atan2(y, x) };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @method _axisMapFor
|
|
520
|
+
* @description Axis-name table for a pad (custom/built-in mapping wins for
|
|
521
|
+
* non-standard pads, else the standard table).
|
|
522
|
+
* @private
|
|
523
|
+
*/
|
|
524
|
+
_axisMapFor(gp) {
|
|
525
|
+
if (gp.mapping !== "standard") {
|
|
526
|
+
const id = gp.id || "";
|
|
527
|
+
for (const m of CUSTOM_MAPPINGS) {
|
|
528
|
+
if (m.test(id)) return m.axes;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return STANDARD_AXES;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @method getGamepadButton
|
|
536
|
+
* @description Looks up a button by semantic name (e.g. "south", "a", "r1",
|
|
537
|
+
* "start", "dpadLeft") or raw index through the pad's active mapping.
|
|
538
|
+
* @param {string|number} name
|
|
539
|
+
* @param {number} [padIndex=0]
|
|
540
|
+
* @returns {{pressed:boolean, value:number, index:number}}
|
|
541
|
+
*/
|
|
542
|
+
getGamepadButton(name, padIndex = 0) {
|
|
543
|
+
const gp = this.gamepads[padIndex];
|
|
544
|
+
if (!gp || !gp.buttons) return { pressed: false, value: 0, index: -1 };
|
|
545
|
+
const map = this._buttonMapFor(gp);
|
|
546
|
+
const idx = typeof name === "number" ? name : map[name];
|
|
547
|
+
if (idx == null) return { pressed: false, value: 0, index: -1 };
|
|
548
|
+
const btn = gp.buttons[idx];
|
|
549
|
+
const value = btn ? btn.value || 0 : 0;
|
|
550
|
+
return {
|
|
551
|
+
pressed: !!(btn && (btn.pressed || value > this.gamepadDeadzone)),
|
|
552
|
+
value,
|
|
553
|
+
index: idx,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @method getGamepadTrigger
|
|
559
|
+
* @description Returns the analog value (0..1) of a trigger.
|
|
560
|
+
* @param {"left"|"right"} [side="left"]
|
|
561
|
+
* @param {number} [padIndex=0]
|
|
562
|
+
* @returns {number}
|
|
563
|
+
*/
|
|
564
|
+
getGamepadTrigger(side = "left", padIndex = 0) {
|
|
565
|
+
return this.getGamepadButton(side === "right" ? "r2" : "l2", padIndex)
|
|
566
|
+
.value;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* @method rumble
|
|
571
|
+
* @description Plays a vibration effect on a pad (where supported).
|
|
572
|
+
* @param {number} [padIndex=0]
|
|
573
|
+
* @param {Object} [opts] - { duration=200, strong=1, weak=1 }
|
|
574
|
+
* @returns {Promise|undefined}
|
|
575
|
+
*/
|
|
576
|
+
rumble(padIndex = 0, opts = {}) {
|
|
577
|
+
const gp = this.gamepads[padIndex];
|
|
578
|
+
const act =
|
|
579
|
+
gp &&
|
|
580
|
+
(gp.vibrationActuator || (gp.hapticActuators && gp.hapticActuators[0]));
|
|
581
|
+
if (!act) return;
|
|
582
|
+
const { duration = 200, strong = 1, weak = 1 } = opts;
|
|
583
|
+
if (typeof act.playEffect === "function") {
|
|
584
|
+
return act.playEffect("dual-rumble", {
|
|
585
|
+
duration,
|
|
586
|
+
strongMagnitude: strong,
|
|
587
|
+
weakMagnitude: weak,
|
|
588
|
+
startDelay: 0,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (typeof act.pulse === "function")
|
|
592
|
+
return act.pulse(Math.max(strong, weak), duration);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* @method onGamepadConnected
|
|
597
|
+
* @description Registers a callback fired when a pad connects (also called for
|
|
598
|
+
* pads already connected at registration time). Receives an info object.
|
|
599
|
+
* @param {Function} cb
|
|
600
|
+
* @returns {InputManager} - this
|
|
601
|
+
*/
|
|
602
|
+
onGamepadConnected(cb) {
|
|
603
|
+
this._connectHandlers.push(cb);
|
|
604
|
+
for (const info of this.getConnectedGamepads()) cb(info);
|
|
605
|
+
return this;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @method onGamepadDisconnected
|
|
610
|
+
* @description Registers a callback fired when a pad disconnects.
|
|
611
|
+
* @returns {InputManager} - this
|
|
612
|
+
*/
|
|
613
|
+
onGamepadDisconnected(cb) {
|
|
614
|
+
this._disconnectHandlers.push(cb);
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** @private */
|
|
619
|
+
_infoFromPad(gp) {
|
|
620
|
+
return {
|
|
621
|
+
index: gp.index,
|
|
622
|
+
id: gp.id || "",
|
|
623
|
+
mapping: gp.mapping || "",
|
|
624
|
+
buttonCount: (gp.buttons || []).length,
|
|
625
|
+
axesCount: (gp.axes || []).length,
|
|
626
|
+
standard: gp.mapping === "standard",
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @method getGamepadInfo
|
|
632
|
+
* @description Diagnostic info for a connected pad (id, mapping, counts).
|
|
633
|
+
* @param {number} [padIndex=0]
|
|
634
|
+
* @returns {Object|null}
|
|
635
|
+
*/
|
|
636
|
+
getGamepadInfo(padIndex = 0) {
|
|
637
|
+
const gp = this.gamepads[padIndex];
|
|
638
|
+
return gp ? this._infoFromPad(gp) : null;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @method getConnectedGamepads
|
|
643
|
+
* @description Info for every currently polled pad.
|
|
644
|
+
* @returns {Object[]}
|
|
645
|
+
*/
|
|
646
|
+
getConnectedGamepads() {
|
|
647
|
+
return this.gamepads.filter(Boolean).map((gp) => this._infoFromPad(gp));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* @method isGamepadConnected
|
|
652
|
+
* @returns {boolean}
|
|
653
|
+
*/
|
|
654
|
+
isGamepadConnected(padIndex = 0) {
|
|
655
|
+
return !!this.gamepads[padIndex];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* @method getLastActiveDevice
|
|
660
|
+
* @description Which device the player most recently *actually used*,
|
|
661
|
+
* not just "is a gamepad plugged in" (see `isGamepadConnected`, which stays
|
|
662
|
+
* true all game long once one is), but "did they just press a key, click,
|
|
663
|
+
* touch, or move a gamepad button/stick". A pad being connected doesn't
|
|
664
|
+
* mean it's what's driving the game right now; this is the signal for
|
|
665
|
+
* swapping on-screen prompts between keyboard and gamepad button icons as
|
|
666
|
+
* the player actually switches between them mid-session.
|
|
667
|
+
*
|
|
668
|
+
* Only counts deliberate input: keydown, mousedown, and touch, not
|
|
669
|
+
* incidental mouse movement, so idly nudging the mouse while playing on a
|
|
670
|
+
* pad won't flip prompts back to keyboard/mouse.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* // once per frame, after input.update():
|
|
674
|
+
* hud.setPromptStyle(input.getLastActiveDevice() === "gamepad" ? "pad" : "keyboard");
|
|
675
|
+
*
|
|
676
|
+
* @returns {"keyboard"|"mouse"|"gamepad"|"touch"|null} - null before any input at all
|
|
677
|
+
*/
|
|
678
|
+
getLastActiveDevice() {
|
|
679
|
+
return this._lastActiveDevice;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* @method getPressedButtons
|
|
684
|
+
* @description Diagnostic: raw indices of all currently pressed buttons on a
|
|
685
|
+
* pad. Handy for discovering an unknown controller's layout.
|
|
686
|
+
* @param {number} [padIndex=0]
|
|
687
|
+
* @returns {number[]}
|
|
688
|
+
*/
|
|
689
|
+
getPressedButtons(padIndex = 0) {
|
|
690
|
+
const gp = this.gamepads[padIndex];
|
|
691
|
+
if (!gp || !gp.buttons) return [];
|
|
692
|
+
const out = [];
|
|
693
|
+
for (let b = 0; b < gp.buttons.length; b++) {
|
|
694
|
+
const btn = gp.buttons[b];
|
|
695
|
+
if (btn && (btn.pressed || (btn.value || 0) > this.gamepadDeadzone))
|
|
696
|
+
out.push(b);
|
|
697
|
+
}
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @method identifyButton
|
|
703
|
+
* @description Resolves with the raw index of the next *new* button press
|
|
704
|
+
* on a pad: the reliable way to support a controller whose layout isn't
|
|
705
|
+
* already covered by the standard/registered mapping tables (a Steam
|
|
706
|
+
* Controller running outside Steam Input, an old flight stick, anything
|
|
707
|
+
* with a scrambled button order), instead of guessing indices: ask for one
|
|
708
|
+
* physical press and record wherever it actually lands on that specific
|
|
709
|
+
* device. Keep calling `update()` as normal while waiting; the promise
|
|
710
|
+
* resolves on the frame a button transitions from up to down. A button
|
|
711
|
+
* already held when this is called doesn't count; only a fresh press does.
|
|
712
|
+
*
|
|
713
|
+
* @example
|
|
714
|
+
* console.log("Press the button you want for Jump…");
|
|
715
|
+
* const index = await input.identifyButton(0);
|
|
716
|
+
* InputManager.registerGamepadMapping(input.getGamepadInfo(0).id, {
|
|
717
|
+
* buttons: { south: index },
|
|
718
|
+
* });
|
|
719
|
+
*
|
|
720
|
+
* @param {number} [padIndex=0]
|
|
721
|
+
* @returns {Promise<number>} - Never resolves if no new button is ever pressed
|
|
722
|
+
*/
|
|
723
|
+
identifyButton(padIndex = 0) {
|
|
724
|
+
return new Promise((resolve) => {
|
|
725
|
+
this._buttonWaiters.push({
|
|
726
|
+
padIndex,
|
|
727
|
+
before: new Set(this.getPressedButtons(padIndex)),
|
|
728
|
+
resolve,
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* @method calibrateGamepad
|
|
735
|
+
* @description Walks a list of semantic button names one at a time, asking
|
|
736
|
+
* for a physical press for each (see {@link identifyButton}), and resolves
|
|
737
|
+
* with a `{name: rawIndex}` map ready to hand straight to
|
|
738
|
+
* `InputManager.registerGamepadMapping`, a complete fix for an oddball
|
|
739
|
+
* controller in a few seconds, without knowing anything about its layout
|
|
740
|
+
* in advance.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* const mapping = await input.calibrateGamepad(
|
|
744
|
+
* 0,
|
|
745
|
+
* ["south", "east", "west", "north", "l1", "r1", "start"],
|
|
746
|
+
* (name, i, total) => showPrompt(`(${i + 1}/${total}) Press the button for "${name}"`)
|
|
747
|
+
* );
|
|
748
|
+
* InputManager.registerGamepadMapping(input.getGamepadInfo(0).id, { buttons: mapping });
|
|
749
|
+
*
|
|
750
|
+
* @param {number} padIndex
|
|
751
|
+
* @param {string[]} names - Semantic names to calibrate, e.g. ["south", "east", "start"]
|
|
752
|
+
* @param {(name: string, index: number, total: number) => void} [onPrompt] -
|
|
753
|
+
* Called right before waiting for each name, so you can show "press ___" UI
|
|
754
|
+
* @returns {Promise<Object.<string, number>>} - { [name]: rawButtonIndex }
|
|
755
|
+
*/
|
|
756
|
+
async calibrateGamepad(padIndex, names, onPrompt) {
|
|
757
|
+
const mapping = {};
|
|
758
|
+
for (let i = 0; i < names.length; i++) {
|
|
759
|
+
const name = names[i];
|
|
760
|
+
if (onPrompt) onPrompt(name, i, names.length);
|
|
761
|
+
mapping[name] = await this.identifyButton(padIndex);
|
|
762
|
+
}
|
|
763
|
+
return mapping;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* @method _decodeHat
|
|
768
|
+
* @description Some non-standard pads report the d-pad as an 8-way "hat" on a
|
|
769
|
+
* single axis instead of buttons 12-15. When a pad is non-standard and exposes
|
|
770
|
+
* an extra (odd) trailing axis, decode it into d-pad direction tokens.
|
|
771
|
+
* @private
|
|
772
|
+
*/
|
|
773
|
+
_decodeHat(gp, i, set) {
|
|
774
|
+
if (gp.mapping === "standard") return;
|
|
775
|
+
const axes = gp.axes || [];
|
|
776
|
+
if (axes.length < 5 || axes.length % 2 === 0) return;
|
|
777
|
+
const v = axes[axes.length - 1];
|
|
778
|
+
if (v == null || v > 1.2 || v < -1.2) return;
|
|
779
|
+
const dirs = [
|
|
780
|
+
["dpadUp"],
|
|
781
|
+
["dpadUp", "dpadRight"],
|
|
782
|
+
["dpadRight"],
|
|
783
|
+
["dpadDown", "dpadRight"],
|
|
784
|
+
["dpadDown"],
|
|
785
|
+
["dpadDown", "dpadLeft"],
|
|
786
|
+
["dpadLeft"],
|
|
787
|
+
["dpadUp", "dpadLeft"],
|
|
788
|
+
];
|
|
789
|
+
const bucket = Math.round((v + 1) * 3.5);
|
|
790
|
+
const names = dirs[Math.max(0, Math.min(7, bucket))];
|
|
791
|
+
for (const n of names) set.add(`pad:${i}:${n}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* @method _collectGamepadTokens
|
|
796
|
+
* @description Rebuilds the active gamepad token set (raw indices, semantic
|
|
797
|
+
* names via the pad's mapping, d-pad, and axis directions) for every connected
|
|
798
|
+
* pad, so gamepad input flows through the same down/justPressed machinery as
|
|
799
|
+
* the keyboard.
|
|
800
|
+
* @private
|
|
801
|
+
*/
|
|
802
|
+
_collectGamepadTokens() {
|
|
803
|
+
const set = this._gamepadTokens;
|
|
804
|
+
set.clear();
|
|
805
|
+
const dz = this.gamepadDeadzone;
|
|
806
|
+
|
|
807
|
+
for (let i = 0; i < this.gamepads.length; i++) {
|
|
808
|
+
const gp = this.gamepads[i];
|
|
809
|
+
if (!gp) continue;
|
|
810
|
+
const map = this._buttonMapFor(gp);
|
|
811
|
+
const buttons = gp.buttons || [];
|
|
812
|
+
|
|
813
|
+
for (let b = 0; b < buttons.length; b++) {
|
|
814
|
+
const btn = buttons[b];
|
|
815
|
+
if (btn && (btn.pressed || (btn.value || 0) > dz)) {
|
|
816
|
+
set.add(`pad:${i}:${b}`);
|
|
817
|
+
if (i === 0) set.add(`gamepad:${b}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
for (const name in map) {
|
|
821
|
+
const btn = buttons[map[name]];
|
|
822
|
+
if (btn && (btn.pressed || (btn.value || 0) > dz))
|
|
823
|
+
set.add(`pad:${i}:${name}`);
|
|
824
|
+
}
|
|
825
|
+
this._decodeHat(gp, i, set);
|
|
826
|
+
|
|
827
|
+
const axes = gp.axes || [];
|
|
828
|
+
for (let a = 0; a < axes.length; a++) {
|
|
829
|
+
const v = axes[a] || 0;
|
|
830
|
+
if (v <= -dz) set.add(`pad:${i}:axis${a}-`);
|
|
831
|
+
else if (v >= dz) set.add(`pad:${i}:axis${a}+`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const axisMap = this._axisMapFor(gp);
|
|
835
|
+
const press = this.stickPressThreshold;
|
|
836
|
+
for (const name in STICK_DIRECTIONS) {
|
|
837
|
+
const [axisName, sign] = STICK_DIRECTIONS[name];
|
|
838
|
+
const idx = axisMap[axisName];
|
|
839
|
+
if (idx == null || axes[idx] == null) continue;
|
|
840
|
+
const v = this._rescaleAxis(axes[idx]);
|
|
841
|
+
if (sign * v >= press) set.add(`pad:${i}:${name}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return set;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* @method update
|
|
849
|
+
* @description Polls gamepads and advances edge-detection state. Call once
|
|
850
|
+
* per frame after reading input.
|
|
851
|
+
*/
|
|
852
|
+
update() {
|
|
853
|
+
if (typeof navigator !== "undefined" && navigator.getGamepads) {
|
|
854
|
+
this.gamepads = Array.from(navigator.getGamepads()).filter(Boolean);
|
|
855
|
+
}
|
|
856
|
+
this.prevDown = new Set(this.down);
|
|
857
|
+
for (const t of this._gamepadTokens) this.down.delete(t);
|
|
858
|
+
this._collectGamepadTokens();
|
|
859
|
+
for (const t of this._gamepadTokens) this.down.add(t);
|
|
860
|
+
if (this._gamepadTokens.size > 0) this._lastActiveDevice = "gamepad";
|
|
861
|
+
if (this._buttonWaiters.length) this._resolveButtonWaiters();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* @method _resolveButtonWaiters
|
|
866
|
+
* @description Resolves any {@link identifyButton} calls whose pad just
|
|
867
|
+
* saw a button go from up to down.
|
|
868
|
+
* @private
|
|
869
|
+
*/
|
|
870
|
+
_resolveButtonWaiters() {
|
|
871
|
+
this._buttonWaiters = this._buttonWaiters.filter((waiter) => {
|
|
872
|
+
const now = this.getPressedButtons(waiter.padIndex);
|
|
873
|
+
const fresh = now.find((b) => !waiter.before.has(b));
|
|
874
|
+
if (fresh == null) return true;
|
|
875
|
+
waiter.resolve(fresh);
|
|
876
|
+
return false;
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* @method destroy
|
|
882
|
+
* @description Removes all event listeners.
|
|
883
|
+
*/
|
|
884
|
+
destroy() {
|
|
885
|
+
if (!this.target) return;
|
|
886
|
+
this.target.removeEventListener("keydown", this._onKeyDown);
|
|
887
|
+
this.target.removeEventListener("keyup", this._onKeyUp);
|
|
888
|
+
this.target.removeEventListener("mousedown", this._onMouseDown);
|
|
889
|
+
this.target.removeEventListener("mouseup", this._onMouseUp);
|
|
890
|
+
this.target.removeEventListener("mousemove", this._onMouseMove);
|
|
891
|
+
this.target.removeEventListener("touchstart", this._onTouch);
|
|
892
|
+
this.target.removeEventListener("touchmove", this._onTouch);
|
|
893
|
+
this.target.removeEventListener("touchend", this._onTouch);
|
|
894
|
+
this.target.removeEventListener(
|
|
895
|
+
"gamepadconnected",
|
|
896
|
+
this._onGamepadConnected
|
|
897
|
+
);
|
|
898
|
+
this.target.removeEventListener(
|
|
899
|
+
"gamepaddisconnected",
|
|
900
|
+
this._onGamepadDisconnected
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export default InputManager;
|