@fonsecabarreto/genesis-gl-core 0.1.0 → 0.1.1

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 (40) hide show
  1. package/README.md +19 -2
  2. package/dist/{Camera-DY_8gx3C.d.ts → Camera-CJVYy9fH.d.ts} +13 -2
  3. package/dist/Core/classes/Material.d.ts +1 -1
  4. package/dist/Core/classes/Material.js +1 -1
  5. package/dist/Core/classes/Model.d.ts +3 -3
  6. package/dist/Core/classes/Model.js +1 -1
  7. package/dist/Core/classes/Renderer.d.ts +11 -5
  8. package/dist/Core/classes/Renderer.js +4 -4
  9. package/dist/Core/classes/Scene.d.ts +2 -2
  10. package/dist/Core/classes/Viewport.d.ts +1 -1
  11. package/dist/Core/classes/Viewport.js +1 -1
  12. package/dist/Core/index.d.ts +4 -4
  13. package/dist/Core/index.js +4 -4
  14. package/dist/Core/utils/load-glb.d.ts +3 -3
  15. package/dist/Core/utils/load-glb.js +4 -4
  16. package/dist/Core/utils/parse-obj.d.ts +2 -2
  17. package/dist/Core/utils/parse-obj.js +4 -4
  18. package/dist/Editor/index.d.ts +126 -15
  19. package/dist/Editor/index.js +471 -74
  20. package/dist/Editor/index.js.map +1 -1
  21. package/dist/Game/controls/KeyboardInput.d.ts +4 -4
  22. package/dist/Game/index.d.ts +308 -7
  23. package/dist/Game/index.js +470 -24
  24. package/dist/Game/index.js.map +1 -1
  25. package/dist/{KeyboardInput-DTsfj3tE.d.ts → KeyboardInput-1xOAabI0.d.ts} +95 -14
  26. package/dist/{Material-BGLkldxv.d.ts → Material-DhwSRbP2.d.ts} +8 -0
  27. package/dist/{Model-CQvDXd-b.d.ts → Model-BBZHnUp1.d.ts} +24 -8
  28. package/dist/{chunk-6LS6AO5H.js → chunk-L66K4AZU.js} +36 -30
  29. package/dist/chunk-L66K4AZU.js.map +1 -0
  30. package/dist/{chunk-JK2HEZAT.js → chunk-QOAQVTAB.js} +26 -22
  31. package/dist/chunk-QOAQVTAB.js.map +1 -0
  32. package/dist/{chunk-5TAAXI6S.js → chunk-XMW2MS66.js} +39 -16
  33. package/dist/chunk-XMW2MS66.js.map +1 -0
  34. package/dist/{chunk-QCQVJCSR.js → chunk-ZCJ3MJZD.js} +103 -67
  35. package/dist/chunk-ZCJ3MJZD.js.map +1 -0
  36. package/package.json +1 -1
  37. package/dist/chunk-5TAAXI6S.js.map +0 -1
  38. package/dist/chunk-6LS6AO5H.js.map +0 -1
  39. package/dist/chunk-JK2HEZAT.js.map +0 -1
  40. package/dist/chunk-QCQVJCSR.js.map +0 -1
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../../src/Game/classes/AnimationController.ts","../../src/Game/classes/PhysicsBody.ts","../../src/Game/classes/Character.ts","../../src/Game/classes/BodyPieces/Arms.ts","../../src/Game/classes/BodyPieces/Head.ts","../../src/Game/classes/BodyPieces/Leg.ts","../../src/Game/classes/Actor.ts","../../src/Core/utils/create-sphere.ts","../../src/Game/classes/InteractiveActor.ts","../../src/Game/GameLoopRunner.ts","../../src/Game/GameApplication.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 { Model } from '../../Core/classes/Model';\nimport { Mesh } from '../../Core/classes/Mesh';\nimport { Skeleton } from '../../Core/classes/Skeleton';\nimport { Vector3 } from '../../Core/domain/interfaces/Vectors';\n\n/**\n * PhysicsBody — intermediate layer between {@link Model} and gameplay entities.\n *\n * Adds all physics state to a renderable model without coupling it to any\n * specific gameplay mechanics (input, animation states, character actions, etc.).\n *\n * ## Layer contract\n * ```\n * Model → transform, AABB, render, skeletal animation\n * └─ PhysicsBody → + velocity, gravity, ground detection, friction\n * └─ Character → + input, animation FSM, jump/roll, stamina\n * ```\n *\n * Any future object that should be affected by physics (falling crates,\n * projectiles, dynamic props) can extend `PhysicsBody` directly without\n * carrying any of `Character`'s gameplay logic.\n *\n * ## Gravity contract\n * `PhysicsBody` does **not** self-apply gravity. The owning physics step\n * (e.g. `Game.onUpdate`) is responsible for adding `gravity * dt` to\n * `velocity[1]` each tick — but only when `useGravity` is `true`.\n * This keeps the simulation loop explicit and easy to pause or override.\n */\nexport class PhysicsBody extends Model {\n // ── Linear dynamics ───────────────────────────────────────────────────────\n\n /** World-space velocity in units/second (XYZ). */\n public velocity: Vector3 = [0, 0, 0];\n\n /**\n * When `true` the owning physics step should add gravity to `velocity[1]`\n * every tick. Set to `false` for server-driven actors whose Y position is\n * authoritative from the network and must not be locally simulated.\n */\n public useGravity = true;\n\n // ── Surface interaction ───────────────────────────────────────────────────\n\n /**\n * Friction coefficient of the surface this body is currently standing on.\n * Updated on landing from the collided model's material.\n * Range: 0 (pure ice) → 1 (instant stop).\n */\n public surfaceFriction = 0.3;\n\n /**\n * Horizontal damping applied while airborne.\n * Kept low so air strafing feels natural and not floaty.\n */\n public readonly airFriction = 0.02;\n\n // ── Mass & restitution ────────────────────────────────────────────────────\n\n /**\n * Mass in kilograms. Used to distribute impulses proportionally when two\n * dynamic bodies collide: heavier objects are displaced and accelerated\n * less. Default: `1.0`. Set higher for heavy objects (e.g. boulders) or\n * lower for light ones (e.g. beach balls).\n */\n public mass = 1.0;\n\n /**\n * Coefficient of restitution — fraction of relative velocity preserved\n * along the collision normal. Range: 0 (perfectly inelastic) → 1 (fully\n * elastic). Default: `0` (rigid body, no bounce). `Ball` overrides this\n * to `0.6`. When two bodies collide the effective restitution is the\n * geometric mean of both values.\n */\n public restitution = 0.0;\n\n // ── Ground state ──────────────────────────────────────────────────────────\n\n protected _isGrounded = false;\n\n public get isGrounded(): boolean {\n return this._isGrounded;\n }\n\n /**\n * Setting to `true` when the previous state was `false` triggers\n * the `onLand()` hook — override in subclasses for gameplay responses\n * (e.g. landing roll, jump counter reset).\n */\n public set isGrounded(v: boolean) {\n const wasGrounded = this._isGrounded;\n this._isGrounded = v;\n if (v && !wasGrounded) {\n this.onLand();\n }\n }\n\n // ── Constructor ───────────────────────────────────────────────────────────\n\n constructor(\n meshes: Mesh[],\n translation: Vector3 = [0, 0, 0],\n scale: Vector3 = [1, 1, 1],\n name = 'physicsBody',\n skeleton: Skeleton | null = null,\n ) {\n super(meshes, translation, scale, name, skeleton);\n }\n\n // ── Virtual hooks ─────────────────────────────────────────────────────────\n\n /**\n * Called once when this body transitions from airborne to grounded.\n * Override to add gameplay responses (landing roll, sound, VFX, etc.).\n * Base implementation does nothing.\n */\n protected onLand(): void {}\n\n /**\n * Called by the collision system when a vertical (Y-axis) surface is hit.\n * Receives the velocity **at the moment of impact** so subclasses can\n * compute a bounce impulse before the caller updates `velocity[1]`.\n *\n * Default: zero out vertical velocity (rigid body, no bounce).\n * Override in `Ball` (or any bouncy actor) to apply a restitution impulse.\n *\n * @param impactVy `velocity[1]` captured just before the collision resolve.\n */\n public resolveYCollision(impactVy: number): void {\n void impactVy;\n this.velocity[1] = 0;\n }\n}\n","import { Vector3 } from '../../Core/domain/interfaces/Vectors';\nimport { Camera } from '../../Core';\nimport { Model } from '../../Core/classes/Model';\nimport { AnimationController } from './AnimationController';\nimport { PhysicsBody } from './PhysicsBody';\n\nexport class Character extends PhysicsBody {\n public id: string;\n health = 100;\n speed = 0.2;\n stamina = 10;\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 /**\n * Called by {@link PhysicsBody.isGrounded} setter on every landing.\n * Queues a landing roll if the impact was hard enough and resets jump state.\n */\n protected override onLand(): void {\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\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","import { Mesh } from '../../Core/classes/Mesh';\nimport { Skeleton } from '../../Core/classes/Skeleton';\nimport { Vector3 } from '../../Core/domain/interfaces/Vectors';\nimport { Model } from '../../Core/classes/Model';\n\n/**\n * Actor — a static, non-physics scene entity.\n *\n * Sits directly on top of {@link Model} and represents any object that is\n * placed in the world but **does not move** (environment props, decorations,\n * landmarks, triggers, etc.).\n *\n * Use {@link InteractiveActor} when the object should react to collisions\n * and participate in the physics simulation loop.\n *\n * ## Layer contract\n * ```\n * Model → transform, AABB, render, skeletal animation\n * └─ Actor → static scene entity (no physics)\n * └─ PhysicsBody → + velocity, gravity, ground detection, friction\n * └─ Character → + input, animation FSM, jump/roll, stamina\n * └─ InteractiveActor → + bounce, rolling friction, impulse exchange\n * ```\n *\n * ## Usage\n * ```ts\n * const prop = new Actor(myModel.meshes, [10, 0, 5], [1, 1, 1], 'lamp_post');\n * prop.collidable = false; // decorative — skip collision\n * scene.add('lamp_0', prop);\n * ```\n */\nexport class Actor extends Model {\n constructor(\n meshes: Mesh[],\n translation: Vector3 = [0, 0, 0],\n scale: Vector3 = [1, 1, 1],\n name = 'actor',\n skeleton: Skeleton | null = null,\n ) {\n super(meshes, translation, scale, name, skeleton);\n }\n}\n","import { Material, Mesh, WebGLCore } from '../classes';\nimport { GL_LINES } from '../classes/Mesh';\nimport { Vector3 } from '../domain/interfaces/Vectors';\n\n/**\n * Create a sphere mesh for lights or other objects\n * @param webglCore WebGL context wrapper\n * @param radius radius of the sphere\n * @param latBands latitude subdivisions\n * @param lonBands longitude subdivisions\n * @param color RGB color of the sphere\n */\nexport function createSphereMesh(\n webglCore: WebGLCore,\n radius = 1,\n latBands = 12,\n lonBands = 12,\n color: Vector3 = [1, 1, 1],\n): Mesh {\n const positions: number[] = [];\n const normals: number[] = [];\n\n for (let lat = 0; lat <= latBands; lat++) {\n const theta = (lat * Math.PI) / latBands;\n const sinTheta = Math.sin(theta);\n const cosTheta = Math.cos(theta);\n\n for (let lon = 0; lon <= lonBands; lon++) {\n const phi = (lon * 2 * Math.PI) / lonBands;\n const sinPhi = Math.sin(phi);\n const cosPhi = Math.cos(phi);\n\n const x = cosPhi * sinTheta;\n const y = cosTheta;\n const z = sinPhi * sinTheta;\n\n positions.push(radius * x, radius * y, radius * z);\n normals.push(x, y, z);\n }\n }\n\n const indices: number[] = [];\n for (let lat = 0; lat < latBands; lat++) {\n for (let lon = 0; lon < lonBands; lon++) {\n const first = lat * (lonBands + 1) + lon;\n const second = first + lonBands + 1;\n\n indices.push(first, second, first + 1);\n indices.push(second, second + 1, first + 1);\n }\n }\n\n const vertices: number[] = [];\n const finalNormals: number[] = [];\n\n // Flatten indices into actual vertex arrays\n for (const idx of indices) {\n vertices.push(\n positions[idx * 3],\n positions[idx * 3 + 1],\n positions[idx * 3 + 2],\n );\n finalNormals.push(\n normals[idx * 3],\n normals[idx * 3 + 1],\n normals[idx * 3 + 2],\n );\n }\n\n const material = new Material(webglCore, {\n albedoColor: [color[0], color[1], color[2], 1],\n unlit: true, // lights always glow\n });\n\n return new Mesh(\n 'sphere',\n new Float32Array(vertices),\n new Float32Array(finalNormals),\n material,\n );\n}\n\n/**\n * Creates a debug arrow/line mesh from `origin` along `direction`.\n * length: how long the arrow is\n * color: RGB array [r,g,b]\n */\nexport function createLightDirectionMesh(\n webglCore: WebGLCore,\n origin: Vector3,\n direction: Vector3,\n length: number = 2,\n color: [number, number, number] = [1, 0.5, 0],\n): Mesh {\n // Normalize direction\n const dx = direction[0];\n const dy = direction[1];\n const dz = direction[2];\n const len = Math.hypot(dx, dy, dz);\n const nx = dx / len;\n const ny = dy / len;\n const nz = dz / len;\n\n // Create two vertices: start at origin, end at origin + direction*length\n const vertices = new Float32Array([\n origin[0],\n origin[1],\n origin[2],\n origin[0] + nx * length,\n origin[1] + ny * length,\n origin[2] + nz * length,\n ]);\n\n // Normals not needed for unlit lines, just placeholder\n const normals = new Float32Array([0, 1, 0, 0, 1, 0]);\n\n // Create a simple unlit material\n const material = new Material(webglCore, {\n albedoColor: [...color, 1],\n unlit: true,\n });\n\n const mesh = new Mesh('light-direction', vertices, normals, material);\n mesh.isCollidable = false; // Debug line shouldn't collide\n mesh.setMode(GL_LINES);\n\n return mesh;\n}\n","import { WebGLCore } from '../../Core/classes/WebGLCore';\nimport { Material } from '../../Core/classes/Material';\nimport { Mesh } from '../../Core/classes/Mesh';\nimport { Vector3 } from '../../Core/domain/interfaces/Vectors';\nimport { createSphereMesh } from '../../Core/utils/create-sphere';\nimport { PhysicsBody } from './PhysicsBody';\n\n/**\n * InteractiveActor — a physics-simulated scene object.\n *\n * Extends {@link PhysicsBody} with:\n * - Bounce behaviour via {@link restitution}.\n * - Horizontal friction via the inherited {@link surfaceFriction}.\n * - An override of {@link resolveYCollision} that applies a bounce impulse.\n * - An override of {@link update} that integrates velocity and applies friction.\n *\n * The constructor accepts any set of {@link Mesh}es so any model can behave\n * as a physics-interactive object. Use the static factory {@link sphere} when\n * you specifically want a procedural sphere shape.\n *\n * ## Usage — custom mesh\n * ```ts\n * const actor = new InteractiveActor(myModel.meshes, [0, 5, 0], 'crate');\n * actor.restitution = 0.2;\n * game.addPhysicsObject('crate_1', actor);\n * ```\n *\n * ## Usage — sphere shorthand\n * ```ts\n * const ball = InteractiveActor.sphere(webglCore, mat, 0.5, [0, 5, 0]);\n * game.addPhysicsObject('ball_1', ball);\n * ```\n */\nexport class InteractiveActor extends PhysicsBody {\n /**\n * Minimum impact speed (|vy|) required to trigger a visible bounce.\n * Below this threshold the actor comes to rest instead of micro-bouncing.\n * Default: `0.05`.\n */\n public minBounceSpeed = 0.05;\n\n constructor(\n meshes: Mesh[],\n position: Vector3 = [0, 0, 0],\n name = 'interactiveActor',\n ) {\n super(meshes, position, [1, 1, 1], name);\n }\n\n // ── Static factories ──────────────────────────────────────────────────────\n\n /**\n * Create an {@link InteractiveActor} with a procedurally generated sphere\n * mesh — convenience shorthand for the common \"physics ball\" use case.\n *\n * @param webglCore WebGL context used to build the mesh buffers.\n * @param material Material applied to the sphere.\n * @param radius Sphere radius in world units. Default `0.5`.\n * @param position Initial world position. Default origin.\n * @param latBands Latitude subdivisions. Default `16`.\n * @param lonBands Longitude subdivisions. Default `16`.\n */\n static sphere(\n webglCore: WebGLCore,\n material: Material,\n radius = 0.5,\n position: Vector3 = [0, 0, 0],\n latBands = 16,\n lonBands = 16,\n ): InteractiveActor {\n const mesh: Mesh = createSphereMesh(\n webglCore,\n radius,\n latBands,\n lonBands,\n (material.albedoColor?.slice(0, 3) as Vector3) ?? [1, 1, 1],\n );\n mesh.material = material;\n\n const actor = new InteractiveActor([mesh], position, 'ball');\n actor.restitution = 0.6;\n actor.surfaceFriction = 0.05;\n return actor;\n }\n\n // ── PhysicsBody hooks ─────────────────────────────────────────────────────\n\n /**\n * Apply a bounce impulse instead of zeroing Y velocity.\n * The collision system passes the velocity captured just before impact so\n * the correct outgoing speed can be computed from {@link restitution}.\n */\n public override resolveYCollision(impactVy: number): void {\n if (Math.abs(impactVy) > this.minBounceSpeed) {\n this.velocity[1] = -impactVy * this.restitution;\n this._isGrounded = false;\n } else {\n this.velocity[1] = 0;\n }\n }\n\n // ── Update ────────────────────────────────────────────────────────────────\n\n /**\n * Advance one physics tick: integrate velocity, apply friction, zero\n * sub-threshold components so the actor eventually comes to rest.\n */\n public override update(deltaTime: number): void {\n super.update(deltaTime);\n\n if (this._isGrounded) {\n const friction = Math.min(0.9, this.surfaceFriction + 0.005);\n this.velocity[0] += (0 - this.velocity[0]) * friction;\n this.velocity[2] += (0 - this.velocity[2]) * friction;\n } else {\n this.velocity[0] *= 1 - this.airFriction;\n this.velocity[2] *= 1 - this.airFriction;\n }\n\n this.move(this.velocity[0], this.velocity[1], this.velocity[2]);\n\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","/**\n * GameLoopRunner\n *\n * Encapsulates a fixed-timestep game loop built on `requestAnimationFrame`.\n *\n * Responsibilities:\n * - Accumulate real elapsed time and fire a fixed-update callback at a\n * constant rate (e.g. 60 Hz physics / logic ticks).\n * - Fire a variable-rate render callback once per animation frame.\n * - Expose start / stop / dispose controls.\n *\n * Design note: this class has no knowledge of scene, renderer, or any game\n * object — it is a pure scheduling primitive consumed by `GameApplication`.\n */\nexport class GameLoopRunner {\n /** Number of milliseconds between each fixed-update tick. */\n private readonly stepMs: number;\n\n private rafHandle: number | null = null;\n private lastTime = 0;\n private accumulated = 0;\n\n private fixedUpdateFn: ((dt: number) => void) | null = null;\n private renderFn: (() => void) | null = null;\n\n /**\n * @param targetFPS Number of fixed-update ticks per second. Defaults to 60.\n */\n constructor(readonly targetFPS: number = 60) {\n this.stepMs = 1000 / targetFPS;\n }\n\n // ── Public API ────────────────────────────────────────────────────────────\n\n /**\n * Begin the loop.\n *\n * @param fixedUpdate Called at the fixed timestep rate with `dt` in seconds.\n * @param render Called once per animation frame (variable rate).\n */\n public start(fixedUpdate: (dt: number) => void, render: () => void): void {\n if (this.rafHandle !== null) {\n console.warn(\n '[GameLoopRunner] start() called while already running — ignoring.',\n );\n return;\n }\n\n this.fixedUpdateFn = fixedUpdate;\n this.renderFn = render;\n this.lastTime = performance.now();\n this.accumulated = 0;\n this.rafHandle = requestAnimationFrame(this.tick);\n }\n\n /** Pause the loop without discarding callbacks. Resume with `resume()`. */\n public pause(): void {\n if (this.rafHandle !== null) {\n cancelAnimationFrame(this.rafHandle);\n this.rafHandle = null;\n }\n }\n\n /** Resume a paused loop. */\n public resume(): void {\n if (this.rafHandle !== null) return;\n if (!this.fixedUpdateFn || !this.renderFn) {\n console.warn(\n '[GameLoopRunner] resume() called before start() — ignoring.',\n );\n return;\n }\n this.lastTime = performance.now();\n this.rafHandle = requestAnimationFrame(this.tick);\n }\n\n /** Stop the loop and release all callbacks. */\n public stop(): void {\n this.pause();\n this.fixedUpdateFn = null;\n this.renderFn = null;\n this.accumulated = 0;\n }\n\n public get isRunning(): boolean {\n return this.rafHandle !== null;\n }\n\n // ── Private ───────────────────────────────────────────────────────────────\n\n /** Arrow function to preserve `this` across rAF callbacks. */\n private readonly tick = (now: number): void => {\n const elapsed = now - this.lastTime;\n this.lastTime = now;\n\n // Guard against huge deltas (e.g. tab was backgrounded)\n this.accumulated += Math.min(elapsed, this.stepMs * 5);\n\n while (this.accumulated >= this.stepMs) {\n this.fixedUpdateFn?.(this.stepMs / 1000);\n this.accumulated -= this.stepMs;\n }\n\n this.renderFn?.();\n\n // Schedule next frame only if still running\n if (this.rafHandle !== null) {\n this.rafHandle = requestAnimationFrame(this.tick);\n }\n };\n}\n","import { WebGLCore } from '../Core/classes/WebGLCore';\nimport { Scene } from '../Core/classes/Scene';\nimport { Viewport } from '../Core/classes/Viewport';\nimport { Renderer } from '../Core/classes/Renderer';\nimport { GameLoopRunner } from './GameLoopRunner';\n\n// ── Configuration ────────────────────────────────────────────────────────────\n\n/**\n * Bootstrap configuration passed to `GameApplication`.\n *\n * All fields except `canvasId` are optional and fall back to sensible defaults.\n */\nexport interface GameApplicationConfig {\n /** ID of the `<canvas>` element that WebGL will render into. */\n canvasId: string;\n /** Logical render width in pixels. Default: `1280`. */\n viewportWidth?: number;\n /** Logical render height in pixels. Default: `720`. */\n viewportHeight?: number;\n /**\n * Number of fixed-update ticks per second.\n * Physics, input and game-logic run at this rate. Default: `60`.\n */\n targetFPS?: number;\n}\n\n// Internal resolved config — all fields required after applying defaults.\ntype ResolvedConfig = Required<GameApplicationConfig>;\n\nconst DEFAULT_CONFIG: Omit<ResolvedConfig, 'canvasId'> = {\n viewportWidth: 1280,\n viewportHeight: 720,\n targetFPS: 60,\n};\n\n// ── Abstract base ────────────────────────────────────────────────────────────\n\n/**\n * GameApplication — abstract base class for GenesisGL games.\n *\n * ## Design: Template Method pattern\n *\n * This class provides the **invariant algorithm skeleton** for a GenesisGL\n * application. Subclasses supply the variant behaviour by overriding the\n * hook methods listed below.\n *\n * ### Lifecycle\n *\n * ```\n * start()\n * ├── onLoadResources() ← abstract — load models, textures, data\n * ├── createScene() ← virtual — build and configure the Scene\n * ├── createViewport() ← virtual — build and configure the Viewport\n * ├── createRenderer() ← virtual — build and configure the Renderer\n * ├── onInit() ← abstract — place entities, wire input, etc.\n * └── loop (per frame)\n * ├── [N×] onUpdate(dt) ← abstract — physics / logic fixed tick\n * ├── onBeforeRender() ← virtual — pre-render hook (e.g. HUD)\n * ├── renderer.render() ← sealed\n * └── onAfterRender() ← virtual — post-render hook\n *\n * stop()\n * └── onDispose() ← virtual — release custom resources\n * ```\n *\n * ### Minimal subclass\n *\n * ```ts\n * class MyGame extends GameApplication {\n * protected async onLoadResources() { ... }\n * protected async onInit() { ... }\n * protected onUpdate(dt: number) { ... }\n * }\n *\n * const game = new MyGame({ canvasId: 'glcanvas' });\n * await game.start();\n * ```\n */\nexport abstract class GameApplication {\n // ── Core engine objects — available after construction ───────────────────\n\n protected readonly webglCore: WebGLCore;\n protected readonly config: ResolvedConfig;\n\n /**\n * The active scene. Populated during construction via `createScene()`\n * so subclasses may reference it as early as `onLoadResources()`.\n */\n protected readonly scene: Scene;\n\n /**\n * Set during `start()`, after `onLoadResources()` returns.\n * Use in `onInit()` and lifecycle hooks.\n */\n public viewport: Viewport | null = null;\n\n /**\n * Set during `start()`, after `viewport` is initialised.\n * `null` before `start()` resolves — safe to check from external code.\n */\n public renderer: Renderer | null = null;\n\n // ── Internal ──────────────────────────────────────────────────────────────\n\n private readonly loopRunner: GameLoopRunner;\n private _started = false;\n\n // ── Constructor ───────────────────────────────────────────────────────────\n\n constructor(config: GameApplicationConfig) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.webglCore = new WebGLCore(this.config.canvasId);\n this.scene = this.createScene();\n this.loopRunner = new GameLoopRunner(this.config.targetFPS);\n }\n\n // ── Public API ────────────────────────────────────────────────────────────\n\n /**\n * Execute the full startup sequence and begin the game loop.\n *\n * The sequence is **sealed** — extend behaviour through the protected hooks,\n * not by overriding `start()`.\n */\n public async start(): Promise<void> {\n if (this._started) {\n console.warn(\n '[GameApplication] start() called more than once — ignoring.',\n );\n return;\n }\n this._started = true;\n\n // 1. Let the subclass load its assets / data before any WebGL work.\n await this.onLoadResources();\n\n // 2. Create the engine objects that depend on resolved resources.\n this.viewport = this.createViewport();\n this.renderer = this.createRenderer();\n\n // 3. Let the subclass place entities, wire input, configure stores, etc.\n await this.onInit();\n\n // 4. Hand off to the loop runner.\n this.loopRunner.start(\n (dt: number) => this.onUpdate(dt),\n () => {\n this.onBeforeRender();\n this.renderer!.render(this.scene);\n this.onAfterRender();\n },\n );\n }\n\n /**\n * Pause the game loop without discarding state.\n * Call `resume()` to continue.\n */\n public pause(): void {\n this.loopRunner.pause();\n }\n\n /** Resume a paused game loop. */\n public resume(): void {\n this.loopRunner.resume();\n }\n\n /**\n * Stop the game loop permanently and invoke `onDispose()`.\n * After calling `stop()` the instance should be discarded.\n */\n public stop(): void {\n this.loopRunner.stop();\n this.onDispose();\n this._started = false;\n }\n\n public get isRunning(): boolean {\n return this.loopRunner.isRunning;\n }\n\n // ── Abstract hooks (must be implemented by subclasses) ───────────────────\n\n /**\n * Load all external assets required by the game (models, textures, data).\n *\n * Called **before** `viewport`, `renderer`, or any scene entities are\n * created, so it is safe to fire async fetch/XHR calls here.\n */\n protected abstract onLoadResources(): Promise<void>;\n\n /**\n * Place entities into the scene, configure input handlers, set up stores, etc.\n *\n * Called once, after `viewport` and `renderer` have been initialised and\n * `onLoadResources()` has resolved.\n */\n protected abstract onInit(): Promise<void> | void;\n\n /**\n * Game-logic / physics update.\n *\n * Called at the fixed timestep rate defined by `config.targetFPS`.\n *\n * @param dt Fixed delta-time in **seconds** (e.g. `1/60 ≈ 0.0167`).\n */\n protected abstract onUpdate(dt: number): void;\n\n // ── Virtual hooks (optional override) ────────────────────────────────────\n\n /**\n * Factory for the `Scene`. Override to customise lights or initial state.\n * Default: `Scene.withDefaultLights(webglCore)`.\n */\n protected createScene(): Scene {\n return Scene.withDefaultLights(this.webglCore);\n }\n\n /**\n * Factory for the `Viewport`. Override to customise camera settings or\n * attach extra resize behaviour.\n * Default: constructed from `config.canvasId`, `viewportWidth`, `viewportHeight`.\n */\n protected createViewport(): Viewport {\n return new Viewport(\n this.config.canvasId,\n this.config.viewportWidth,\n this.config.viewportHeight,\n this.webglCore,\n );\n }\n\n /**\n * Factory for the `Renderer`. Override to enable debug mode or swap\n * the renderer implementation.\n * Default: `new Renderer(webglCore, viewport)`.\n */\n protected createRenderer(): Renderer {\n return new Renderer(this.webglCore, this.viewport!);\n }\n\n /**\n * Called every animation frame **before** `renderer.render(scene)`.\n *\n * Suitable for: updating HUD overlays, syncing camera state, or any\n * visual concern that must run at display rate rather than the fixed tick.\n */\n protected onBeforeRender(): void {}\n\n /**\n * Called every animation frame **after** `renderer.render(scene)`.\n *\n * Suitable for: post-process passes, 2D canvas overlays, analytics.\n */\n protected onAfterRender(): void {}\n\n /**\n * Called by `stop()` to release any custom resources held by the subclass\n * (event listeners, WebSocket connections, audio contexts, etc.).\n *\n * The base `Scene` and engine objects are **not** auto-disposed here;\n * call `scene.dispose()` explicitly if needed.\n */\n protected onDispose(): void {}\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
- import { C as Camera } from './Camera-DY_8gx3C.js';
1
+ import { C as Camera } from './Camera-CJVYy9fH.js';
2
2
  import { K as KeyboardControl } from './KeyboardControl-5w7Vm0J0.js';
3
+ import { a as AnimationClip, i as Model, g as Mesh, S as Skeleton } from './Model-BBZHnUp1.js';
3
4
  import { Vector3 } from './Core/domain/interfaces/Vectors.js';
4
- import { a as AnimationClip, i as Model } from './Model-CQvDXd-b.js';
5
5
 
6
6
  /** A named state that maps to one animation clip. */
7
7
  interface AnimationState {
@@ -78,21 +78,97 @@ declare class AnimationController {
78
78
  get currentState(): string | null;
79
79
  }
80
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. */
81
+ /**
82
+ * PhysicsBody — intermediate layer between {@link Model} and gameplay entities.
83
+ *
84
+ * Adds all physics state to a renderable model without coupling it to any
85
+ * specific gameplay mechanics (input, animation states, character actions, etc.).
86
+ *
87
+ * ## Layer contract
88
+ * ```
89
+ * Model → transform, AABB, render, skeletal animation
90
+ * └─ PhysicsBody → + velocity, gravity, ground detection, friction
91
+ * └─ Character → + input, animation FSM, jump/roll, stamina
92
+ * ```
93
+ *
94
+ * Any future object that should be affected by physics (falling crates,
95
+ * projectiles, dynamic props) can extend `PhysicsBody` directly without
96
+ * carrying any of `Character`'s gameplay logic.
97
+ *
98
+ * ## Gravity contract
99
+ * `PhysicsBody` does **not** self-apply gravity. The owning physics step
100
+ * (e.g. `Game.onUpdate`) is responsible for adding `gravity * dt` to
101
+ * `velocity[1]` each tick — but only when `useGravity` is `true`.
102
+ * This keeps the simulation loop explicit and easy to pause or override.
103
+ */
104
+ declare class PhysicsBody extends Model {
105
+ /** World-space velocity in units/second (XYZ). */
106
+ velocity: Vector3;
107
+ /**
108
+ * When `true` the owning physics step should add gravity to `velocity[1]`
109
+ * every tick. Set to `false` for server-driven actors whose Y position is
110
+ * authoritative from the network and must not be locally simulated.
111
+ */
112
+ useGravity: boolean;
113
+ /**
114
+ * Friction coefficient of the surface this body is currently standing on.
115
+ * Updated on landing from the collided model's material.
116
+ * Range: 0 (pure ice) → 1 (instant stop).
117
+ */
86
118
  surfaceFriction: number;
87
- /** Horizontal damping while airborne. 0 = no air drag, 1 = instant stop. */
119
+ /**
120
+ * Horizontal damping applied while airborne.
121
+ * Kept low so air strafing feels natural and not floaty.
122
+ */
88
123
  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. */
124
+ /**
125
+ * Mass in kilograms. Used to distribute impulses proportionally when two
126
+ * dynamic bodies collide: heavier objects are displaced and accelerated
127
+ * less. Default: `1.0`. Set higher for heavy objects (e.g. boulders) or
128
+ * lower for light ones (e.g. beach balls).
129
+ */
130
+ mass: number;
131
+ /**
132
+ * Coefficient of restitution — fraction of relative velocity preserved
133
+ * along the collision normal. Range: 0 (perfectly inelastic) → 1 (fully
134
+ * elastic). Default: `0` (rigid body, no bounce). `Ball` overrides this
135
+ * to `0.6`. When two bodies collide the effective restitution is the
136
+ * geometric mean of both values.
137
+ */
138
+ restitution: number;
139
+ protected _isGrounded: boolean;
94
140
  get isGrounded(): boolean;
141
+ /**
142
+ * Setting to `true` when the previous state was `false` triggers
143
+ * the `onLand()` hook — override in subclasses for gameplay responses
144
+ * (e.g. landing roll, jump counter reset).
145
+ */
95
146
  set isGrounded(v: boolean);
147
+ constructor(meshes: Mesh[], translation?: Vector3, scale?: Vector3, name?: string, skeleton?: Skeleton | null);
148
+ /**
149
+ * Called once when this body transitions from airborne to grounded.
150
+ * Override to add gameplay responses (landing roll, sound, VFX, etc.).
151
+ * Base implementation does nothing.
152
+ */
153
+ protected onLand(): void;
154
+ /**
155
+ * Called by the collision system when a vertical (Y-axis) surface is hit.
156
+ * Receives the velocity **at the moment of impact** so subclasses can
157
+ * compute a bounce impulse before the caller updates `velocity[1]`.
158
+ *
159
+ * Default: zero out vertical velocity (rigid body, no bounce).
160
+ * Override in `Ball` (or any bouncy actor) to apply a restitution impulse.
161
+ *
162
+ * @param impactVy `velocity[1]` captured just before the collision resolve.
163
+ */
164
+ resolveYCollision(impactVy: number): void;
165
+ }
166
+
167
+ declare class Character extends PhysicsBody {
168
+ id: string;
169
+ health: number;
170
+ speed: number;
171
+ stamina: number;
96
172
  /** Pending auto-roll on landing after a double jump. */
97
173
  private _landingRoll;
98
174
  /** Minimum downward impact speed (negative Y velocity) required to trigger
@@ -120,6 +196,11 @@ declare class Character extends Model {
120
196
  private readonly _jumpDuration;
121
197
  private _jumpsUsed;
122
198
  readonly maxJumps = 2;
199
+ /**
200
+ * Called by {@link PhysicsBody.isGrounded} setter on every landing.
201
+ * Queues a landing roll if the impact was hard enough and resets jump state.
202
+ */
203
+ protected onLand(): void;
123
204
  constructor(id: string, baseModel: Model);
124
205
  /** Whether the character has meaningful horizontal velocity.
125
206
  * Default threshold is 20% of configured speed — so the walk animation
@@ -163,4 +244,4 @@ declare class KeyboardInput {
163
244
  applyInput(character: Character, camera: Camera): void;
164
245
  }
165
246
 
166
- export { AnimationController as A, Character as C, KeyboardInput as K, type TransitionCondition as T, type AnimationState as a, type AnimationTransition as b };
247
+ export { AnimationController as A, Character as C, KeyboardInput as K, PhysicsBody as P, type TransitionCondition as T, type AnimationState as a, type AnimationTransition as b };
@@ -55,6 +55,14 @@ declare class Material {
55
55
  diffuse: Vector3;
56
56
  /** Physics friction coefficient [0–1]. 0 = no resistance (ice), 1 = instant stop. */
57
57
  friction: number;
58
+ private readonly _lightDirs;
59
+ private readonly _lightColors;
60
+ private readonly _lightIntensities;
61
+ private readonly _lightTypes;
62
+ private readonly _lightPositions;
63
+ private readonly _lightConstants;
64
+ private readonly _lightLinears;
65
+ private readonly _lightQuadratics;
58
66
  constructor(webglCore: WebGLCore, options?: Partial<Material>);
59
67
  setColorHex(hex: string): void;
60
68
  setColor(rgba: [number, number, number, number]): void;
@@ -1,7 +1,7 @@
1
+ import { vec3, quat, mat4 } from 'gl-matrix';
1
2
  import { Vector3 } from './Core/domain/interfaces/Vectors.js';
2
3
  import { W as WebGLCore } from './WebGLCore-DR7ZHJB0.js';
3
- import { b as Material } from './Material-BGLkldxv.js';
4
- import { vec3, quat, mat4 } from 'gl-matrix';
4
+ import { b as Material } from './Material-DhwSRbP2.js';
5
5
 
6
6
  interface AABB {
7
7
  min: Vector3;
@@ -123,6 +123,10 @@ declare class Skeleton {
123
123
  * `global * inverseBindMatrix` evaluates to identity in the bind pose.
124
124
  */
125
125
  readonly rootTransform: mat4;
126
+ /** Per-joint global transform matrices, reused each frame (allocated once in constructor). */
127
+ private readonly _globals;
128
+ private readonly _local;
129
+ private readonly _final;
126
130
  constructor(joints: Joint[], rootTransform?: mat4);
127
131
  /** Number of joints in the skeleton. */
128
132
  get jointCount(): number;
@@ -199,6 +203,11 @@ declare class AnimationClip {
199
203
  speed: number;
200
204
  /** Whether the clip is currently advancing. */
201
205
  playing: boolean;
206
+ /**
207
+ * Per-joint pose objects reused across frames to avoid per-frame allocations.
208
+ * The vec3/quat inside each entry are also pre-allocated on first use.
209
+ */
210
+ private readonly _posePool;
202
211
  constructor(name: string, channels: AnimationChannel[], duration: number);
203
212
  /** Start / resume playback. */
204
213
  play(): void;
@@ -214,10 +223,12 @@ declare class AnimationClip {
214
223
  */
215
224
  update(deltaTime: number): void;
216
225
  /**
217
- * Sample every channel at the current playhead and return a map of
218
- * joint-index local pose.
226
+ * Sample every channel at the current playhead and write per-joint local
227
+ * transforms into `out` (or a newly created map if none is provided).
228
+ *
229
+ * Pass a persistent `Map` to avoid allocating a new one every frame.
219
230
  */
220
- sample(): Map<number, JointPose>;
231
+ sample(out?: Map<number, JointPose>): Map<number, JointPose>;
221
232
  /**
222
233
  * Create an independent copy of this clip (shares the underlying sampler
223
234
  * data but has its own playback state).
@@ -237,20 +248,25 @@ declare class Model {
237
248
  translation: Vector3;
238
249
  rotation: Vector3;
239
250
  scale: Vector3;
240
- boundingBox: AABB;
241
251
  /** The skeleton driving skinned meshes (set after GLB load if present). */
242
252
  skeleton: Skeleton | null;
243
253
  /** Named animation clips available on this model. */
244
254
  animations: Map<string, AnimationClip>;
245
255
  /** The currently playing animation clip (if any). */
246
256
  activeAnimation: AnimationClip | null;
257
+ private _boundingBox;
258
+ private _bboxDirty;
247
259
  private _boundsDefinedManually;
248
260
  private _modelMatrix;
249
261
  private _matrixDirty;
262
+ private readonly _poseMap;
263
+ /** World-space AABB, lazily recomputed after any transform change. */
264
+ get boundingBox(): AABB;
265
+ set boundingBox(value: AABB);
250
266
  /** When `false` the model is excluded from collision detection. */
251
267
  collidable: boolean;
252
268
  constructor(meshes: Mesh[], translation?: Vector3, scale?: Vector3, _name?: string, skeleton?: Skeleton | null);
253
- /** Set absolute position. Marks matrix dirty and updates bounding box. */
269
+ /** Set absolute position. Marks matrix and bounding box dirty. */
254
270
  setTranslation(x: number, y: number, z: number): void;
255
271
  /** Set absolute rotation (radians per axis). */
256
272
  setRotation(x: number, y: number, z: number): void;
@@ -258,7 +274,7 @@ declare class Model {
258
274
  setScale(x: number, y: number, z: number): void;
259
275
  /** Translate the model by a delta. */
260
276
  move(dx: number, dy: number, dz?: number): void;
261
- private getModelMatrix;
277
+ getModelMatrix(): mat4;
262
278
  /**
263
279
  * Manually define the collision bounding box relative to the model's
264
280
  * current translation.
@@ -157,6 +157,15 @@ var Material = class _Material {
157
157
  // Kd
158
158
  /** Physics friction coefficient [0–1]. 0 = no resistance (ice), 1 = instant stop. */
159
159
  friction = 0.3;
160
+ // ── Pre-allocated light uniform buffers (reused every frame, zero GC pressure) ──
161
+ _lightDirs = new Float32Array(MAX_LIGHTS * 3);
162
+ _lightColors = new Float32Array(MAX_LIGHTS * 3);
163
+ _lightIntensities = new Float32Array(MAX_LIGHTS);
164
+ _lightTypes = new Int32Array(MAX_LIGHTS);
165
+ _lightPositions = new Float32Array(MAX_LIGHTS * 3);
166
+ _lightConstants = new Float32Array(MAX_LIGHTS);
167
+ _lightLinears = new Float32Array(MAX_LIGHTS);
168
+ _lightQuadratics = new Float32Array(MAX_LIGHTS);
160
169
  setColorHex(hex) {
161
170
  this.albedoColor = parseHexToRgbArray(hex);
162
171
  }
@@ -218,72 +227,69 @@ var Material = class _Material {
218
227
  if (this.uniformLocations["uTexture"])
219
228
  gl.uniform1i(this.uniformLocations["uTexture"], 0);
220
229
  }
221
- const MAX = MAX_LIGHTS;
222
230
  if (!this.unlit && lights?.length) {
223
- const count = Math.min(lights.length, MAX);
231
+ const count = Math.min(lights.length, MAX_LIGHTS);
224
232
  if (this.uniformLocations["uLightCount"])
225
233
  gl.uniform1i(this.uniformLocations["uLightCount"], count);
226
- const dirs = [];
227
- const colors = [];
228
- const intensities = [];
229
- const types = [];
230
- const positions = [];
231
- const constants = [];
232
- const linears = [];
233
- const quadratics = [];
234
234
  for (let i = 0; i < count; i++) {
235
235
  const light = lights[i];
236
+ const i3 = i * 3;
236
237
  let typeInt = 0;
237
238
  if (light.type === "point") typeInt = 1;
238
239
  else if (light.type === "ambient") typeInt = 2;
239
- types.push(typeInt);
240
- dirs.push(...light.direction ?? [0, 0, 0]);
241
- positions.push(...light.position ?? [0, 0, 0]);
242
- colors.push(...light.color);
243
- intensities.push(light.intensity);
244
- constants.push(light.constant);
245
- linears.push(light.linear);
246
- quadratics.push(light.quadratic);
240
+ this._lightTypes[i] = typeInt;
241
+ const dir = light.direction ?? [0, 0, 0];
242
+ this._lightDirs[i3] = dir[0];
243
+ this._lightDirs[i3 + 1] = dir[1];
244
+ this._lightDirs[i3 + 2] = dir[2];
245
+ const pos = light.position ?? [0, 0, 0];
246
+ this._lightPositions[i3] = pos[0];
247
+ this._lightPositions[i3 + 1] = pos[1];
248
+ this._lightPositions[i3 + 2] = pos[2];
249
+ this._lightColors[i3] = light.color[0];
250
+ this._lightColors[i3 + 1] = light.color[1];
251
+ this._lightColors[i3 + 2] = light.color[2];
252
+ this._lightIntensities[i] = light.intensity;
253
+ this._lightConstants[i] = light.constant;
254
+ this._lightLinears[i] = light.linear;
255
+ this._lightQuadratics[i] = light.quadratic;
247
256
  }
248
257
  if (this.uniformLocations["uLightDirection[0]"])
249
258
  gl.uniform3fv(
250
259
  this.uniformLocations["uLightDirection[0]"],
251
- new Float32Array(dirs)
260
+ this._lightDirs
252
261
  );
253
262
  if (this.uniformLocations["uLightColor[0]"])
254
263
  gl.uniform3fv(
255
264
  this.uniformLocations["uLightColor[0]"],
256
- new Float32Array(colors)
265
+ this._lightColors
257
266
  );
258
267
  if (this.uniformLocations["uLightIntensity[0]"])
259
268
  gl.uniform1fv(
260
269
  this.uniformLocations["uLightIntensity[0]"],
261
- new Float32Array(intensities)
270
+ this._lightIntensities
262
271
  );
263
272
  if (this.uniformLocations["uLightType[0]"])
264
- gl.uniform1iv(
265
- this.uniformLocations["uLightType[0]"],
266
- new Int32Array(types)
267
- );
273
+ gl.uniform1iv(this.uniformLocations["uLightType[0]"], this._lightTypes);
268
274
  if (this.uniformLocations["uLightPosition[0]"])
269
275
  gl.uniform3fv(
270
276
  this.uniformLocations["uLightPosition[0]"],
271
- new Float32Array(positions)
277
+ this._lightPositions
272
278
  );
273
279
  if (this.uniformLocations["uLightConstant[0]"])
274
280
  gl.uniform1fv(
275
281
  this.uniformLocations["uLightConstant[0]"],
276
- new Float32Array(constants)
282
+ this._lightConstants
277
283
  );
278
284
  if (this.uniformLocations["uLightLinear[0]"])
279
285
  gl.uniform1fv(
280
286
  this.uniformLocations["uLightLinear[0]"],
281
- new Float32Array(linears)
287
+ this._lightLinears
282
288
  );
283
289
  if (this.uniformLocations["uLightQuadratic[0]"])
284
290
  gl.uniform1fv(
285
291
  this.uniformLocations["uLightQuadratic[0]"],
286
- new Float32Array(quadratics)
292
+ this._lightQuadratics
287
293
  );
288
294
  }
289
295
  }
@@ -293,4 +299,4 @@ export {
293
299
  MAX_LIGHTS,
294
300
  Material
295
301
  };
296
- //# sourceMappingURL=chunk-6LS6AO5H.js.map
302
+ //# sourceMappingURL=chunk-L66K4AZU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Core/utils/load-texture.ts","../src/Core/utils/parse-hex-to-rgb.ts","../src/Core/classes/Material.ts"],"sourcesContent":["export interface TextureOptions {\n /** When true, use gl.REPEAT wrap mode for tiling textures. */\n repeat?: boolean;\n}\n\nexport async function loadWebGlTexture(\n gl: WebGLRenderingContext,\n url: string,\n options: TextureOptions = {},\n): Promise<WebGLTexture> {\n return new Promise((resolve, reject) => {\n const texture = gl.createTexture();\n if (!texture) return reject(new Error('Failed to create texture'));\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n\n // Placeholder pixel (gray) until image loads\n const level = 0;\n const internalFormat = gl.RGBA;\n const width = 1;\n const height = 1;\n const border = 0;\n const srcFormat = gl.RGBA;\n const srcType = gl.UNSIGNED_BYTE;\n const pixel = new Uint8Array([128, 128, 128, 255]); // gray\n gl.texImage2D(\n gl.TEXTURE_2D,\n level,\n internalFormat,\n width,\n height,\n border,\n srcFormat,\n srcType,\n pixel,\n );\n\n const image = new Image();\n image.crossOrigin = 'anonymous';\n image.onload = () => {\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(\n gl.TEXTURE_2D,\n level,\n internalFormat,\n srcFormat,\n srcType,\n image,\n );\n\n // Auto mipmaps and filtering\n if (isPowerOf2(image.width) && isPowerOf2(image.height)) {\n gl.generateMipmap(gl.TEXTURE_2D);\n gl.texParameteri(\n gl.TEXTURE_2D,\n gl.TEXTURE_MIN_FILTER,\n gl.LINEAR_MIPMAP_LINEAR,\n );\n // Wrap mode: REPEAT for tiling, otherwise default (REPEAT is GL default for POT)\n if (options.repeat) {\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);\n }\n } else {\n // Non-power-of-2: REPEAT is not supported in WebGL 1\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n }\n\n resolve(texture);\n };\n image.onerror = reject;\n image.src = url;\n });\n}\n\nfunction isPowerOf2(value: number) {\n return (value & (value - 1)) === 0;\n}\n","import { Vector4 } from '../domain/interfaces/Vectors';\n\nexport function parseHexToRgbArray(hex: string): Vector4 {\n // Remove leading #\n if (hex.startsWith('#')) hex = hex.slice(1);\n\n let r = 1,\n g = 1,\n b = 1,\n a = 1;\n\n if (hex.length === 6) {\n r = parseInt(hex.slice(0, 2), 16) / 255;\n g = parseInt(hex.slice(2, 4), 16) / 255;\n b = parseInt(hex.slice(4, 6), 16) / 255;\n a = 1;\n } else if (hex.length === 8) {\n r = parseInt(hex.slice(0, 2), 16) / 255;\n g = parseInt(hex.slice(2, 4), 16) / 255;\n b = parseInt(hex.slice(4, 6), 16) / 255;\n a = parseInt(hex.slice(6, 8), 16) / 255;\n } else {\n throw new Error('Invalid hex color format. Use #RRGGBB or #RRGGBBAA.');\n }\n\n return [r, g, b, a];\n}\n","import { Vector3, Vector4 } from '../domain/interfaces/Vectors';\nimport { loadWebGlTexture, TextureOptions } from '../utils/load-texture';\nimport { parseHexToRgbArray } from '../utils/parse-hex-to-rgb';\nimport { Light } from './Light';\nimport WebGLCore from './WebGLCore';\n\n/** Maximum number of lights supported per draw call (must match the shader). */\nexport const MAX_LIGHTS = 5;\n\n/**\n * Encapsulates GPU material state: shader program reference, uniform/attribute\n * caches, albedo colour, textures, and lighting properties.\n */\nexport class Material {\n // cache\n public program: WebGLProgram;\n public uniformLocations: Record<string, WebGLUniformLocation | null> = {};\n public attribLocations: Record<string, number> = {};\n\n // Attributes\n public albedoColor: Vector4 = [1, 1, 1, 1];\n public unlit: boolean = false;\n public texture?: WebGLTexture;\n public specular: Vector3 = [0.3, 0.3, 0.3];\n public shininess: number = 64;\n public doubleSided: boolean = false;\n //others\n public ambientColor: Vector3 = [0.1, 0.1, 0.1]; // Ka\n public dissolve: number = 1.0; // d or Tr\n public diffuse: Vector3 = [1, 1, 1]; // Kd\n /** Physics friction coefficient [0–1]. 0 = no resistance (ice), 1 = instant stop. */\n public friction: number = 0.3;\n\n // ── Pre-allocated light uniform buffers (reused every frame, zero GC pressure) ──\n private readonly _lightDirs = new Float32Array(MAX_LIGHTS * 3);\n private readonly _lightColors = new Float32Array(MAX_LIGHTS * 3);\n private readonly _lightIntensities = new Float32Array(MAX_LIGHTS);\n private readonly _lightTypes = new Int32Array(MAX_LIGHTS);\n private readonly _lightPositions = new Float32Array(MAX_LIGHTS * 3);\n private readonly _lightConstants = new Float32Array(MAX_LIGHTS);\n private readonly _lightLinears = new Float32Array(MAX_LIGHTS);\n private readonly _lightQuadratics = new Float32Array(MAX_LIGHTS);\n\n constructor(\n private webglCore: WebGLCore,\n options: Partial<Material> = {},\n ) {\n this.program = webglCore.getProgram();\n const { gl } = webglCore;\n\n // Cache uniforms\n const names = [\n 'uColor',\n 'uUnlit',\n 'uModel',\n 'uView',\n 'uProjection',\n 'uLightCount',\n 'uUseTexture',\n 'uTexture',\n // --- LIGHTING UNIFORMS ---\n 'uViewPosition', // Camera position for specular light\n 'uShininess',\n 'uSpecularColor',\n 'uAmbientColor',\n 'uDissolve',\n 'uDiffuseColor',\n 'uLightDirection[0]',\n 'uLightColor[0]',\n 'uLightIntensity[0]',\n 'uLightType[0]',\n 'uLightPosition[0]', // NEW: For Point Lights\n 'uLightConstant[0]', // NEW: Attenuation\n 'uLightLinear[0]', // NEW: Attenuation\n 'uLightQuadratic[0]', // NEW: Attenuation\n // --- SKINNING UNIFORMS ---\n 'uUseSkinning',\n 'uJointMatrices[0]',\n ];\n for (const name of names) {\n this.uniformLocations[name] = gl.getUniformLocation(this.program, name);\n }\n\n // Cache attributes\n const attribs = [\n 'aPosition',\n 'aNormal',\n 'aTexCoord',\n 'aJointIndices',\n 'aJointWeights',\n ];\n for (const name of attribs) {\n this.attribLocations[name] = gl.getAttribLocation(this.program, name);\n }\n\n Object.assign(this, options);\n }\n\n setColorHex(hex: string) {\n this.albedoColor = parseHexToRgbArray(hex);\n }\n\n setColor(rgba: [number, number, number, number]) {\n this.albedoColor = rgba;\n this.dissolve = rgba[3];\n this.diffuse = [rgba[0], rgba[1], rgba[2]];\n }\n\n /**\n * Load an image from URL and create a WebGL texture.\n */\n async loadTexture(url: string, options?: TextureOptions): Promise<void> {\n const { gl } = this.webglCore;\n this.texture = await loadWebGlTexture(gl, url, options);\n }\n\n /**\n * Create an independent copy of this material.\n * Shares the same WebGL program but copies all mutable properties.\n * The texture reference is shared (immutable GPU resource).\n */\n clone(): Material {\n const copy = new Material(this.webglCore);\n copy.albedoColor = [...this.albedoColor];\n copy.specular = [...this.specular];\n copy.ambientColor = [...this.ambientColor];\n copy.diffuse = [...this.diffuse];\n copy.shininess = this.shininess;\n copy.dissolve = this.dissolve;\n copy.unlit = this.unlit;\n copy.doubleSided = this.doubleSided;\n copy.friction = this.friction;\n copy.texture = this.texture; // shared GPU resource\n return copy;\n }\n\n apply(gl: WebGLRenderingContext, lights: Light[], viewPosition?: Vector3) {\n if (this.uniformLocations['uColor'])\n gl.uniform4fv(this.uniformLocations['uColor'], this.albedoColor);\n\n if (this.uniformLocations['uUnlit'])\n gl.uniform1i(this.uniformLocations['uUnlit'], this.unlit ? 1 : 0);\n\n // --- NEW MATERIAL UNIFORMS ---\n if (this.uniformLocations['uShininess'])\n gl.uniform1f(this.uniformLocations['uShininess'], this.shininess);\n\n if (this.uniformLocations['uSpecularColor'])\n gl.uniform3fv(this.uniformLocations['uSpecularColor'], this.specular);\n\n if (this.uniformLocations['uDissolve'])\n gl.uniform1f(this.uniformLocations['uDissolve'], this.dissolve);\n\n if (this.uniformLocations['uAmbientColor'])\n gl.uniform3fv(this.uniformLocations['uAmbientColor'], this.ambientColor);\n\n if (this.uniformLocations['uDiffuseColor'])\n gl.uniform3fv(this.uniformLocations['uDiffuseColor'], this.diffuse);\n\n // --- NEW CAMERA UNIFORM FOR SPECULAR HIGHLIGHTS ---\n if (viewPosition && this.uniformLocations['uViewPosition']) {\n gl.uniform3fv(this.uniformLocations['uViewPosition'], viewPosition);\n }\n\n // Texture\n const hasTexture = !!this.texture;\n if (this.uniformLocations['uUseTexture'])\n gl.uniform1i(this.uniformLocations['uUseTexture'], hasTexture ? 1 : 0);\n\n if (hasTexture && this.texture) {\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.texture);\n if (this.uniformLocations['uTexture'])\n gl.uniform1i(this.uniformLocations['uTexture'], 0);\n }\n\n // Lights — fill pre-allocated typed arrays in-place (zero per-frame allocations)\n if (!this.unlit && lights?.length) {\n const count = Math.min(lights.length, MAX_LIGHTS);\n\n if (this.uniformLocations['uLightCount'])\n gl.uniform1i(this.uniformLocations['uLightCount'], count);\n\n for (let i = 0; i < count; i++) {\n const light = lights[i];\n const i3 = i * 3;\n\n let typeInt = 0;\n if (light.type === 'point') typeInt = 1;\n else if (light.type === 'ambient') typeInt = 2;\n this._lightTypes[i] = typeInt;\n\n const dir = light.direction ?? [0, 0, 0];\n this._lightDirs[i3] = dir[0];\n this._lightDirs[i3 + 1] = dir[1];\n this._lightDirs[i3 + 2] = dir[2];\n\n const pos = light.position ?? [0, 0, 0];\n this._lightPositions[i3] = pos[0];\n this._lightPositions[i3 + 1] = pos[1];\n this._lightPositions[i3 + 2] = pos[2];\n\n this._lightColors[i3] = light.color[0];\n this._lightColors[i3 + 1] = light.color[1];\n this._lightColors[i3 + 2] = light.color[2];\n\n this._lightIntensities[i] = light.intensity;\n this._lightConstants[i] = light.constant;\n this._lightLinears[i] = light.linear;\n this._lightQuadratics[i] = light.quadratic;\n }\n\n if (this.uniformLocations['uLightDirection[0]'])\n gl.uniform3fv(\n this.uniformLocations['uLightDirection[0]'],\n this._lightDirs,\n );\n if (this.uniformLocations['uLightColor[0]'])\n gl.uniform3fv(\n this.uniformLocations['uLightColor[0]'],\n this._lightColors,\n );\n if (this.uniformLocations['uLightIntensity[0]'])\n gl.uniform1fv(\n this.uniformLocations['uLightIntensity[0]'],\n this._lightIntensities,\n );\n if (this.uniformLocations['uLightType[0]'])\n gl.uniform1iv(this.uniformLocations['uLightType[0]'], this._lightTypes);\n if (this.uniformLocations['uLightPosition[0]'])\n gl.uniform3fv(\n this.uniformLocations['uLightPosition[0]'],\n this._lightPositions,\n );\n if (this.uniformLocations['uLightConstant[0]'])\n gl.uniform1fv(\n this.uniformLocations['uLightConstant[0]'],\n this._lightConstants,\n );\n if (this.uniformLocations['uLightLinear[0]'])\n gl.uniform1fv(\n this.uniformLocations['uLightLinear[0]'],\n this._lightLinears,\n );\n if (this.uniformLocations['uLightQuadratic[0]'])\n gl.uniform1fv(\n this.uniformLocations['uLightQuadratic[0]'],\n this._lightQuadratics,\n );\n }\n }\n}\n"],"mappings":"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","names":[]}