@fonsecabarreto/genesis-gl-core 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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/dist/Camera-DY_8gx3C.d.ts +45 -0
  4. package/dist/Core/classes/Material.d.ts +3 -0
  5. package/dist/Core/classes/Material.js +9 -0
  6. package/dist/Core/classes/Material.js.map +1 -0
  7. package/dist/Core/classes/Model.d.ts +5 -0
  8. package/dist/Core/classes/Model.js +7 -0
  9. package/dist/Core/classes/Model.js.map +1 -0
  10. package/dist/Core/classes/Renderer.d.ts +30 -0
  11. package/dist/Core/classes/Renderer.js +11 -0
  12. package/dist/Core/classes/Renderer.js.map +1 -0
  13. package/dist/Core/classes/Scene.d.ts +37 -0
  14. package/dist/Core/classes/Scene.js +7 -0
  15. package/dist/Core/classes/Scene.js.map +1 -0
  16. package/dist/Core/classes/Viewport.d.ts +37 -0
  17. package/dist/Core/classes/Viewport.js +7 -0
  18. package/dist/Core/classes/Viewport.js.map +1 -0
  19. package/dist/Core/domain/interfaces/Vectors.d.ts +4 -0
  20. package/dist/Core/domain/interfaces/Vectors.js +1 -0
  21. package/dist/Core/domain/interfaces/Vectors.js.map +1 -0
  22. package/dist/Core/index.d.ts +10 -0
  23. package/dist/Core/index.js +51 -0
  24. package/dist/Core/index.js.map +1 -0
  25. package/dist/Core/utils/get-overlap.d.ts +3 -0
  26. package/dist/Core/utils/get-overlap.js +11 -0
  27. package/dist/Core/utils/get-overlap.js.map +1 -0
  28. package/dist/Core/utils/load-glb.d.ts +101 -0
  29. package/dist/Core/utils/load-glb.js +697 -0
  30. package/dist/Core/utils/load-glb.js.map +1 -0
  31. package/dist/Core/utils/parse-obj.d.ts +10 -0
  32. package/dist/Core/utils/parse-obj.js +183 -0
  33. package/dist/Core/utils/parse-obj.js.map +1 -0
  34. package/dist/Editor/index.d.ts +364 -0
  35. package/dist/Editor/index.js +1737 -0
  36. package/dist/Editor/index.js.map +1 -0
  37. package/dist/Game/controls/KeyboardInput.d.ts +8 -0
  38. package/dist/Game/controls/KeyboardInput.js +7 -0
  39. package/dist/Game/controls/KeyboardInput.js.map +1 -0
  40. package/dist/Game/index.d.ts +45 -0
  41. package/dist/Game/index.js +353 -0
  42. package/dist/Game/index.js.map +1 -0
  43. package/dist/KeyboardControl-5w7Vm0J0.d.ts +18 -0
  44. package/dist/KeyboardInput-DTsfj3tE.d.ts +166 -0
  45. package/dist/Material-BGLkldxv.d.ts +74 -0
  46. package/dist/Model-CQvDXd-b.d.ts +302 -0
  47. package/dist/WebGLCore-DR7ZHJB0.d.ts +22 -0
  48. package/dist/chunk-3ULETMWF.js +144 -0
  49. package/dist/chunk-3ULETMWF.js.map +1 -0
  50. package/dist/chunk-5TAAXI6S.js +330 -0
  51. package/dist/chunk-5TAAXI6S.js.map +1 -0
  52. package/dist/chunk-6LS6AO5H.js +296 -0
  53. package/dist/chunk-6LS6AO5H.js.map +1 -0
  54. package/dist/chunk-JK2HEZAT.js +317 -0
  55. package/dist/chunk-JK2HEZAT.js.map +1 -0
  56. package/dist/chunk-P7QOKDLY.js +57 -0
  57. package/dist/chunk-P7QOKDLY.js.map +1 -0
  58. package/dist/chunk-QCQVJCSR.js +968 -0
  59. package/dist/chunk-QCQVJCSR.js.map +1 -0
  60. package/dist/chunk-SUNYSY45.js +81 -0
  61. package/dist/chunk-SUNYSY45.js.map +1 -0
  62. package/package.json +83 -0
