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.
Files changed (208) hide show
  1. package/README.md +2198 -968
  2. package/dist/types/index.d.ts +75 -32
  3. package/dist/types/src/Animator.d.ts +50 -0
  4. package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
  5. package/dist/types/src/Camera.d.ts +122 -0
  6. package/dist/types/src/CameraController.d.ts +107 -0
  7. package/dist/types/src/CanvasText.d.ts +91 -0
  8. package/dist/types/src/CollisionLayers.d.ts +58 -0
  9. package/dist/types/{Color.d.ts → src/Color.d.ts} +6 -6
  10. package/dist/types/src/Coroutine.d.ts +111 -0
  11. package/dist/types/src/DebugOverlay.d.ts +86 -0
  12. package/dist/types/src/Drawable.d.ts +271 -0
  13. package/dist/types/src/Easing.d.ts +22 -0
  14. package/dist/types/src/Emerald.d.ts +420 -0
  15. package/dist/types/src/EmeraldDB.d.ts +159 -0
  16. package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
  17. package/dist/types/src/GLUtils.d.ts +4 -0
  18. package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
  19. package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +75 -23
  20. package/dist/types/src/Interpolator.d.ts +66 -0
  21. package/dist/types/src/Material.d.ts +87 -0
  22. package/dist/types/src/MathUtils.d.ts +81 -0
  23. package/dist/types/src/ParticleEmitter.d.ts +131 -0
  24. package/dist/types/src/Physics.d.ts +337 -0
  25. package/dist/types/src/Pool.d.ts +53 -0
  26. package/dist/types/src/PostEffects.d.ts +68 -0
  27. package/dist/types/src/PostProcessor.d.ts +124 -0
  28. package/dist/types/src/RenderTarget.d.ts +56 -0
  29. package/dist/types/src/Scene.d.ts +62 -0
  30. package/dist/types/src/ScreenEffects.d.ts +111 -0
  31. package/dist/types/src/Serializer.d.ts +86 -0
  32. package/dist/types/src/Shaders.d.ts +2 -0
  33. package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
  34. package/dist/types/src/SpatialGrid.d.ts +50 -0
  35. package/dist/types/src/SpriteBatch.d.ts +89 -0
  36. package/dist/types/src/StateMachine.d.ts +59 -0
  37. package/dist/types/src/Storage.d.ts +89 -0
  38. package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
  39. package/dist/types/src/TextureAtlas.d.ts +55 -0
  40. package/dist/types/src/Tilemap.d.ts +91 -0
  41. package/dist/types/src/Time.d.ts +53 -0
  42. package/dist/types/src/Timer.d.ts +54 -0
  43. package/dist/types/src/Transform.d.ts +94 -0
  44. package/dist/types/src/Tween.d.ts +81 -0
  45. package/dist/types/src/UI.d.ts +121 -0
  46. package/dist/types/src/components/Behaviour.d.ts +71 -0
  47. package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
  48. package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
  49. package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
  50. package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
  51. package/dist/types/src/components/Collider.d.ts +102 -0
  52. package/dist/types/src/components/GameObject.d.ts +135 -0
  53. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  54. package/dist/types/src/components/RigidBody.d.ts +456 -0
  55. package/dist/types/src/importers/Aseprite.d.ts +79 -0
  56. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  57. package/dist/types/src/importers/TiledMap.d.ts +62 -0
  58. package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
  59. package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
  60. package/dist/types/src/managers/AssetManager.d.ts +116 -0
  61. package/dist/types/src/managers/AudioManager.d.ts +259 -0
  62. package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
  63. package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
  64. package/dist/types/src/managers/GLManager.d.ts +84 -0
  65. package/dist/types/src/managers/GLState.d.ts +37 -0
  66. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  67. package/dist/types/src/managers/IDManager.d.ts +31 -0
  68. package/dist/types/src/managers/InputManager.d.ts +381 -0
  69. package/dist/types/src/managers/NetworkManager.d.ts +93 -0
  70. package/dist/types/src/managers/RenderStats.d.ts +34 -0
  71. package/dist/types/src/managers/SceneManager.d.ts +44 -0
  72. package/dist/types/src/managers/ShaderManager.d.ts +55 -0
  73. package/dist/types/src/managers/TextureManager.d.ts +102 -0
  74. package/dist/types/src/particlesystem/Particle.d.ts +64 -0
  75. package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
  76. package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
  77. package/dist/types/src/physics/AABB.d.ts +92 -0
  78. package/dist/types/src/physics/Body.d.ts +435 -0
  79. package/dist/types/src/physics/BodyType.d.ts +6 -0
  80. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  81. package/dist/types/src/physics/Collision.d.ts +102 -0
  82. package/dist/types/src/physics/Contact.d.ts +206 -0
  83. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  84. package/dist/types/src/physics/Distance.d.ts +54 -0
  85. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  86. package/dist/types/src/physics/Fixture.d.ts +221 -0
  87. package/dist/types/src/physics/Island.d.ts +52 -0
  88. package/dist/types/src/physics/Joint.d.ts +59 -0
  89. package/dist/types/src/physics/Math2D.d.ts +371 -0
  90. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  91. package/dist/types/src/physics/Settings.d.ts +22 -0
  92. package/dist/types/src/physics/Shapes.d.ts +207 -0
  93. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  94. package/dist/types/src/physics/World.d.ts +274 -0
  95. package/dist/types/src/physics/index.d.ts +34 -0
  96. package/index.js +78 -0
  97. package/package.json +73 -60
  98. package/src/Animator.js +95 -0
  99. package/src/BitmapText.js +6 -5
  100. package/src/Camera.js +183 -0
  101. package/src/CameraController.js +192 -0
  102. package/src/CanvasText.js +281 -0
  103. package/src/CollisionLayers.js +86 -0
  104. package/src/Color.js +26 -18
  105. package/src/Coroutine.js +259 -0
  106. package/src/DebugOverlay.js +246 -0
  107. package/src/Drawable.js +842 -582
  108. package/src/Easing.js +57 -0
  109. package/src/Emerald.js +1150 -459
  110. package/src/EmeraldDB.js +328 -0
  111. package/src/FPSCounter.js +43 -43
  112. package/src/GLUtils.js +60 -67
  113. package/src/Instance.js +41 -5
  114. package/src/InstancedTexture.js +308 -127
  115. package/src/Interpolator.js +124 -0
  116. package/src/Material.js +202 -0
  117. package/src/MathUtils.js +134 -0
  118. package/src/ParticleEmitter.js +284 -0
  119. package/src/Physics.js +431 -40
  120. package/src/Pool.js +85 -0
  121. package/src/PostEffects.js +296 -0
  122. package/src/PostProcessor.js +304 -0
  123. package/src/RenderTarget.js +134 -0
  124. package/src/Scene.js +115 -83
  125. package/src/ScreenEffects.js +266 -0
  126. package/src/Serializer.js +131 -0
  127. package/src/Shaders.js +150 -165
  128. package/src/Shapes.js +118 -129
  129. package/src/SpatialGrid.js +111 -0
  130. package/src/SpriteBatch.js +299 -0
  131. package/src/StateMachine.js +82 -0
  132. package/src/Storage.js +175 -47
  133. package/src/Texture.js +58 -67
  134. package/src/TextureAtlas.js +96 -0
  135. package/src/Tilemap.js +274 -0
  136. package/src/Time.js +51 -6
  137. package/src/Timer.js +99 -0
  138. package/src/Transform.js +100 -7
  139. package/src/Tween.js +160 -0
  140. package/src/UI.js +394 -0
  141. package/src/components/Behaviour.js +90 -0
  142. package/src/components/BoxCollider.js +29 -12
  143. package/src/components/BoxColliderDebug.js +3 -4
  144. package/src/components/CircleCollider.js +26 -12
  145. package/src/components/CircleColliderDebug.js +25 -24
  146. package/src/components/Collider.js +150 -34
  147. package/src/components/GameObject.js +130 -21
  148. package/src/components/PolygonCollider.js +55 -0
  149. package/src/components/RigidBody.js +556 -8
  150. package/src/importers/Aseprite.js +142 -0
  151. package/src/importers/ForgeLevel.js +581 -0
  152. package/src/importers/TiledMap.js +158 -0
  153. package/src/lights/DirectionalLight.js +6 -15
  154. package/src/lights/PointLight.js +4 -4
  155. package/src/managers/AssetManager.js +239 -0
  156. package/src/managers/AudioManager.js +565 -146
  157. package/src/managers/EventManager.js +488 -477
  158. package/src/managers/GLManager.js +57 -0
  159. package/src/managers/GLState.js +70 -0
  160. package/src/managers/Gamepad.js +126 -0
  161. package/src/managers/IDManager.js +24 -2
  162. package/src/managers/InputManager.js +905 -0
  163. package/src/managers/NetworkManager.js +178 -0
  164. package/src/managers/RenderStats.js +34 -0
  165. package/src/managers/SceneManager.js +30 -0
  166. package/src/managers/ShaderManager.js +0 -2
  167. package/src/managers/TextureManager.js +218 -0
  168. package/src/particlesystem/Particle.js +82 -7
  169. package/src/particlesystem/ParticleSettings.js +21 -3
  170. package/src/particlesystem/Particles.js +80 -31
  171. package/src/physics/AABB.js +207 -0
  172. package/src/physics/Body.js +862 -0
  173. package/src/physics/BodyType.js +16 -0
  174. package/src/physics/BroadPhase.js +641 -0
  175. package/src/physics/Collision.js +534 -0
  176. package/src/physics/Contact.js +500 -0
  177. package/src/physics/ContactSolver.js +526 -0
  178. package/src/physics/Distance.js +403 -0
  179. package/src/physics/DistanceJoint.js +227 -0
  180. package/src/physics/Fixture.js +346 -0
  181. package/src/physics/Island.js +203 -0
  182. package/src/physics/Joint.js +78 -0
  183. package/src/physics/Math2D.js +573 -0
  184. package/src/physics/RevoluteJoint.js +278 -0
  185. package/src/physics/Settings.js +78 -0
  186. package/src/physics/Shapes.js +549 -0
  187. package/src/physics/TimeOfImpact.js +87 -0
  188. package/src/physics/World.js +731 -0
  189. package/src/physics/index.js +79 -0
  190. package/dist/types/Drawable.d.ts +0 -157
  191. package/dist/types/Emerald.d.ts +0 -73
  192. package/dist/types/GLUtils.d.ts +0 -6
  193. package/dist/types/Physics.d.ts +0 -139
  194. package/dist/types/Scene.d.ts +0 -39
  195. package/dist/types/Shaders.d.ts +0 -4
  196. package/dist/types/Storage.d.ts +0 -46
  197. package/dist/types/Time.d.ts +0 -22
  198. package/dist/types/Transform.d.ts +0 -41
  199. package/dist/types/components/BoxColliderDebug.d.ts +0 -19
  200. package/dist/types/components/CircleColliderDebug.d.ts +0 -19
  201. package/dist/types/components/Collider.d.ts +0 -53
  202. package/dist/types/components/GameObject.d.ts +0 -72
  203. package/dist/types/components/RigidBody.d.ts +0 -104
  204. package/dist/types/managers/AudioManager.d.ts +0 -60
  205. package/dist/types/managers/GLManager.d.ts +0 -47
  206. package/dist/types/managers/IDManager.d.ts +0 -21
  207. package/dist/types/managers/SceneManager.d.ts +0 -22
  208. 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;