@@ -0,0 +1,353 @@
1
+ import {
2
+ KeyboardInput
3
+ } from "../chunk-P7QOKDLY.js";
4
+ import "../chunk-SUNYSY45.js";
5
+ import {
6
+ Mesh
7
+ } from "../chunk-QCQVJCSR.js";
8
+ import "../chunk-6LS6AO5H.js";
9
+ import {
10
+ Model
11
+ } from "../chunk-JK2HEZAT.js";
12
+ import "../chunk-3ULETMWF.js";
13
+ import "../chunk-5TAAXI6S.js";
14
+
15
+ // src/Game/classes/AnimationController.ts
16
+ var AnimationController = class {
17
+ /**
18
+ * @param animations - The model's animation map (same reference).
19
+ * @param playFn - Callback that actually starts an animation clip
20
+ * on the model (e.g. `model.playAnimation`).
21
+ */
22
+ constructor(animations, playFn) {
23
+ this.animations = animations;
24
+ this.playFn = playFn;
25
+ }
26
+ animations;
27
+ playFn;
28
+ states = /* @__PURE__ */ new Map();
29
+ transitions = [];
30
+ currentStateName = null;
31
+ // ── State management ────────────────────────────────────────
32
+ /** Register an animation state. */
33
+ addState(state) {
34
+ this.states.set(state.name, state);
35
+ return this;
36
+ }
37
+ /** Convenience: register multiple states at once. */
38
+ addStates(states) {
39
+ for (const s of states) this.addState(s);
40
+ return this;
41
+ }
42
+ /** Register a transition between two states. */
43
+ addTransition(from, to, condition) {
44
+ this.transitions.push({ from, to, condition });
45
+ return this;
46
+ }
47
+ // ── Playback ────────────────────────────────────────────────
48
+ /** Force-start a specific state (e.g. on init). */
49
+ start(stateName) {
50
+ const state = this.states.get(stateName);
51
+ if (!state) {
52
+ console.warn(`[AnimationController] State "${stateName}" not found`);
53
+ return;
54
+ }
55
+ this.currentStateName = stateName;
56
+ this.playFn(state.clipName, state.loop ?? true);
57
+ const clip = this.animations.get(state.clipName);
58
+ if (clip) clip.speed = state.speed ?? 1;
59
+ }
60
+ /**
61
+ * Evaluate all transitions from the current state.
62
+ * Call this once per frame. The first matching transition wins.
63
+ */
64
+ evaluate() {
65
+ if (!this.currentStateName) return;
66
+ for (const t of this.transitions) {
67
+ if (t.from === this.currentStateName && t.condition()) {
68
+ this.start(t.to);
69
+ return;
70
+ }
71
+ }
72
+ }
73
+ /** The name of the currently active state (or `null`). */
74
+ get currentState() {
75
+ return this.currentStateName;
76
+ }
77
+ };
78
+
79
+ // src/Game/classes/Character.ts
80
+ var Character = class extends Model {
81
+ id;
82
+ health = 100;
83
+ /** Friction applied when grounded — updated from the surface material on landing.
84
+ * 0 = no resistance (pure ice), 1 = instant stop. */
85
+ surfaceFriction = 0.3;
86
+ /** Horizontal damping while airborne. 0 = no air drag, 1 = instant stop. */
87
+ airFriction = 0.02;
88
+ speed = 0.2;
89
+ stamina = 10;
90
+ velocity = [0, 0, 0];
91
+ _isGrounded = false;
92
+ /** Setting to true (landing) resets the jump counter. */
93
+ get isGrounded() {
94
+ return this._isGrounded;
95
+ }
96
+ set isGrounded(v) {
97
+ if (v && !this._isGrounded) {
98
+ const hardLanding = this.velocity[1] <= this.minLandingImpact;
99
+ if (this.isInputActive && hardLanding) this._landingRoll = true;
100
+ this._jumpsUsed = 0;
101
+ this.isDoubleJumping = false;
102
+ }
103
+ this._isGrounded = v;
104
+ }
105
+ /** Pending auto-roll on landing after a double jump. */
106
+ _landingRoll = false;
107
+ /** Minimum downward impact speed (negative Y velocity) required to trigger
108
+ * a landing roll. Tune this value — jumpForce is 0.65, so:
109
+ * -0.20 = any jump from ground, -0.40 = only falls from significant height */
110
+ minLandingImpact = -0.6;
111
+ /** True during the airborne period after a second (mid-air) jump. */
112
+ isDoubleJumping = false;
113
+ /** True while movement keys are held — drives run animation independently of velocity. */
114
+ isInputActive = false;
115
+ jumpForce = 0.65;
116
+ // ── Acceleration system — target velocity set by input each tick —————
117
+ // lerp rate is derived from surface friction so ice feels sluggish and
118
+ // sand feels grippy both when accelerating and decelerating.
119
+ _targetVx = 0;
120
+ _targetVz = 0;
121
+ animController;
122
+ // ── Roll state ───────────────────────────────────────────────
123
+ _isRolling = false;
124
+ _rollTimer = 0;
125
+ _rollDuration = 0.5;
126
+ // seconds
127
+ _rollSpeed = 0.2;
128
+ /** True when rolling was triggered by a hard landing (not a manual roll). */
129
+ isLandingRoll = false;
130
+ /** Direction captured at roll start (world-space XZ). */
131
+ _rollDir = [0, 1];
132
+ // ── Jump state ───────────────────────────────────────────────
133
+ _isJumping = false;
134
+ _jumpTimer = 0;
135
+ _jumpDuration = 0.1;
136
+ // seconds
137
+ _jumpsUsed = 0;
138
+ maxJumps = 2;
139
+ constructor(id, baseModel) {
140
+ super(
141
+ baseModel.meshes,
142
+ [...baseModel.translation],
143
+ [...baseModel.scale],
144
+ baseModel.name,
145
+ baseModel.skeleton?.clone() ?? null
146
+ );
147
+ this.id = id;
148
+ this.rotation = [...baseModel.rotation];
149
+ for (const [name, clip] of baseModel.animations) {
150
+ this.animations.set(name, clip.clone());
151
+ }
152
+ this.updateBoundingBox();
153
+ this.animController = new AnimationController(
154
+ this.animations,
155
+ (clip, loop) => this.playAnimation(clip, loop)
156
+ );
157
+ }
158
+ /** Whether the character has meaningful horizontal velocity.
159
+ * Default threshold is 20% of configured speed — so the walk animation
160
+ * plays until the character has genuinely slowed down, not just slightly. */
161
+ isMoving(threshold = this.speed * 0.6) {
162
+ return Math.abs(this.velocity[0]) > threshold || Math.abs(this.velocity[2]) > threshold;
163
+ }
164
+ /** True while the roll animation is playing. */
165
+ get isRolling() {
166
+ return this._isRolling;
167
+ }
168
+ /** True while the jump animation is playing. */
169
+ get isJumping() {
170
+ return this._isJumping;
171
+ }
172
+ /** Start a jump — first jump requires grounded, second can fire mid-air. */
173
+ jump() {
174
+ if (this._isRolling) return;
175
+ if (this._jumpsUsed >= this.maxJumps) return;
176
+ const isSecondJump = this._jumpsUsed >= 1;
177
+ this.velocity[1] = this.jumpForce;
178
+ if (this._targetVx !== 0 || this._targetVz !== 0) {
179
+ const boost = 0.5;
180
+ this.velocity[0] += this._targetVx * boost;
181
+ this.velocity[2] += this._targetVz * boost;
182
+ }
183
+ this.isGrounded = false;
184
+ this._isJumping = true;
185
+ this._jumpTimer = 0;
186
+ this._jumpsUsed++;
187
+ if (isSecondJump) this.isDoubleJumping = true;
188
+ }
189
+ /** Start a roll — only if grounded and not already rolling/jumping. */
190
+ roll() {
191
+ if (!this.isGrounded || this._isRolling || this._isJumping || !this.isInputActive)
192
+ return;
193
+ this.isLandingRoll = false;
194
+ this._beginRoll();
195
+ }
196
+ /** Internal roll starter — skips input guards (used for forced landing roll). */
197
+ _beginRoll(fromLanding = false) {
198
+ if (!this.isGrounded) return;
199
+ this._isRolling = true;
200
+ this.isLandingRoll = fromLanding;
201
+ this._rollTimer = 0;
202
+ const yaw = this.rotation[1];
203
+ this._rollDir = [Math.sin(yaw), Math.cos(yaw)];
204
+ }
205
+ getDirectionFromCamera(camera) {
206
+ const yaw = camera.yaw;
207
+ const forward = [Math.sin(yaw), 0, Math.cos(yaw)];
208
+ const right = [Math.cos(yaw), 0, -Math.sin(yaw)];
209
+ return { forward, right };
210
+ }
211
+ moveRelative(forwardAmount, rightAmount, camera) {
212
+ const { forward, right } = this.getDirectionFromCamera(camera);
213
+ const vx = forward[0] * forwardAmount + right[0] * rightAmount;
214
+ const vz = forward[2] * forwardAmount + right[2] * rightAmount;
215
+ const len = Math.sqrt(vx * vx + vz * vz);
216
+ const nx = len > 0 ? vx / len : 0;
217
+ const nz = len > 0 ? vz / len : 0;
218
+ const topSpeed = this.speed * (1 - this.surfaceFriction);
219
+ this._targetVx = nx * topSpeed;
220
+ this._targetVz = nz * topSpeed;
221
+ if (nx !== 0 || nz !== 0) {
222
+ this.setRotation(0, Math.atan2(nx, nz), 0);
223
+ }
224
+ this.consumeStamina();
225
+ }
226
+ consumeStamina() {
227
+ this.stamina = Math.max(0, this.stamina - 0.5);
228
+ }
229
+ update(deltaTime = 0.016) {
230
+ if (this._isRolling) {
231
+ this._rollTimer += deltaTime;
232
+ this.velocity[0] = this._rollDir[0] * this._rollSpeed;
233
+ this.velocity[2] = this._rollDir[1] * this._rollSpeed;
234
+ this._targetVx = 0;
235
+ this._targetVz = 0;
236
+ if (this._rollTimer >= this._rollDuration) {
237
+ this._isRolling = false;
238
+ this.isLandingRoll = false;
239
+ }
240
+ } else {
241
+ const fr = this.isGrounded ? this.surfaceFriction : this.airFriction;
242
+ const accel = Math.min(0.9, fr + 5e-3);
243
+ this.velocity[0] += (this._targetVx - this.velocity[0]) * accel;
244
+ this.velocity[2] += (this._targetVz - this.velocity[2]) * accel;
245
+ this._targetVx = 0;
246
+ this._targetVz = 0;
247
+ if (!this.isGrounded && this.velocity[1] < 0) {
248
+ this.velocity[1] *= 1 - this.airFriction * 4;
249
+ }
250
+ }
251
+ if (!this.isGrounded && this._jumpsUsed === 0) {
252
+ this._jumpsUsed = 1;
253
+ }
254
+ if (this._isJumping) {
255
+ this._jumpTimer += deltaTime;
256
+ if (this._jumpTimer >= this._jumpDuration && this.isGrounded) {
257
+ this._isJumping = false;
258
+ if (this._landingRoll && !this._isRolling) {
259
+ this._landingRoll = false;
260
+ this._beginRoll(true);
261
+ }
262
+ }
263
+ } else if (this.isGrounded) {
264
+ this._isJumping = false;
265
+ if (this._landingRoll && !this._isRolling) {
266
+ this._landingRoll = false;
267
+ this._beginRoll(true);
268
+ }
269
+ }
270
+ this.animController.evaluate();
271
+ super.update(deltaTime);
272
+ this.move(this.velocity[0], this.velocity[1], this.velocity[2]);
273
+ if (Math.abs(this.velocity[0]) < 1e-3) this.velocity[0] = 0;
274
+ if (Math.abs(this.velocity[1]) < 1e-3) this.velocity[1] = 0;
275
+ if (Math.abs(this.velocity[2]) < 1e-3) this.velocity[2] = 0;
276
+ }
277
+ stop() {
278
+ this.velocity = [0, 0, 0];
279
+ }
280
+ };
281
+
282
+ // src/Game/classes/BodyPieces/Arms.ts
283
+ var Arm = class extends Mesh {
284
+ swingPhase = 0;
285
+ swingSpeed = 5;
286
+ swingAmount = 0.7;
287
+ side;
288
+ constructor(vertices, normals, material, side = 1) {
289
+ super("character_arm", vertices, normals, material);
290
+ this.side = side;
291
+ }
292
+ /**
293
+ * Updates the animation state for this arm.
294
+ * Returns the desired rotation (in radians) to be applied externally.
295
+ */
296
+ animate(moving, deltaTime, invert = 1) {
297
+ if (moving) {
298
+ this.swingPhase += deltaTime * this.swingSpeed;
299
+ return Math.sin(this.swingPhase) * this.swingAmount * invert * this.side;
300
+ } else {
301
+ this.swingPhase *= 0.9;
302
+ return 0;
303
+ }
304
+ }
305
+ };
306
+
307
+ // src/Game/classes/BodyPieces/Head.ts
308
+ var Head = class _Head extends Mesh {
309
+ constructor(vertices, normals, material) {
310
+ super("character_head", vertices, normals, material);
311
+ }
312
+ /**
313
+ * Create a Head mesh from an existing Mesh.
314
+ * Optionally override translation and scale.
315
+ */
316
+ static fromMesh(mesh) {
317
+ return new _Head(mesh.vertices, mesh.normals, mesh.material);
318
+ }
319
+ };
320
+
321
+ // src/Game/classes/BodyPieces/Leg.ts
322
+ var Leg = class extends Mesh {
323
+ swingAngle = 0;
324
+ swingSpeed = 12;
325
+ swingAmount = 0.4;
326
+ side;
327
+ constructor(vertices, normals, material, side) {
328
+ super("character_leg", vertices, normals, material);
329
+ this.side = side;
330
+ }
331
+ /**
332
+ * Updates the swing animation state for this leg.
333
+ * The actual rotation transform should be applied by a parent model/skeleton.
334
+ */
335
+ animate(moving, deltaTime) {
336
+ if (moving) {
337
+ this.swingAngle += deltaTime * this.swingSpeed;
338
+ return Math.sin(this.swingAngle) * this.swingAmount * this.side;
339
+ } else {
340
+ this.swingAngle *= 0.9;
341
+ return 0;
342
+ }
343
+ }
344
+ };
345
+ export {
346
+ AnimationController,
347
+ Arm,
348
+ Character,
349
+ Head,
350
+ KeyboardInput,
351
+ Leg
352
+ };
353
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Game/classes/AnimationController.ts","../../src/Game/classes/Character.ts","../../src/Game/classes/BodyPieces/Arms.ts","../../src/Game/classes/BodyPieces/Head.ts","../../src/Game/classes/BodyPieces/Leg.ts"],"sourcesContent":["import { AnimationClip } from '../../Core/classes/AnimationClip';\n\n/** A named state that maps to one animation clip. */\nexport interface AnimationState {\n /** Unique state name, e.g. \"idle\", \"run\", \"jump\". */\n name: string;\n /** The animation clip name (key in Model.animations). */\n clipName: string;\n /** Whether the clip should loop (default `true`). */\n loop?: boolean;\n /** Playback speed multiplier (default `1`). 2 = double speed, 0.5 = half. */\n speed?: number;\n}\n\n/**\n * A condition evaluated every frame to decide whether to transition.\n * Return `true` to trigger the transition.\n */\nexport type TransitionCondition = () => boolean;\n\n/** A rule that connects two states. */\nexport interface AnimationTransition {\n from: string;\n to: string;\n condition: TransitionCondition;\n}\n\n/**\n * Decoupled animation state-machine controller.\n *\n * Register states (idle, run, …) and transitions between them,\n * then call `evaluate()` every frame. The controller resolves the\n * current state and drives `playAnimation` on the owner model —\n * no index juggling, no hardcoded names scattered around the codebase.\n *\n * @example\n * ```ts\n * const ctrl = new AnimationController(animations);\n *\n * ctrl.addState({ name: 'idle', clipName: 'Armature|idle' });\n * ctrl.addState({ name: 'run', clipName: 'Armature|run' });\n *\n * ctrl.addTransition('idle', 'run', () => character.isMoving());\n * ctrl.addTransition('run', 'idle', () => !character.isMoving());\n *\n * ctrl.start('idle');\n *\n * // in game loop:\n * ctrl.evaluate();\n * ```\n */\nexport class AnimationController {\n private states = new Map<string, AnimationState>();\n private transitions: AnimationTransition[] = [];\n private currentStateName: string | null = null;\n\n /**\n * @param animations - The model's animation map (same reference).\n * @param playFn - Callback that actually starts an animation clip\n * on the model (e.g. `model.playAnimation`).\n */\n constructor(\n private readonly animations: Map<string, AnimationClip>,\n private readonly playFn: (clipName: string, loop: boolean) => void,\n ) {}\n\n // ── State management ────────────────────────────────────────\n\n /** Register an animation state. */\n addState(state: AnimationState): this {\n this.states.set(state.name, state);\n return this;\n }\n\n /** Convenience: register multiple states at once. */\n addStates(states: AnimationState[]): this {\n for (const s of states) this.addState(s);\n return this;\n }\n\n /** Register a transition between two states. */\n addTransition(\n from: string,\n to: string,\n condition: TransitionCondition,\n ): this {\n this.transitions.push({ from, to, condition });\n return this;\n }\n\n // ── Playback ────────────────────────────────────────────────\n\n /** Force-start a specific state (e.g. on init). */\n start(stateName: string): void {\n const state = this.states.get(stateName);\n if (!state) {\n console.warn(`[AnimationController] State \"${stateName}\" not found`);\n return;\n }\n this.currentStateName = stateName;\n this.playFn(state.clipName, state.loop ?? true);\n // Apply playback speed — must be set after playFn because playAnimation resets the clip\n const clip = this.animations.get(state.clipName);\n if (clip) clip.speed = state.speed ?? 1;\n }\n\n /**\n * Evaluate all transitions from the current state.\n * Call this once per frame. The first matching transition wins.\n */\n evaluate(): void {\n if (!this.currentStateName) return;\n\n for (const t of this.transitions) {\n if (t.from === this.currentStateName && t.condition()) {\n this.start(t.to);\n return; // one transition per frame\n }\n }\n }\n\n /** The name of the currently active state (or `null`). */\n get currentState(): string | null {\n return this.currentStateName;\n }\n}\n","import { Vector3 } from '../../Core/domain/interfaces/Vectors';\nimport { Camera } from '../../Core';\nimport { Model } from '../../Core/classes/Model';\nimport { AnimationController } from './AnimationController';\n\nexport class Character extends Model {\n public id: string;\n health = 100;\n /** Friction applied when grounded — updated from the surface material on landing.\n * 0 = no resistance (pure ice), 1 = instant stop. */\n surfaceFriction = 0.3;\n /** Horizontal damping while airborne. 0 = no air drag, 1 = instant stop. */\n readonly airFriction = 0.02;\n speed = 0.2;\n stamina = 10;\n public velocity: Vector3 = [0, 0, 0];\n private _isGrounded = false;\n /** Setting to true (landing) resets the jump counter. */\n public get isGrounded(): boolean {\n return this._isGrounded;\n }\n public set isGrounded(v: boolean) {\n if (v && !this._isGrounded) {\n // Queue a landing roll only if:\n // 1. player is still pressing a direction\n // 2. impact speed exceeds the minimum threshold (works for jumps AND falls)\n const hardLanding = this.velocity[1] <= this.minLandingImpact;\n if (this.isInputActive && hardLanding) this._landingRoll = true;\n this._jumpsUsed = 0;\n this.isDoubleJumping = false;\n }\n this._isGrounded = v;\n }\n /** Pending auto-roll on landing after a double jump. */\n private _landingRoll = false;\n /** Minimum downward impact speed (negative Y velocity) required to trigger\n * a landing roll. Tune this value — jumpForce is 0.65, so:\n * -0.20 = any jump from ground, -0.40 = only falls from significant height */\n public minLandingImpact = -0.60;\n /** True during the airborne period after a second (mid-air) jump. */\n public isDoubleJumping = false;\n /** True while movement keys are held — drives run animation independently of velocity. */\n public isInputActive = false;\n private jumpForce = 0.65;\n\n // ── Acceleration system — target velocity set by input each tick —————\n // lerp rate is derived from surface friction so ice feels sluggish and\n // sand feels grippy both when accelerating and decelerating.\n private _targetVx = 0;\n private _targetVz = 0;\n public readonly animController: AnimationController;\n\n // ── Roll state ───────────────────────────────────────────────\n private _isRolling = false;\n private _rollTimer = 0;\n private readonly _rollDuration = 0.5; // seconds\n private readonly _rollSpeed = 0.2;\n /** True when rolling was triggered by a hard landing (not a manual roll). */\n public isLandingRoll = false;\n /** Direction captured at roll start (world-space XZ). */\n private _rollDir: [number, number] = [0, 1];\n\n // ── Jump state ───────────────────────────────────────────────\n private _isJumping = false;\n private _jumpTimer = 0;\n private readonly _jumpDuration = 0.1; // seconds\n private _jumpsUsed = 0;\n readonly maxJumps = 2;\n\n constructor(id: string, baseModel: Model) {\n // Clone the base model — pass skeleton so the constructor's bbox\n // computation includes the rootTransform from the very first frame.\n super(\n baseModel.meshes,\n [...baseModel.translation],\n [...baseModel.scale],\n baseModel.name,\n baseModel.skeleton?.clone() ?? null,\n );\n\n this.id = id;\n this.rotation = [...baseModel.rotation];\n\n // Carry over animations so the Character can play them\n for (const [name, clip] of baseModel.animations) {\n this.animations.set(name, clip.clone());\n }\n\n this.updateBoundingBox();\n\n this.animController = new AnimationController(\n this.animations,\n (clip, loop) => this.playAnimation(clip, loop),\n );\n }\n\n /** Whether the character has meaningful horizontal velocity.\n * Default threshold is 20% of configured speed — so the walk animation\n * plays until the character has genuinely slowed down, not just slightly. */\n public isMoving(threshold = this.speed * 0.6): boolean {\n return (\n Math.abs(this.velocity[0]) > threshold ||\n Math.abs(this.velocity[2]) > threshold\n );\n }\n\n /** True while the roll animation is playing. */\n public get isRolling(): boolean {\n return this._isRolling;\n }\n\n /** True while the jump animation is playing. */\n public get isJumping(): boolean {\n return this._isJumping;\n }\n\n /** Start a jump — first jump requires grounded, second can fire mid-air. */\n public jump(): void {\n if (this._isRolling) return;\n if (this._jumpsUsed >= this.maxJumps) return;\n const isSecondJump = this._jumpsUsed >= 1;\n this.velocity[1] = this.jumpForce;\n // Directional boost — if the player has directional input this tick,\n // add an impulse in that direction so jumps feel snappy and purposeful.\n // Factor of 2.5 on top of the normal target velocity gives a clear boost\n // without being excessive.\n if (this._targetVx !== 0 || this._targetVz !== 0) {\n const boost = 0.5;\n this.velocity[0] += this._targetVx * boost;\n this.velocity[2] += this._targetVz * boost;\n }\n this.isGrounded = false;\n this._isJumping = true;\n this._jumpTimer = 0;\n this._jumpsUsed++;\n if (isSecondJump) this.isDoubleJumping = true;\n }\n\n /** Start a roll — only if grounded and not already rolling/jumping. */\n public roll(): void {\n if (\n !this.isGrounded ||\n this._isRolling ||\n this._isJumping ||\n !this.isInputActive\n )\n return;\n this.isLandingRoll = false;\n this._beginRoll();\n }\n\n /** Internal roll starter — skips input guards (used for forced landing roll). */\n private _beginRoll(fromLanding = false): void {\n if (!this.isGrounded) return;\n this._isRolling = true;\n this.isLandingRoll = fromLanding;\n this._rollTimer = 0;\n const yaw = this.rotation[1];\n this._rollDir = [Math.sin(yaw), Math.cos(yaw)];\n }\n\n private getDirectionFromCamera(camera: Camera): {\n forward: Vector3;\n right: Vector3;\n } {\n const yaw = camera.yaw;\n const forward: Vector3 = [Math.sin(yaw), 0, Math.cos(yaw)];\n const right: Vector3 = [Math.cos(yaw), 0, -Math.sin(yaw)];\n return { forward, right };\n }\n\n public moveRelative(\n forwardAmount: number,\n rightAmount: number,\n camera: Camera,\n ) {\n const { forward, right } = this.getDirectionFromCamera(camera);\n const vx = forward[0] * forwardAmount + right[0] * rightAmount;\n const vz = forward[2] * forwardAmount + right[2] * rightAmount;\n\n // Normalise diagonal input so diagonal isn't faster than straight\n const len = Math.sqrt(vx * vx + vz * vz);\n const nx = len > 0 ? vx / len : 0;\n const nz = len > 0 ? vz / len : 0;\n\n // Set desired velocity — update() will lerp toward it this tick.\n // Top speed is scaled by (1 - friction): ice ≈ full speed, sand ≈ 45% speed.\n const topSpeed = this.speed * (1 - this.surfaceFriction);\n this._targetVx = nx * topSpeed;\n this._targetVz = nz * topSpeed;\n\n if (nx !== 0 || nz !== 0) {\n this.setRotation(0, Math.atan2(nx, nz), 0);\n }\n\n this.consumeStamina();\n }\n\n public consumeStamina() {\n this.stamina = Math.max(0, this.stamina - 0.5);\n }\n\n public override update(deltaTime: number = 0.016) {\n // ── Roll tick ——————————————————————————\n if (this._isRolling) {\n this._rollTimer += deltaTime;\n // Drive character forward at roll speed\n this.velocity[0] = this._rollDir[0] * this._rollSpeed;\n this.velocity[2] = this._rollDir[1] * this._rollSpeed;\n // Discard any pending input target\n this._targetVx = 0;\n this._targetVz = 0;\n if (this._rollTimer >= this._rollDuration) {\n this._isRolling = false;\n this.isLandingRoll = false;\n }\n } else {\n // ── Lerp horizontal velocity toward input target —————————\n // accel is derived from friction — lower base (0.005) means\n // very low friction surfaces (ice) take much longer to build speed:\n // ice (0.005) → 0.010 → ~45% speed after 1s — very sluggish\n // grass(0.35) → 0.355 → ~99% speed after 5 ticks — responsive\n // sand (0.55) → 0.555 → instant — heavy/grippy\n const fr = this.isGrounded ? this.surfaceFriction : this.airFriction;\n const accel = Math.min(0.9, fr + 0.005);\n this.velocity[0] += (this._targetVx - this.velocity[0]) * accel;\n this.velocity[2] += (this._targetVz - this.velocity[2]) * accel;\n // Reset target — must be re-set every tick by input or it decays to 0\n this._targetVx = 0;\n this._targetVz = 0;\n\n // Vertical air resistance: damp downward velocity to create terminal velocity.\n // Only applied while falling (velocity[1] < 0) and airborne.\n if (!this.isGrounded && this.velocity[1] < 0) {\n this.velocity[1] *= 1 - this.airFriction * 4;\n }\n }\n\n // ── Jump tick ──────────────────────────────────────────────\n // Consume the \"first jump\" slot when the player walks off a ledge without\n // jumping — prevents getting 2 full mid-air jumps from a fall.\n if (!this.isGrounded && this._jumpsUsed === 0) {\n this._jumpsUsed = 1;\n }\n if (this._isJumping) {\n this._jumpTimer += deltaTime;\n if (this._jumpTimer >= this._jumpDuration && this.isGrounded) {\n this._isJumping = false;\n // Auto-roll on landing if coming from a double jump\n if (this._landingRoll && !this._isRolling) {\n this._landingRoll = false;\n this._beginRoll(true);\n }\n }\n } else if (this.isGrounded) {\n this._isJumping = false;\n if (this._landingRoll && !this._isRolling) {\n this._landingRoll = false;\n this._beginRoll(true);\n }\n }\n\n // Evaluate animation state transitions\n this.animController.evaluate();\n\n // Advance skeletal animation (if any)\n super.update(deltaTime);\n\n // Movement\n this.move(this.velocity[0], this.velocity[1], this.velocity[2]);\n\n // Tiny threshold to stop completely (lerp never reaches exact 0)\n if (Math.abs(this.velocity[0]) < 0.001) this.velocity[0] = 0;\n if (Math.abs(this.velocity[1]) < 0.001) this.velocity[1] = 0;\n if (Math.abs(this.velocity[2]) < 0.001) this.velocity[2] = 0;\n }\n\n public stop() {\n this.velocity = [0, 0, 0];\n }\n}\n","import { Material, Mesh } from '../../../Core';\n\nexport class Arm extends Mesh {\n private swingPhase = 0;\n private swingSpeed = 5;\n private swingAmount = 0.7;\n private side: 1 | -1;\n\n constructor(\n vertices: Float32Array,\n normals: Float32Array,\n material: Material,\n side: 1 | -1 = 1, // right = 1, left = -1\n ) {\n super('character_arm', vertices, normals, material);\n this.side = side;\n }\n\n /**\n * Updates the animation state for this arm.\n * Returns the desired rotation (in radians) to be applied externally.\n */\n public animate(moving: boolean, deltaTime: number, invert = 1): number {\n if (moving) {\n this.swingPhase += deltaTime * this.swingSpeed;\n // Swing opposite arms with side factor and invert option\n return Math.sin(this.swingPhase) * this.swingAmount * invert * this.side;\n } else {\n // When not moving, ease toward rest\n this.swingPhase *= 0.9;\n return 0;\n }\n }\n}\n","import { Material, Mesh } from '../../../Core';\n\nexport class Head extends Mesh {\n constructor(\n vertices: Float32Array,\n normals: Float32Array,\n material: Material,\n ) {\n super('character_head', vertices, normals, material);\n }\n\n /**\n * Create a Head mesh from an existing Mesh.\n * Optionally override translation and scale.\n */\n static fromMesh(mesh: Mesh) {\n return new Head(mesh.vertices, mesh.normals, mesh.material);\n }\n}\n","import { Material, Mesh } from '../../../Core';\n\nexport class Leg extends Mesh {\n private swingAngle = 0;\n private swingSpeed = 12;\n private swingAmount = 0.4;\n private side: 1 | -1;\n\n constructor(\n vertices: Float32Array,\n normals: Float32Array,\n material: Material,\n side: 1 | -1,\n ) {\n super('character_leg', vertices, normals, material);\n this.side = side;\n }\n\n /**\n * Updates the swing animation state for this leg.\n * The actual rotation transform should be applied by a parent model/skeleton.\n */\n public animate(moving: boolean, deltaTime: number): number {\n if (moving) {\n this.swingAngle += deltaTime * this.swingSpeed;\n // Return desired rotation angle (around X axis)\n return Math.sin(this.swingAngle) * this.swingAmount * this.side;\n } else {\n // Gradually relax back to rest\n this.swingAngle *= 0.9;\n return 0;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,18 @@
1
+ type ControlKey = 'up' | 'down' | 'left' | 'right' | 'jump' | 'roll';
2
+ type ControlListener = (key: ControlKey, isPressed: boolean) => void;
3
+ declare class KeyboardControl {
4
+ private pressedKeys;
5
+ private listeners;
6
+ private keyDownHandler?;
7
+ private keyUpHandler?;
8
+ enable(): void;
9
+ disable(): void;
10
+ onChange(listener: ControlListener): void;
11
+ /** Remove a previously registered listener. */
12
+ removeListener(listener: ControlListener): void;
13
+ private notify;
14
+ private mapKey;
15
+ isPressed(key: ControlKey): boolean;
16
+ }
17
+
18
+ export { KeyboardControl as K };
@@ -0,0 +1,166 @@
1
+ import { C as Camera } from './Camera-DY_8gx3C.js';
2
+ import { K as KeyboardControl } from './KeyboardControl-5w7Vm0J0.js';
3
+ import { Vector3 } from './Core/domain/interfaces/Vectors.js';
4
+ import { a as AnimationClip, i as Model } from './Model-CQvDXd-b.js';
5
+
6
+ /** A named state that maps to one animation clip. */
7
+ interface AnimationState {
8
+ /** Unique state name, e.g. "idle", "run", "jump". */
9
+ name: string;
10
+ /** The animation clip name (key in Model.animations). */
11
+ clipName: string;
12
+ /** Whether the clip should loop (default `true`). */
13
+ loop?: boolean;
14
+ /** Playback speed multiplier (default `1`). 2 = double speed, 0.5 = half. */
15
+ speed?: number;
16
+ }
17
+ /**
18
+ * A condition evaluated every frame to decide whether to transition.
19
+ * Return `true` to trigger the transition.
20
+ */
21
+ type TransitionCondition = () => boolean;
22
+ /** A rule that connects two states. */
23
+ interface AnimationTransition {
24
+ from: string;
25
+ to: string;
26
+ condition: TransitionCondition;
27
+ }
28
+ /**
29
+ * Decoupled animation state-machine controller.
30
+ *
31
+ * Register states (idle, run, …) and transitions between them,
32
+ * then call `evaluate()` every frame. The controller resolves the
33
+ * current state and drives `playAnimation` on the owner model —
34
+ * no index juggling, no hardcoded names scattered around the codebase.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const ctrl = new AnimationController(animations);
39
+ *
40
+ * ctrl.addState({ name: 'idle', clipName: 'Armature|idle' });
41
+ * ctrl.addState({ name: 'run', clipName: 'Armature|run' });
42
+ *
43
+ * ctrl.addTransition('idle', 'run', () => character.isMoving());
44
+ * ctrl.addTransition('run', 'idle', () => !character.isMoving());
45
+ *
46
+ * ctrl.start('idle');
47
+ *
48
+ * // in game loop:
49
+ * ctrl.evaluate();
50
+ * ```
51
+ */
52
+ declare class AnimationController {
53
+ private readonly animations;
54
+ private readonly playFn;
55
+ private states;
56
+ private transitions;
57
+ private currentStateName;
58
+ /**
59
+ * @param animations - The model's animation map (same reference).
60
+ * @param playFn - Callback that actually starts an animation clip
61
+ * on the model (e.g. `model.playAnimation`).
62
+ */
63
+ constructor(animations: Map<string, AnimationClip>, playFn: (clipName: string, loop: boolean) => void);
64
+ /** Register an animation state. */
65
+ addState(state: AnimationState): this;
66
+ /** Convenience: register multiple states at once. */
67
+ addStates(states: AnimationState[]): this;
68
+ /** Register a transition between two states. */
69
+ addTransition(from: string, to: string, condition: TransitionCondition): this;
70
+ /** Force-start a specific state (e.g. on init). */
71
+ start(stateName: string): void;
72
+ /**
73
+ * Evaluate all transitions from the current state.
74
+ * Call this once per frame. The first matching transition wins.
75
+ */
76
+ evaluate(): void;
77
+ /** The name of the currently active state (or `null`). */
78
+ get currentState(): string | null;
79
+ }
80
+
81
+ declare class Character extends Model {
82
+ id: string;
83
+ health: number;
84
+ /** Friction applied when grounded — updated from the surface material on landing.
85
+ * 0 = no resistance (pure ice), 1 = instant stop. */
86
+ surfaceFriction: number;
87
+ /** Horizontal damping while airborne. 0 = no air drag, 1 = instant stop. */
88
+ readonly airFriction = 0.02;
89
+ speed: number;
90
+ stamina: number;
91
+ velocity: Vector3;
92
+ private _isGrounded;
93
+ /** Setting to true (landing) resets the jump counter. */
94
+ get isGrounded(): boolean;
95
+ set isGrounded(v: boolean);
96
+ /** Pending auto-roll on landing after a double jump. */
97
+ private _landingRoll;
98
+ /** Minimum downward impact speed (negative Y velocity) required to trigger
99
+ * a landing roll. Tune this value — jumpForce is 0.65, so:
100
+ * -0.20 = any jump from ground, -0.40 = only falls from significant height */
101
+ minLandingImpact: number;
102
+ /** True during the airborne period after a second (mid-air) jump. */
103
+ isDoubleJumping: boolean;
104
+ /** True while movement keys are held — drives run animation independently of velocity. */
105
+ isInputActive: boolean;
106
+ private jumpForce;
107
+ private _targetVx;
108
+ private _targetVz;
109
+ readonly animController: AnimationController;
110
+ private _isRolling;
111
+ private _rollTimer;
112
+ private readonly _rollDuration;
113
+ private readonly _rollSpeed;
114
+ /** True when rolling was triggered by a hard landing (not a manual roll). */
115
+ isLandingRoll: boolean;
116
+ /** Direction captured at roll start (world-space XZ). */
117
+ private _rollDir;
118
+ private _isJumping;
119
+ private _jumpTimer;
120
+ private readonly _jumpDuration;
121
+ private _jumpsUsed;
122
+ readonly maxJumps = 2;
123
+ constructor(id: string, baseModel: Model);
124
+ /** Whether the character has meaningful horizontal velocity.
125
+ * Default threshold is 20% of configured speed — so the walk animation
126
+ * plays until the character has genuinely slowed down, not just slightly. */
127
+ isMoving(threshold?: number): boolean;
128
+ /** True while the roll animation is playing. */
129
+ get isRolling(): boolean;
130
+ /** True while the jump animation is playing. */
131
+ get isJumping(): boolean;
132
+ /** Start a jump — first jump requires grounded, second can fire mid-air. */
133
+ jump(): void;
134
+ /** Start a roll — only if grounded and not already rolling/jumping. */
135
+ roll(): void;
136
+ /** Internal roll starter — skips input guards (used for forced landing roll). */
137
+ private _beginRoll;
138
+ private getDirectionFromCamera;
139
+ moveRelative(forwardAmount: number, rightAmount: number, camera: Camera): void;
140
+ consumeStamina(): void;
141
+ update(deltaTime?: number): void;
142
+ stop(): void;
143
+ }
144
+
145
+ /**
146
+ * Thin adapter: forwards keyboard state into the Character.
147
+ * Does NOT run its own requestAnimationFrame — the game loop polls
148
+ * {@link applyInput} every fixed-update tick.
149
+ */
150
+ declare class KeyboardInput {
151
+ private control;
152
+ private cb?;
153
+ private keysPressed;
154
+ private rollRequested;
155
+ private jumpRequested;
156
+ constructor(control: KeyboardControl, cb?: ((pos: any) => void) | undefined);
157
+ /** Wire up key listeners. Call once after constructing. */
158
+ bind(): void;
159
+ /**
160
+ * Called once per fixed-update from the game loop.
161
+ * Reads currently pressed keys and drives the character.
162
+ */
163
+ applyInput(character: Character, camera: Camera): void;
164
+ }
165
+
166
+ export { AnimationController as A, Character as C, KeyboardInput as K, type TransitionCondition as T, type AnimationState as a, type AnimationTransition as b };
@@ -0,0 +1,74 @@
1
+ import { Vector3, Vector4 } from './Core/domain/interfaces/Vectors.js';
2
+ import { W as WebGLCore } from './WebGLCore-DR7ZHJB0.js';
3
+
4
+ interface TextureOptions {
5
+ /** When true, use gl.REPEAT wrap mode for tiling textures. */
6
+ repeat?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Supported light types.
11
+ * - `directional` – parallel rays (sun-like).
12
+ * - `point` – omnidirectional with distance attenuation.
13
+ * - `ambient` – constant contribution everywhere.
14
+ */
15
+ type LightType = 'directional' | 'point' | 'ambient';
16
+ /**
17
+ * A scene light with type, position, direction, colour, intensity,
18
+ * and attenuation factors (for point lights).
19
+ */
20
+ declare class Light {
21
+ type: LightType;
22
+ position: Vector3;
23
+ direction: Vector3;
24
+ color: Vector3;
25
+ intensity: number;
26
+ constant: number;
27
+ linear: number;
28
+ quadratic: number;
29
+ constructor(type?: LightType, position?: Vector3, // for point lights
30
+ direction?: Vector3, // for directional lights
31
+ color?: Vector3, intensity?: number, constant?: number, linear?: number, quadratic?: number);
32
+ move(dx: number, dy: number, dz: number): void;
33
+ rotateY(delta: number): void;
34
+ }
35
+
36
+ /** Maximum number of lights supported per draw call (must match the shader). */
37
+ declare const MAX_LIGHTS = 5;
38
+ /**
39
+ * Encapsulates GPU material state: shader program reference, uniform/attribute
40
+ * caches, albedo colour, textures, and lighting properties.
41
+ */
42
+ declare class Material {
43
+ private webglCore;
44
+ program: WebGLProgram;
45
+ uniformLocations: Record<string, WebGLUniformLocation | null>;
46
+ attribLocations: Record<string, number>;
47
+ albedoColor: Vector4;
48
+ unlit: boolean;
49
+ texture?: WebGLTexture;
50
+ specular: Vector3;
51
+ shininess: number;
52
+ doubleSided: boolean;
53
+ ambientColor: Vector3;
54
+ dissolve: number;
55
+ diffuse: Vector3;
56
+ /** Physics friction coefficient [0–1]. 0 = no resistance (ice), 1 = instant stop. */
57
+ friction: number;
58
+ constructor(webglCore: WebGLCore, options?: Partial<Material>);
59
+ setColorHex(hex: string): void;
60
+ setColor(rgba: [number, number, number, number]): void;
61
+ /**
62
+ * Load an image from URL and create a WebGL texture.
63
+ */
64
+ loadTexture(url: string, options?: TextureOptions): Promise<void>;
65
+ /**
66
+ * Create an independent copy of this material.
67
+ * Shares the same WebGL program but copies all mutable properties.
68
+ * The texture reference is shared (immutable GPU resource).
69
+ */
70
+ clone(): Material;
71
+ apply(gl: WebGLRenderingContext, lights: Light[], viewPosition?: Vector3): void;
72
+ }
73
+
74
+ export { Light as L, MAX_LIGHTS as M, type LightType as a, Material as b };