diff --git a/.cursor/main.mdc b/.cursor/main.mdc new file mode 100644 index 0000000..904a026 --- /dev/null +++ b/.cursor/main.mdc @@ -0,0 +1,200 @@ +--- +alwaysApply: true +--- +# Règles Cursor - Moteur de Jeu 2D WebGL TypeScript + +## Comportement de l'IA - Rôle et Expertise + +Je suis un expert en développement de moteurs de jeu 2D, WebGL, TypeScript et architecture logicielle. Je connais les meilleures pratiques actuelles et les dernières avancées technologiques dans ces domaines. + +### Mon Expertise +- **WebGL/GPU Programming** : Expert en optimisation GPU, batch rendering, shaders GLSL, gestion des buffers et minimisation des draw calls +- **Architecture ECS** : Maîtrise des patterns Entity Component System, composition over inheritance, systèmes découplés +- **Performance** : Expert en profiling, optimisation mémoire, object pooling, spatial partitioning, dirty flags +- **TypeScript** : Maîtrise approfondie avec strict mode, types avancés, génériques, interfaces +- **Game Engine Architecture** : Connaissance des architectures modulaires, systèmes de rendu, physique, collisions + +### Comment je travaille +1. **Proactivité** : Je suggère les meilleures solutions avant que des problèmes n'apparaissent +2. **Performance First** : Je priorise toujours la performance et suggère des optimisations pertinentes +3. **Code Quality** : Je maintiens un code propre, documenté, typé strictement et respectant les SOLID principles +4. **Connaissances à jour** : J'utilise les dernières pratiques WebGL, TypeScript et patterns de game engine +5. **Architecture solide** : Je propose des solutions évolutives et maintenables +6. **Documentation** : J'ajoute des commentaires explicatifs pour les décisions architecturales complexes + +### Mes Priorités +- ✅ Performance et optimisation (SpriteBatch, pooling, culling) +- ✅ TypeScript strict avec types explicites +- ✅ Architecture ECS respectée +- ✅ Gestion mémoire efficace (éviter GC spikes) +- ✅ Code testable et modulaire +- ✅ Documentation des choix techniques importants + +### Quand je code +- Je vérifie toujours la performance avant/après optimisations +- J'utilise systématiquement `deltaTime` pour tous les mouvements +- Je privilégie la composition à l'héritage +- Je documente les algorithmes complexes et les décisions d'architecture +- Je suggère des améliorations même si le code fonctionne + +## Architecture Globale + +Ce projet est un moteur de jeu 2D utilisant WebGL et TypeScript. L'architecture suit le pattern ECS (Entity Component System) avec composition plutôt qu'héritage. + +### Structure des Modules +- `core/` : Engine, GameLoop, SceneManager, Time, Scene +- `rendering/` : WebGLRenderer, SpriteBatch (★ crucial), Shader, Material, Texture, Camera, RenderQueue +- `webgl/` : GLContext, Buffer, VertexArray, Framebuffer +- `entities/` : GameObject, Transform, Component (classe de base) +- `components/` : SpriteRenderer, Animator, RigidBody, Colliders, ParticleEmitter, TilemapRenderer +- `systems/` : PhysicsSystem, CollisionSystem, AnimationSystem +- `input/` : InputManager +- `audio/` : AudioManager +- `assets/` : AssetLoader +- `math/` : Vector2, Matrix3, Rect, Color +- `utils/` : ObjectPool, EventBus, Quadtree, Debug + +## Principes de Performance (CRITIQUES) + +### 1. SpriteBatch - OBLIGATOIRE +- Accumule plusieurs sprites dans un buffer avant de les dessiner +- Flush seulement quand : batch plein, texture change, shader change +- Objectif : minimiser les draw calls (10000 sprites = 1 draw call) +- Structure vertex : [x, y, u, v, r, g, b, a] + +### 2. Object Pooling +- Utiliser ObjectPool pour tout ce qui spawn/despawn fréquemment +- Projectiles, particules, ennemis +- Réutiliser plutôt que créer/détruire (évite GC) + +### 3. Spatial Partitioning +- Utiliser Quadtree pour collisions si >500 objets +- Réduit de O(n²) à O(n log n) + +### 4. Dirty Flags +- Marquer les objets "sales" (dirty) quand changent +- Recalculer seulement si dirty (ex: Transform.worldMatrix) +- Économise 80-90% des calculs pour objets statiques + +### 5. Frustum Culling +- Ne rendre que les objets visibles par la caméra +- Skip les objets hors écran + +## Patterns de Code + +### ECS (Entity Component System) +```typescript +// Composition, PAS héritage +const enemy = new GameObject(); +enemy.addComponent(new SpriteRenderer()); +enemy.addComponent(new RigidBody()); +enemy.addComponent(new BoxCollider()); +``` + +### Cycle de Vie Component +- `awake()` : Appelé à l'ajout +- `start()` : Avant première update +- `update(deltaTime)` : Chaque frame +- `onDestroy()` : À la destruction + +### DeltaTime OBLIGATOIRE +- TOUS les mouvements utilisent `deltaTime` +- `position.x += speed * Time.deltaTime` (pas `position.x += 5`) +- Permet indépendance des FPS + +### Fixed Timestep pour Physique +- Physique en timestep fixe (ex: 1/60s) +- Logique de jeu en timestep variable + +## Règles TypeScript + +- Mode strict activé +- Interfaces pour tout (IRenderable, IUpdatable, ICollidable) +- Types explicites, éviter `any` +- Classes avec responsabilité unique + +## WebGL Best Practices + +1. **Minimiser les State Changes** : Shader, texture, blend mode coûteux +2. **Tri de RenderQueue** : Par layer → shader → texture +3. **Texture Atlas** : Combiner plusieurs textures en une +4. **Gérer les erreurs** : `gl.getError()` après opérations critiques +5. **Bind/Unbind** : Toujours unbind après usage + +## Structure des Classes + +### Transform +- Propriétés locales : `localPosition`, `localRotation`, `localScale` +- Propriétés monde : `position`, `rotation`, `scale` (getters/setters) +- Hiérarchie parent-enfant supportée +- Matrice worldMatrix avec cache et dirty flag + +### GameObject +- `transform: Transform` (toujours présent) +- `components: Map` +- Hiérarchie parent-enfant +- Cycle de vie : awake → start → update → destroy +- `tag` pour catégorisation + +### Component +- Classe abstraite +- `gameObject: GameObject` référence +- `transform` shortcut vers `gameObject.transform` +- `enabled: boolean` + +## Conventions de Nommage + +- Classes : PascalCase (`SpriteRenderer`, `WebGLRenderer`) +- Méthodes : camelCase (`update`, `render`, `getWorldMatrix`) +- Propriétés privées : `_private` avec underscore +- Interfaces : Préfixe `I` (`IRenderable`, `IUpdatable`) +- Constantes : UPPER_SNAKE_CASE + +## Mathématiques + +- `Vector2` : (x, y) avec méthodes add, subtract, multiply, normalize, length, distance, lerp +- `Matrix3` : Transformations 2D (translation, rotation, scale) +- `Rect` : Rectangle avec contains, overlaps, intersection +- `Color` : RGBA (0-1) + +## Event Bus + +Utiliser EventBus pour communication découplée entre systèmes : +```typescript +eventBus.on("player:died", (data) => { ... }); +eventBus.emit("player:died", { score: 1500 }); +``` + +## Debug et Performance + +- Dessiner les colliders (debug mode) +- Afficher FPS, draw calls, objets actifs +- Profiling avec `performance.mark()` et `performance.measure()` +- Ne jamais deviner les bottlenecks, mesurer d'abord + +## Anti-Patterns à ÉVITER + +❌ `position.x += 5` (dépend des FPS) +✅ `position.x += 300 * Time.deltaTime` (indépendant des FPS) + +❌ Créer/détruire objets chaque frame +✅ Utiliser ObjectPool + +❌ Tester toutes les collisions O(n²) +✅ Utiliser Quadtree + +❌ Recalculer matrices chaque frame +✅ Utiliser dirty flags + +❌ Héritage profond (Enemy extends Character extends Entity) +✅ Composition avec Components + +## Notes Importantes + +- Le SpriteBatch est le cœur de la performance +- TOUJOURS utiliser deltaTime pour les mouvements +- Fixed timestep pour physique (déterministe) +- TypeScript strict mode +- Documenter les décisions importantes dans le code +- Tester chaque système indépendamment avant intégration + diff --git a/.gitignore b/.gitignore index 485dee6..b7a1cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,30 @@ -.idea +# Dépendances +node_modules/ + +# Build +dist/ +*.js.map + +# Vite +.vite/ +dist-ssr/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Cache +.cache/ +*.cache + diff --git a/DEV_LOG.md b/DEV_LOG.md new file mode 100644 index 0000000..e316269 --- /dev/null +++ b/DEV_LOG.md @@ -0,0 +1,166 @@ +# Historique de Développement + +## 2025-11-02 - Setup Initial + +- Configuration du projet : package.json, tsconfig.json (strict mode), .gitignore +- Structure de dossiers complète selon CORE.MD (core, rendering, webgl, entities, components, systems, input, audio, assets, math, utils, physics, animation, shaders) +- Index.html de base avec canvas WebGL +- Point d'entrée src/index.ts créé +- Compilation TypeScript fonctionnelle + +## 2025-11-02 - Premier Rendu WebGL + +- GLContext : Abstraction WebGL basique avec initialisation, clear, resize, gestion d'erreurs +- Shader : Classe de compilation et gestion des shaders GLSL (vertex/fragment) +- Test de rendu : Triangle rose coloré affiché à l'écran avec boucle requestAnimationFrame +- Fonctionnalité : Rendu WebGL basique opérationnel + +## 2025-11-02 - Configuration Dev Server + +- Vite : Serveur de développement avec hot-reload ajouté +- Configuration : vite.config.ts avec port 3000 et ouverture automatique +- Scripts : Migration vers pnpm, script `dev` pour développement +- Index.html : Adaptation pour Vite (chargement direct des .ts) + +## 2025-11-02 - Classes Math et Buffer + +- Vector2 : Classe complète avec add, subtract, multiply, normalize, distance, lerp, rotate, etc. (mutables et immutables) +- Color : Classe RGBA avec lerp, multiply, conversion hex/RGBA, constantes prédéfinies +- Buffer : Encapsulation WebGL buffer avec bind, uploadData, uploadSubData, gestion STATIC/DYNAMIC/STREAM +- Amélioration rendu : 3 triangles colorés (rouge, vert, bleu pulsant) avec utilisation des nouvelles classes + +## 2025-11-02 - Core : Time et GameLoop + Math complètes + +- Time : Classe statique pour gestion deltaTime, timeScale, fps, time total. Essentiel pour mouvements indépendants des FPS +- GameLoop : Boucle de jeu avec requestAnimationFrame, fixed timestep pour physique, callbacks update/fixedUpdate/render +- Rect : Rectangle avec contains, overlaps, intersection, union, expand, shrink, propriétés calculées (left, right, top, bottom, center) +- Matrix3 : Matrice 3×3 pour transformations 2D (translate, rotate, scale, multiply, inverse, orthographic projection) +- Intégration : Test de rendu utilise maintenant GameLoop et Time, affichage FPS en temps réel + +## 2025-11-02 - Texture et Camera + +- Texture : Chargement d'images WebGL (loadFromURL asynchrone), gestion filtrage (NEAREST/LINEAR), wrapping (CLAMP/REPEAT), bind/unbind, dispose +- Camera : Projection orthographique, view matrix avec position/zoom/rotation, conversions écran↔monde, bounds (limites), visible bounds pour culling, dirty flags pour optimisation + +## 2025-11-02 - Architecture complète : WebGLRenderer, SpriteBatch, Scene, SceneManager, Engine + +- WebGLRenderer : Orchestrateur du rendu, gestion shaders/textures, rendu de scènes avec caméra, clear canvas +- SpriteBatch : Batch rendering crucial (accumule sprites en 1 draw call), gestion vertices/indices, flush automatique, transformation avec matrices +- Scene : Scène de jeu avec GameObjects (structure prête), caméra intégrée, méthodes onLoad/onUnload, update/render +- SceneManager : Gestion multiple scènes (Map), chargement/déchargement, transitions, update/render scène active +- Engine : Orchestrateur principal, initialise tous les systèmes, démarre GameLoop, API publique (start/stop/pause/resume), resize automatique + +## 2025-11-02 - Intégration Engine dans index.ts + +- Refactorisation : index.ts utilise maintenant Engine au lieu d'instancier directement GLContext/GameLoop +- Workflow : Engine → initialize() → create Scene → registerScene() → loadScene() → start() +- Test de rendu : 3 triangles colorés (rouge, vert, bleu pulsant) intégrés via Engine/Scene/WebGLRenderer +- Architecture propre : Code organisé avec Engine comme point d'entrée unique + +## 2025-11-02 - Phase 4 : Entity Component System - GameObject, Transform, Component + +- Component : Classe abstraite de base pour tous les composants, philosophie ECS (composition > héritage), cycle de vie (awake, start, update, onDestroy), référence au GameObject parent, état enabled/disabled +- Transform : Position/rotation/scale locales et monde, hiérarchie parent-enfant (setParent), matrice worldMatrix avec dirty flags pour optimisation (cache), méthodes translate/rotate/lookAt, conversions local↔monde, propriétés calculées (right, up, forward) +- GameObject : Entité de base du jeu, contient un Transform (automatique) et des Components (Map), hiérarchie parent-enfant (setParent/addChild/removeChild), cycle de vie complet (awake/start/update/destroy), propriétés (name, active, tag, layer), recherche (findChild, getComponentInChildren), destruction différée pour sécurité +- Scene : Intégration GameObject complète, update automatique de tous les GameObjects actifs, méthodes de recherche (findGameObjectByName, findGameObjectsByTag), nettoyage automatique des objets détruits +- Compilation : Tous les fichiers compilent sans erreur, TypeScript strict mode respecté + +## 2025-11-02 - Phase 5 : Input Manager et Composants de Rendu + +- InputManager : Gestion centralisée des entrées (clavier, souris), états différenciés (Down/Held/Up/Released), normalisation des touches, gestion molette, conversion coordonnées écran↔monde via caméra, intégré dans Engine (update chaque frame) +- SpriteRenderer : Composant pour affichage de sprites 2D, propriétés (texture, sourceRect pour spritesheets, tint, flipX/Y, layer, pivot), rendu via SpriteBatch, méthode setSprite() pour configuration rapide +- Animation : Structure de données pour animations sprite (frames, frameDuration, loop, events), calcul durée totale, accès sécurisé aux frames +- Animator : Composant de gestion d'animations, recherche automatique du SpriteRenderer, play/stop/pause/resume, support boucle/one-shot, vitesse de lecture ajustable, déclenchement d'événements à certaines frames, update automatique du sourceRect +- Engine : Intégration InputManager complète (propriété `input` publique), initialisation et update automatique dans GameLoop, dispose() pour nettoyage +- Compilation : Tous les nouveaux fichiers compilent sans erreur + +## 2025-11-02 - Phase 6 : Système de Rendu Complet + +- WebGLRenderer : Intégration SpriteBatch complète, rendu automatique de toutes les scènes via SpriteBatch, application des matrices projection/view via shader uniforms +- Scene.getAllRenderables() : Collecte récursive de tous les SpriteRenderer (GameObjects + enfants), tri automatique par layer (z-index), filtre objets actifs/enabled +- Scene._collectRenderablesRecursive() : Parcours hiérarchique complet pour trouver tous les SpriteRenderer dans la scène +- SpriteBatch : Configuration complète des attributs WebGL (aPosition, aTexCoord, aColor), positions monde passées au shader (transformation view/projection dans le vertex shader), support shader optionnel dans begin() +- Pipeline de rendu complet : WebGLRenderer.render() → Scene.getAllRenderables() → SpriteRenderer.render() → SpriteBatch.draw() → flush() → drawElements() +- Compilation : Système de rendu complet et fonctionnel, prêt pour afficher des sprites avec GameObjects + +## 2025-11-02 - Phase 7 : Exemple de Jeu Complet dans index.ts + +- Exemple complet : Implémentation d'un jeu de test dans index.ts avec tous les systèmes +- PlayerController : Composant custom pour contrôler le joueur avec les flèches/WASD, changement d'animation automatique (idle/walk) +- Textures générées : Fonction createSolidColorTexture() pour créer des textures de couleur solide sans dépendances externes +- GameObjects multiples : Joueur contrôlable, 5 ennemis statiques, 3 pièces animées, 100 tiles de sol (grille 10x10) +- Animations : Joueur avec animations idle/walk, pièces avec animation spin rapide +- Rendu complet : Tous les SpriteRenderer sont rendus automatiquement via le système de rendu de la scène +- InputManager : Testé avec déplacement du joueur (flèches ou WASD), animations qui changent selon le mouvement +- Systèmes testés : GameObject, Transform, Component, SpriteRenderer, Animator, InputManager, Scene, Engine, WebGLRenderer, SpriteBatch +- Compilation : Code compile sans erreur, exemple prêt à être exécuté et testé dans le navigateur + +## 2025-11-02 - Phase 8 : Physique et Collisions + +- Collider2D : Classe de base abstraite pour tous les colliders, support triggers, événements OnTriggerEnter/Exit et OnCollisionEnter/Exit avec callbacks +- BoxCollider2D : Collider rectangulaire (AABB - Axis-Aligned Bounding Box), détection de collisions Box-Box et Box-Circle +- CircleCollider2D : Collider circulaire, détection de collisions Circle-Circle, délègue Box-Circle à BoxCollider2D +- Rigidbody2D : Composant de physique avec vélocité linéaire/angulaire, masse, drag (friction), gravité, forces et impulsions, intégration du mouvement via fixed timestep +- PhysicsSystem : Système de détection et résolution de collisions, fixed timestep (60 FPS) pour stabilité physique, collecte récursive des colliders, résolution de collisions avec séparation proportionnelle à la masse, gestion des triggers vs collisions physiques, nettoyage automatique des événements Exit +- Intégration Engine : PhysicsSystem intégré dans Engine, appelé dans GameLoop.onFixedUpdate(), accessible via engine.physics +- Compilation : Tous les fichiers compilent sans erreur, système de physique complet et prêt à l'utilisation + +## 2025-11-02 - Phase 9 : Asset Management + +- AssetLoader : Système centralisé de chargement d'assets (textures, JSON), cache automatique, chargement parallèle avec promesses, gestion des erreurs, préchargement avec progression (callback onProgress), méthodes dispose() pour libérer la mémoire +- SpriteSheet : Gestion de spritesheets avec grille uniforme, création automatique de sprites depuis la grille, méthodes getSprite/getSpriteByIndex/getSpriteByGridPosition, support création manuelle de sprites personnalisés, propriétés (spriteWidth, spriteHeight, columns, rows) +- TextureAtlas : Gestion d'atlas de textures avec JSON (format compatible TexturePacker), parse automatique des frames depuis JSON, recherche de sprites par nom, méthode statique fromAssetLoader() pour création facile, support des métadonnées d'atlas +- Intégration Engine : AssetLoader intégré dans Engine, accessible via engine.assets, dispose() appelé dans Engine.dispose() +- Fichier d'export : index.ts dans src/assets pour faciliter l'importation de tous les composants Asset Management +- Compilation : Tous les fichiers compilent sans erreur, système d'Asset Management complet et prêt à l'utilisation + +## 2025-11-02 - Phase 10 : UI System + +- UICanvas : Système d'interface utilisateur séparé de la scène de jeu, caméra dédiée pour l'UI, gestion de GameObjects UI, rendu en overlay par-dessus la scène, intégré dans Scene avec méthode createUICanvas() +- UIComponent : Classe de base abstraite pour tous les composants UI, extends Component, gestion RectTransform automatique, cycle de vie initialize()/update()/render(), méthode render() pour dessiner via SpriteBatch +- RectTransform : Système de layout complet avec anchors (anchorMin/anchorMax), pivot, anchoredPosition, size, canvasSize, anchors presets (TopLeft, TopCenter, TopRight, MiddleLeft, MiddleCenter, MiddleRight, BottomLeft, BottomCenter, BottomRight, StretchHorizontal, StretchVertical, StretchAll), hiérarchie parent-enfant, conversion automatique coordonnées UI ↔ écran WebGL +- UIText : Affichage de texte utilisant Canvas 2D pour le rendu, propriétés (text, fontSize, fontFamily, color, align), mise à jour automatique de la texture, rendu via SpriteBatch avec gestion pivot/anchor, support ancrage et positionnement flexible +- UIButton : Bouton interactif avec états (normal, hover, pressed), couleurs personnalisables par état, détection hover/press via InputManager et conversion coordonnées, callback onClick pour actions personnalisées, rendu automatique avec transition couleur selon état +- UIImage : Affichage d'images dans l'UI, utilisation de Texture WebGL, gestion sourceRect pour sprites, support tint color, rendu via SpriteBatch avec gestion pivot/anchor +- Intégration Scene : UICanvas intégré dans Scene, update() et render() automatiques, nettoyage dans onUnload(), rendu en overlay via WebGLRenderer.render() après le rendu de la scène +- Exemple index.ts : Création d'un score texte (TopLeft), bouton interactif (BottomCenter) avec texte, démonstration du système UI complet avec ancrage et interactions +- Compilation : Tous les fichiers compilent sans erreur, système UI complet et fonctionnel, prêt pour création d'interfaces utilisateur dans les jeux + +## 2025-11-03 - Phase 11 : Intégration Spritesheets Personnalisés + +- Structure Assets : Migration des sprites de src/Sprites/ vers public/Sprites/ pour compatibilité Vite +- Organisation Sprites : 4 dossiers d'animations (IDLE, RUN, ATTACK 1, ATTACK 2), 4 directions par animation (down, left, right, up), 8 frames horizontales par spritesheet +- Auto-détection Dimensions : Système automatique de calcul de la taille des frames (largeur_texture / 8 frames), log des dimensions détectées dans la console pour debug +- Configuration Joueur : Transform.rotation = 90° pour corriger l'orientation des sprites, Transform.scale = 2x pour meilleure visibilité, collider auto-ajusté aux dimensions détectées +- Chargement Assets : AssetLoader charge toutes les textures (16 spritesheets au total), gestion fallback vers textures de test en cas d'erreur, logs détaillés du chargement +- SpriteSheet Helper : Fonction createAnimationFromSpriteSheet() pour créer automatiquement les animations depuis les textures chargées, calcul automatique du nombre de colonnes/rows +- Animations Configurées : 16 animations créées (4 états × 4 directions), frameDuration ajusté (0.15s pour idle, 0.1s pour run/attack), loop activé pour idle/run, désactivé pour attack +- PlayerController : Gestion automatique du changement de texture selon la direction et l'état, mapping gauche/droite inversé pour compenser la rotation, support 8 directions de mouvement, animations synchronisées avec les déplacements +- Debug Console : Affichage des dimensions de texture chargée, taille calculée vs configurée, warnings si dimensions incorrectes, vérification du nombre de frames détectées +- Optimisations : Sprites redimensionnés à 2x pour visibilité, rotation corrigée, colliders adaptés dynamiquement, système prêt pour spritesheets de tailles variables + +## 2025-11-03 - Canvas Plein Écran et Redimensionnement Automatique + +- HTML Fullscreen : Canvas prend maintenant 100% de la largeur et hauteur de la fenêtre, overflow hidden pour éviter les scrollbars, info FPS en overlay (position absolute) +- Initialisation Dynamique : Canvas créé avec window.innerWidth et window.innerHeight au lieu de dimensions fixes (800x600) +- Joueur Centré : Position initiale du joueur calculée dynamiquement (width/2, height/2) pour être toujours au centre de l'écran +- Redimensionnement Automatique : Event listener sur window.resize pour adapter automatiquement le canvas, renderer et caméra quand la fenêtre change de taille +- Responsive : Le jeu s'adapte maintenant à toutes les résolutions d'écran (desktop, laptop, fullscreen) +- CSS Optimisé : Body et HTML en 100% width/height, canvas en display block pour éviter les espaces blancs, info FPS avec fond semi-transparent en overlay + +## 2025-11-04 - Migration TopDownCharacter : 8 Directions et Sprites 16x16 + +- **Nouveaux Sprites TopDownCharacter** : Migration complète des anciens sprites vers TopDownCharacter (16x16 pixels, 4 frames par animation au lieu de 8) +- **8 Directions de Mouvement** : Implémentation complète du système 8 directions (haut, bas, gauche, droite + 4 diagonales : haut-gauche, haut-droite, bas-gauche, bas-droite) +- **Gestion Inputs Combinés** : Détection Z+Q, Z+D, S+Q, S+D pour mouvement diagonal fluide, normalisation des vecteurs pour vitesse constante +- **PlayerController Refactorisé** : Nouvelle méthode `getDirection8Way()` pour calculer la direction en 8 points cardinaux selon les inputs combinés +- **Chargement Assets** : 20 spritesheets chargés (8 walk + 8 roll + 4 slash), chemins mis à jour vers `/TopDownCharacter/Character/` +- **Animations 4 Frames** : Adaptation du `createAnimationFromSpriteSheet()` pour gérer 4 frames au lieu de 8, frameDuration ajusté (0.12s walk, 0.08s roll/slash) +- **Collider Ajusté** : BoxCollider2D du joueur adapté à 16x16 pixels (taille réelle du sprite) au lieu de l'ancienne taille calculée +- **Rotation Supprimée** : Transform.rotation = 0° pour TopDownCharacter (sprites correctement orientés dès l'origine) +- **Scale Optimisé** : Transform.scale = 3× pour sprites 16x16 (affichage 48x48) pour meilleure visibilité à l'écran +- **20 Animations Créées** : 8 walk (toutes directions), 8 roll (toutes directions), 4 slash (diagonales uniquement) +- **Mapping Directions** : up/down/left/right + upleft/upright/downleft/downright, priorité diagonales si deux touches simultanées +- **Messages Console Améliorés** : Logs détaillés du chargement, dimensions auto-détectées, instructions contrôles 8 directions +- **Compilation** : Aucune erreur TypeScript strict mode, tous les systèmes fonctionnels avec nouveaux sprites + diff --git a/README.md b/README.md index 84d78be..0bb4670 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,101 @@ -# tiny-shooter-game +# CastleStorm Engine -CastleStorm un jeu d'action ayant pour but de survivre le plus longtemps possible. +Moteur de jeu 2D avec WebGL en TypeScript. -Au cours de votre aventure, -vous rencontrerez des ennemis de plus en plus puissants. -Tuer les ennemis vous permettra de gagner de l'expérience, -et de l'argent pour acheter des améliorations dans une boutique tout le long de votre partie. +## 🎯 Objectifs -Attention, vous n'avez qu'une seule vie, Mort = Game Over ! (。◕‿◕。) +Moteur de jeu 2D performant utilisant : +- **WebGL** pour le rendu GPU +- **TypeScript** avec strict mode +- **ECS (Entity Component System)** pour l'architecture +- **Optimisations** : SpriteBatch, Object Pooling, Spatial Partitioning -Pour vous déplacer, utilisez les touches ZQSD, -et pour attaquer, utilisez le clic gauche de votre souris. +## 📋 Prérequis + +- Node.js 18+ et npm +- Un navigateur moderne supportant WebGL + +## 🚀 Installation + +```bash +# Installer les dépendances +pnpm install + +# Compiler le projet +pnpm run build + +# Mode dev avec hot-reload (recommandé) +pnpm run dev +``` + +Le serveur de dev s'ouvre automatiquement sur `http://localhost:3000`. + +## 📁 Structure du Projet + +``` +src/ +├── core/ # Engine, GameLoop, SceneManager, Time +├── rendering/ # WebGLRenderer, SpriteBatch, Shader, Camera +├── webgl/ # Abstraction WebGL (GLContext, Buffer, etc.) +├── entities/ # GameObject, Transform, Component +├── components/ # SpriteRenderer, Animator, RigidBody, etc. +├── systems/ # PhysicsSystem, CollisionSystem +├── input/ # InputManager +├── audio/ # AudioManager +├── assets/ # AssetLoader +├── math/ # Vector2, Matrix3, Rect, Color +└── utils/ # ObjectPool, EventBus, Quadtree +``` + +## 🏗️ Architecture + +Le moteur suit le pattern **ECS (Entity Component System)** : + +- **Entities (GameObject)** : Objets du jeu +- **Components** : Comportements/composants attachés aux entités +- **Systems** : Systèmes qui mettent à jour les composants + +## 📚 Documentation + +Voir `docs/CORE.MD` pour la documentation complète de l'architecture. + +## 🛠️ Développement + +### Compilation + +```bash +npm run build # Compilation unique +npm run watch # Mode watch +``` + +### Scripts disponibles + +- `pnpm run dev` : Serveur de développement avec hot-reload (Vite) +- `pnpm run build` : Compile le TypeScript et build de production +- `pnpm run preview` : Aperçu du build de production +- `pnpm run clean` : Supprime le dossier dist + +## 🎮 Utilisation + +```typescript +import { Engine } from './core/Engine'; + +const engine = new Engine(800, 600); +await engine.initialize(); +engine.start(); +``` + +## 📝 TODO + +Le développement suit la checklist de `docs/CORE.MD` : + +- [x] Phase 1 : Setup et configuration +- [ ] Phase 2 : Core du moteur +- [ ] Phase 3 : Système WebGL +- [ ] Phase 4 : Système de rendu +- [ ] ... + +## 📄 Licence + +MIT -C'est parti ! Soyez réactif et alerte. Bonne chance ! Vous en aurez besoin :) diff --git a/assets/css/main.css b/assets/css/main.css deleted file mode 100644 index 38d83ce..0000000 --- a/assets/css/main.css +++ /dev/null @@ -1,22 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Press Start 2P', cursive; - font-size: 1.5rem; -} - -.background { - background-image: url("../img/background.jpg"); - background-size: cover; - background-position: center; -} - -.sideMenu { - background-image: url("../img/bricks.png"); - background-repeat: repeat; - box-shadow: inset 0px 0px 6px 6px rgba(0, 0, 0, 0.5); -} \ No newline at end of file diff --git a/assets/img/background.jpg b/assets/img/background.jpg deleted file mode 100644 index 59c790e..0000000 Binary files a/assets/img/background.jpg and /dev/null differ diff --git a/assets/img/bricks.png b/assets/img/bricks.png deleted file mode 100644 index bebd5eb..0000000 Binary files a/assets/img/bricks.png and /dev/null differ diff --git a/assets/img/gui/gui.png b/assets/img/gui/gui.png deleted file mode 100644 index bb75b1d..0000000 Binary files a/assets/img/gui/gui.png and /dev/null differ diff --git a/assets/img/logo_small.png b/assets/img/logo_small.png deleted file mode 100644 index b153717..0000000 Binary files a/assets/img/logo_small.png and /dev/null differ diff --git a/assets/img/loots/health/potion.png b/assets/img/loots/health/potion.png deleted file mode 100644 index 59a5236..0000000 Binary files a/assets/img/loots/health/potion.png and /dev/null differ diff --git a/assets/img/loots/money/bag.png b/assets/img/loots/money/bag.png deleted file mode 100644 index ca6691a..0000000 Binary files a/assets/img/loots/money/bag.png and /dev/null differ diff --git a/assets/img/loots/money/coin.png b/assets/img/loots/money/coin.png deleted file mode 100644 index 55bfa9d..0000000 Binary files a/assets/img/loots/money/coin.png and /dev/null differ diff --git a/assets/img/loots/money/pile.png b/assets/img/loots/money/pile.png deleted file mode 100644 index 10522f1..0000000 Binary files a/assets/img/loots/money/pile.png and /dev/null differ diff --git a/assets/img/projectile/arrow1.png b/assets/img/projectile/arrow1.png deleted file mode 100644 index 621840d..0000000 Binary files a/assets/img/projectile/arrow1.png and /dev/null differ diff --git a/assets/img/projectile/arrow2.png b/assets/img/projectile/arrow2.png deleted file mode 100644 index 3464adc..0000000 Binary files a/assets/img/projectile/arrow2.png and /dev/null differ diff --git a/assets/img/projectile/arrow3.png b/assets/img/projectile/arrow3.png deleted file mode 100644 index a69218f..0000000 Binary files a/assets/img/projectile/arrow3.png and /dev/null differ diff --git a/assets/img/projectile/arrow4.png b/assets/img/projectile/arrow4.png deleted file mode 100644 index 03a99d0..0000000 Binary files a/assets/img/projectile/arrow4.png and /dev/null differ diff --git a/assets/img/tile.png b/assets/img/tile.png deleted file mode 100644 index 535f851..0000000 Binary files a/assets/img/tile.png and /dev/null differ diff --git a/docs/CONTINUE_PROMPT.md b/docs/CONTINUE_PROMPT.md new file mode 100644 index 0000000..8a97166 --- /dev/null +++ b/docs/CONTINUE_PROMPT.md @@ -0,0 +1,207 @@ +# Prompt pour Continuer le Développement du Moteur + +## État Actuel du Moteur (Phase 11 - Gameplay Avancé) + +### ✅ Systèmes Implémentés et Fonctionnels + +**Core :** +- ✅ Engine, GameLoop, Time, Scene, SceneManager +- ✅ Camera avec projection orthographique et matrices view/projection +- ✅ Système de coordonnées monde/écran fonctionnel +- ✅ Ordre d'update optimisé (Scene avant InputManager pour détecter Down/Released) + +**Math :** +- ✅ Vector2, Color, Rect, Matrix3 +- ✅ Toutes les opérations mathématiques nécessaires + +**WebGL :** +- ✅ GLContext, Shader, Buffer, Texture +- ✅ WebGLRenderer avec gestion complète du rendu +- ✅ SpriteBatch pour batch rendering optimisé + +**ECS (Entity Component System) :** +- ✅ Component (classe abstraite de base) +- ✅ Transform (position, rotation, scale, hiérarchie parent-enfant) +- ✅ GameObject (entité avec Transform et Components) + +**Rendu :** +- ✅ SpriteRenderer (composant de rendu de sprites) +- ✅ Système de collecte automatique des renderables +- ✅ Tri par layer (z-index) +- ✅ Pipeline de rendu complet : Scene → getAllRenderables() → SpriteRenderer.render() → SpriteBatch + +**Animations :** +- ✅ Animation (structure de données) +- ✅ Animator (composant de gestion d'animations) +- ✅ Système d'animations multi-directionnelles (8 directions) + +**Input :** +- ✅ InputManager (clavier, souris, états Down/Held/Up/Released) +- ✅ Intégré dans Engine (update automatique optimisé) +- ✅ Conversion coordonnées écran → monde (getMouseWorldPosition) + +**Physique :** +- ✅ Collider2D (classe de base avec triggers et événements) +- ✅ BoxCollider2D (collider rectangulaire AABB) +- ✅ CircleCollider2D (collider circulaire) +- ✅ Rigidbody2D (vélocité, forces, gravité, drag) +- ✅ PhysicsSystem (détection et résolution de collisions avec fixed timestep) +- ✅ Événements de collision (OnTriggerEnter/Exit, OnCollisionEnter/Exit) + +**Asset Management :** +- ✅ AssetLoader (chargement centralisé avec cache, préchargement avec progression) +- ✅ SpriteSheet (spritesheets avec grille uniforme) +- ✅ TextureAtlas (atlas de textures avec JSON) + +**UI System :** +- ✅ UICanvas (système d'interface utilisateur, intégré dans Scene) +- ✅ UIComponent (classe de base pour tous les éléments UI) +- ✅ RectTransform (système de layout avec anchors, pivots, anchors presets) +- ✅ UIText (affichage de texte avec Canvas 2D) +- ✅ UIButton (bouton interactif avec états hover/pressed, callbacks onClick) +- ✅ UIImage (affichage d'images dans l'UI) +- ✅ Rendu UI en overlay (par-dessus la scène de jeu) + +**Gameplay (GameScene + PlayerController) :** +- ✅ Contrôle twin-stick (ZQSD pour mouvement, souris pour orientation) +- ✅ Système de direction basé sur curseur (8 directions avec atan2) +- ✅ Animations walk 8 directions (synchronisées avec direction curseur) +- ✅ Système de tir de projectiles (clic gauche, cooldown) +- ✅ Projectiles avec physique et destruction +- ✅ Attaque au corps-à-corps avec épée (touche A) +- ✅ Animations slash 4 directions (diagonales) +- ✅ Système de dash/roll (clic droit) +- ✅ Animations roll 8 directions avec boost de vitesse +- ✅ Gestion des états (attaque/dash bloquent le mouvement) +- ✅ Caméra qui suit le joueur +- ✅ Environnement de test (sol, murs, ennemis, pièces) + +### Principes à Suivre + +- **TypeScript strict mode** : Toujours respecter +- **ECS (Composition > Héritage)** : Utiliser des Components, pas d'héritage de GameObject +- **deltaTime** : Tous les mouvements doivent utiliser Time.deltaTime +- **Dirty flags** : Utiliser pour optimiser (Transform, Camera) +- **SpriteBatch** : Toujours utiliser pour le rendu (pas de draw direct) + +## Prochaines Étapes Possibles + +### Phase 8 : Physique et Collisions ✅ COMPLÉTÉ +- [x] Collider2D (classe de base abstraite avec triggers et événements) +- [x] BoxCollider2D (collider rectangulaire AABB) +- [x] CircleCollider2D (collider circulaire) +- [x] PhysicsSystem (détection et résolution de collisions) +- [x] Rigidbody2D (physique de mouvement avec vélocité, forces, gravité) +- [x] Trigger events (OnTriggerEnter, OnTriggerExit, OnCollisionEnter, OnCollisionExit) + +### Phase 9 : Asset Management ✅ COMPLÉTÉ +- [x] AssetLoader (chargement de textures, JSON) +- [x] Système de cache des assets +- [x] Préchargement avec progression (callback onProgress) +- [x] SpriteSheet (spritesheets avec grille uniforme) +- [x] TextureAtlas (atlas de textures avec JSON) + +### Phase 10 : UI System ✅ COMPLÉTÉ +- [x] UICanvas (système d'interface) +- [x] UIButton, UIText, UIImage +- [x] Layout system (anchors, constraints) +- [x] Events UI (clics, hover) + +### Phase 11 : Gameplay Avancé ✅ COMPLÉTÉ +- [x] Contrôle twin-stick (mouvement ZQSD + souris pour viser) +- [x] Système de direction 8-way basé sur curseur +- [x] Animations multi-directionnelles (walk, roll, slash) +- [x] Système de tir de projectiles avec cooldown +- [x] Attaque au corps-à-corps (épée) +- [x] Système de dash/roll pour esquiver +- [x] Gestion des états du joueur + +### Phase 12 : Systèmes de Combat +- [ ] Système de vie (HealthComponent) +- [ ] Système de dégâts (DamageComponent) +- [ ] Barre de vie (UI) +- [ ] Système d'ennemis avec IA basique +- [ ] Drop d'objets au kill +- [ ] Score et système de vagues + +### Phase 13 : Effets Visuels +- [ ] ParticleSystem (effets de particules) +- [ ] Screen shake (tremblement caméra) +- [ ] Flash/hit effects +- [ ] Trails (traînées visuelles) + +### Phase 14 : Systèmes Avancés +- [ ] Tilemap (rendu de tiles optimisé) +- [ ] Pathfinding (A* ou équivalent) +- [ ] StateMachine (machine à états pour IA) +- [ ] Audio System (musique, effets sonores) + +## Format pour les Nouvelles Fonctionnalités + +Quand tu implémentes une nouvelle fonctionnalité : + +1. **Crée les fichiers nécessaires** dans les bons dossiers +2. **Respecte l'architecture existante** (ECS, Components, etc.) +3. **Intègre dans Engine/Scene** si nécessaire +4. **Teste la compilation** : `npm run build` +5. **Ajoute un exemple dans index.ts** si applicable +6. **Met à jour DEV_LOG.md** avec la date et description + +## Exemple de Prompt pour Nouvelle Fonctionnalité + +``` +Je veux implémenter [Nom de la fonctionnalité] selon CORE.MD. + +État actuel : +[Lister ce qui existe déjà] + +À faire : +- [ ] [Détailler chaque étape] + +Contraintes : +- TypeScript strict mode +- ECS (Components, pas héritage) +- deltaTime pour les mouvements +- SpriteBatch pour le rendu +- Dirty flags pour optimiser +``` + +## Notes Importantes + +- **Pas d'héritage de GameObject** : Tout doit être des Components +- **Cycle de vie** : Utiliser awake(), start(), update(), onDestroy() +- **Coordonnées** : Système Y-up, caméra à (0,0) par défaut +- **Rendu** : Scene.getAllRenderables() collecte automatiquement les SpriteRenderer +- **Input** : Accès via `engine.input` dans les Components + +## Fichiers Clés + +- `src/core/Engine.ts` - Point d'entrée principal +- `src/core/Scene.ts` - Gestion des GameObjects +- `src/entities/GameObject.ts` - Entité de base +- `src/entities/Component.ts` - Base pour tous les composants +- `src/rendering/WebGLRenderer.ts` - Gestion du rendu +- `src/index.ts` - Exemple de jeu de test + +--- + +**Dernière mise à jour** : Phase 11 complétée - Gameplay avancé avec contrôle twin-stick, dash/roll, et animations 8 directions + +## 📋 Prompt Simple pour IA + +``` +Jeu 2D top-down avec moteur custom TypeScript/WebGL (architecture ECS). + +État actuel : +- Moteur complet : ECS, WebGL, Physique, Animations, UI +- Joueur fonctionnel : twin-stick control, dash, attaque épée, tir projectiles, animations 8 directions +- GameScene avec environnement de test + +Prochaines étapes : +- Phase 12 : Système de combat (vie, dégâts, ennemis avec IA) +- Phase 13 : Effets visuels (particules, screen shake) +- Phase 14 : Systèmes avancés (tilemap, pathfinding, audio) + +Voir docs/CORE.MD pour l'architecture et docs/CONTINUE_PROMPT.md pour les détails. +``` + diff --git a/docs/CORE.MD b/docs/CORE.MD new file mode 100644 index 0000000..fae3428 --- /dev/null +++ b/docs/CORE.MD @@ -0,0 +1,3296 @@ +Documentation complète : Architecture d'un moteur de jeu 2D avec WebGL en TypeScript + +Table des matières + +Introduction et principes généraux +Architecture globale +Core du moteur +Système de rendu WebGL +Système d'entités et composants +Systèmes auxiliaires +Mathématiques +Optimisations +Structure de fichiers +Flow d'exécution +Extensions possibles + + +1. Introduction et principes généraux +Qu'est-ce qu'un moteur de jeu 2D ? +Un moteur de jeu est un ensemble de systèmes qui orchestrent : + +Le rendu : Afficher les images à l'écran +La logique : Mettre à jour les positions, les comportements +Les entrées : Gérer clavier, souris, tactile +La physique : Collisions, gravité +L'audio : Sons et musique +Les assets : Charger et gérer les ressources + +Pourquoi WebGL pour la 2D ? +Canvas 2D utilise le CPU, WebGL utilise le GPU (carte graphique). +Comparaison : + +Canvas 2D : ~1000 sprites maximum à 60 FPS +WebGL : ~10000+ sprites à 60 FPS + +Le GPU a des milliers de petits processeurs qui travaillent en parallèle, idéal pour dessiner beaucoup d'objets. +Principes clés pour la performance + +Batch Rendering : Dessiner plusieurs objets en un seul appel +Object Pooling : Réutiliser les objets au lieu de les créer/détruire +Spatial Partitioning : Ne tester les collisions qu'entre objets proches +Dirty Flags : Ne recalculer que ce qui change +Canvas Layering : Séparer éléments statiques et dynamiques (si multi-canvas) + +Principes pour l'évolutivité + +ECS (Entity Component System) : Composition plutôt qu'héritage +Event Bus : Communication découplée entre systèmes +Interfaces : Abstraction pour chaque système majeur +Modularité : Chaque système indépendant + + +2. Architecture globale +Vue d'ensemble +Engine (point d'entrée) +│ +├── Core/ +│ ├── GameLoop → Boucle principale (update/render) +│ ├── SceneManager → Gestion des scènes/niveaux +│ └── Time → Gestion du temps (deltaTime, etc.) +│ +├── Rendering/ +│ ├── WebGLRenderer → Coordonne tout le rendu +│ ├── SpriteBatch → Optimisation batch rendering +│ ├── Shader → Programmes GPU +│ ├── Material → Shader + paramètres +│ ├── Texture → Gestion des images +│ ├── Camera → Viewport et transformations +│ └── RenderQueue → Tri et optimisation +│ +├── WebGL/ +│ ├── GLContext → Abstraction WebGL +│ ├── Buffer → Buffers GPU (vertices, indices) +│ ├── VertexArray → Configuration des attributs +│ └── Framebuffer → Rendu dans une texture +│ +├── Entities/ +│ ├── GameObject → Entité de base du jeu +│ ├── Transform → Position, rotation, scale +│ └── Component → Classe de base des composants +│ +├── Components/ +│ ├── SpriteRenderer → Affiche un sprite +│ ├── Animator → Gère les animations +│ ├── RigidBody → Physique +│ ├── Collider → Détection de collision +│ ├── ParticleEmitter → Système de particules +│ └── TilemapRenderer → Affichage de tilemap +│ +├── Systems/ +│ ├── PhysicsSystem → Mise à jour de la physique +│ ├── CollisionSystem → Détection/résolution collisions +│ └── AnimationSystem → Mise à jour des animations +│ +├── Input/ +│ └── InputManager → Clavier, souris, tactile +│ +├── Audio/ +│ └── AudioManager → Sons et musique +│ +├── Assets/ +│ └── AssetLoader → Chargement des ressources +│ +├── Math/ +│ ├── Vector2 → Vecteur 2D (x, y) +│ ├── Matrix3 → Matrice 3×3 (transformations 2D) +│ ├── Rect → Rectangle +│ └── Color → Couleur RGBA +│ +└── Utils/ + ├── ObjectPool → Réutilisation d'objets + ├── EventBus → Système d'événements + ├── Quadtree → Spatial partitioning + └── Debug → Outils de debug +``` + +--- + +## 3. Core du moteur + +### 3.1 Engine (orchestrateur principal) + +**Rôle** : Point d'entrée, initialise et coordonne tous les systèmes. + +**Responsabilités** : +- Créer le canvas WebGL +- Initialiser tous les managers (Input, Audio, Assets, Renderer) +- Créer et gérer le SceneManager +- Démarrer/arrêter la GameLoop +- Gérer le cycle de vie (start, pause, resume, stop) +- Exposer une API publique pour l'utilisateur + +**Propriétés importantes** : +- `canvas: HTMLCanvasElement` - Le canvas de rendu +- `renderer: WebGLRenderer` - Le système de rendu +- `sceneManager: SceneManager` - Gestion des scènes +- `inputManager: InputManager` - Gestion des entrées +- `audioManager: AudioManager` - Gestion de l'audio +- `assetLoader: AssetLoader` - Chargement des ressources +- `gameLoop: GameLoop` - La boucle de jeu +- `isRunning: boolean` - État du moteur + +**Méthodes principales** : +- `initialize()` - Setup initial +- `start()` - Démarre le moteur +- `stop()` - Arrête le moteur +- `pause()` - Met en pause +- `resume()` - Reprend +- `loadScene(name: string)` - Charge une scène + +--- + +### 3.2 GameLoop (boucle de jeu) + +**Rôle** : Cœur battant du moteur, appelle update() et render() en boucle. + +**Responsabilités** : +- Utiliser `requestAnimationFrame()` pour synchronisation avec l'écran +- Calculer le `deltaTime` (temps écoulé depuis la dernière frame) +- Appeler `update()` pour la logique du jeu +- Appeler `render()` pour le dessin +- Gérer le fixed timestep pour la physique (optionnel mais recommandé) +- Gérer la pause/reprise + +**Propriétés importantes** : +- `isRunning: boolean` - État de la boucle +- `lastTime: number` - Timestamp de la frame précédente +- `deltaTime: number` - Temps écoulé depuis dernière frame +- `targetFPS: number` - FPS cible (optionnel, pour limiter) +- `accumulator: number` - Pour fixed timestep physique + +**Cycle d'une frame** : +``` +1. Calculer deltaTime (currentTime - lastTime) +2. Mettre à jour Time.deltaTime +3. Appeler fixedUpdate() pour la physique (timestep fixe) +4. Appeler update() pour la logique (timestep variable) +5. Appeler render() pour le dessin +6. Demander la prochaine frame via requestAnimationFrame() +``` + +**Pourquoi fixed timestep pour la physique ?** : +La physique doit être déterministe. Avec un timestep fixe (ex: 16ms = 60 FPS), les calculs sont toujours identiques, évitant les bugs bizarres. + +--- + +### 3.3 SceneManager (gestion des scènes) + +**Rôle** : Gère les différents "écrans" du jeu (menu, niveaux, game over...). + +**Responsabilités** : +- Stocker toutes les scènes disponibles +- Charger/décharger une scène +- Gérer les transitions entre scènes +- N'update/render que la scène active +- Précharger les scènes en arrière-plan (optionnel) + +**Propriétés importantes** : +- `scenes: Map` - Toutes les scènes +- `activeScene: Scene | null` - La scène actuellement active +- `isTransitioning: boolean` - En cours de transition + +**Méthodes principales** : +- `registerScene(name: string, scene: Scene)` - Enregistre une scène +- `loadScene(name: string)` - Change de scène +- `unloadScene(name: string)` - Décharge une scène +- `getActiveScene(): Scene` - Récupère la scène active +- `update(deltaTime: number)` - Update la scène active +- `render()` - Rend la scène active + +**Exemple d'utilisation** : +``` +sceneManager.registerScene("MainMenu", new MainMenuScene()); +sceneManager.registerScene("Level1", new Level1Scene()); +sceneManager.registerScene("GameOver", new GameOverScene()); + +sceneManager.loadScene("MainMenu"); +// Plus tard... +sceneManager.loadScene("Level1"); +``` + +--- + +### 3.4 Scene (une scène de jeu) + +**Rôle** : Contient tous les GameObjects d'un niveau/écran. + +**Responsabilités** : +- Stocker tous les GameObjects de la scène +- Gérer la hiérarchie des objets +- Initialiser la scène (onLoad) +- Nettoyer la scène (onUnload) +- Fournir des méthodes de recherche d'objets +- Update tous les GameObjects actifs +- Collecter les objets à rendre + +**Propriétés importantes** : +- `name: string` - Nom de la scène +- `gameObjects: GameObject[]` - Liste de tous les objets +- `camera: Camera` - Caméra principale de la scène +- `isLoaded: boolean` - État de chargement + +**Méthodes principales** : +- `onLoad()` - Appelé au chargement de la scène +- `onUnload()` - Appelé au déchargement +- `update(deltaTime: number)` - Update tous les objets +- `addGameObject(obj: GameObject)` - Ajoute un objet +- `removeGameObject(obj: GameObject)` - Retire un objet +- `findGameObjectByName(name: string)` - Recherche par nom +- `getAllRenderables()` - Récupère tous les objets à rendre + +--- + +### 3.5 Time (gestion du temps) + +**Rôle** : Centralise toutes les informations temporelles du jeu. + +**Responsabilités** : +- Fournir le `deltaTime` à tout le moteur +- Gérer le `timeScale` (ralenti, accéléré) +- Compter le temps total écoulé +- Compter les frames rendues +- Calculer les FPS réels + +**Propriétés importantes** : +- `deltaTime: number` - Temps écoulé depuis dernière frame (en secondes) +- `unscaledDeltaTime: number` - DeltaTime non affecté par timeScale +- `timeScale: number` - Multiplicateur de temps (1 = normal, 0.5 = ralenti, 2 = rapide) +- `time: number` - Temps total depuis le démarrage (en secondes) +- `frameCount: number` - Nombre de frames rendues +- `fps: number` - FPS moyen + +**Pourquoi c'est crucial** : +Sans deltaTime, un objet qui bouge de 5 pixels par frame ira : +- 2× plus vite sur un écran 120Hz que sur un 60Hz +- Beaucoup plus lentement si le jeu lag + +**Avec deltaTime** : +``` +// Mauvais : +position.x += 5; // Dépend des FPS + +// Bon : +position.x += 300 * Time.deltaTime; // 300 pixels/seconde indépendant des FPS +``` + +--- + +## 4. Système de rendu WebGL + +### 4.1 WebGL : Concepts de base + +**WebGL** est une API pour utiliser le GPU du navigateur. + +**Pipeline de rendu WebGL** : +``` +1. Données JavaScript (positions, couleurs, etc.) + ↓ +2. Buffers GPU (envoi des données au GPU) + ↓ +3. Vertex Shader (transforme chaque sommet) + ↓ +4. Rasterization (convertit triangles en pixels) + ↓ +5. Fragment Shader (colorie chaque pixel) + ↓ +6. Framebuffer (image finale) +``` + +**Concepts clés** : + +**Vertices (sommets)** : Points dans l'espace 3D/2D. En 2D, tout est composé de triangles. + +**Buffers** : Mémoire sur le GPU contenant des données (positions, couleurs, UVs...). + +**Shaders** : Petits programmes qui s'exécutent sur le GPU, écrits en GLSL (langage proche du C). + +**Uniforms** : Variables globales partagées par tous les vertices/pixels (ex: matrices de transformation, couleur globale). + +**Attributes** : Données qui varient pour chaque vertex (ex: position, couleur). + +**Textures** : Images stockées sur le GPU. + +**Système de coordonnées WebGL** : +``` + Y (1) + | + | + -1 ----+---- 1 X + | + | + -1 + +Centre = (0, 0) +Haut-droite = (1, 1) +Bas-gauche = (-1, -1) +``` + +Il faut convertir les coordonnées pixel en coordonnées normalisées (NDC = Normalized Device Coordinates). + +--- + +### 4.2 WebGLRenderer (coordonne le rendu) + +**Rôle** : Chef d'orchestre du système de rendu. + +**Responsabilités** : +- Initialiser le contexte WebGL +- Gérer le SpriteBatch +- Gérer la bibliothèque de shaders +- Gérer le cache de textures +- Gérer la RenderQueue +- Clear le canvas +- Coordonner le rendu de la scène +- Appliquer les transformations de caméra + +**Propriétés importantes** : +- `gl: WebGLRenderingContext` - Contexte WebGL +- `spriteBatch: SpriteBatch` - Système de batch rendering +- `renderQueue: RenderQueue` - File de rendu triée +- `shaderLibrary: Map` - Tous les shaders +- `textureCache: Map` - Toutes les textures chargées +- `defaultShader: Shader` - Shader sprite par défaut +- `currentShader: Shader` - Shader actuellement actif +- `viewportWidth: number` - Largeur du viewport +- `viewportHeight: number` - Hauteur du viewport + +**Méthodes principales** : +- `initialize()` - Setup WebGL +- `clear(color: Color)` - Efface le canvas +- `render(scene: Scene, camera: Camera)` - Rend une scène +- `loadShader(name: string, vs: string, fs: string)` - Charge un shader +- `loadTexture(name: string, url: string)` - Charge une texture +- `resize(width: number, height: number)` - Redimensionne le viewport + +**Flow de rendu** : +``` +1. Clear le canvas +2. Collecter tous les objets visibles de la scène +3. Les soumettre à la RenderQueue +4. Trier la RenderQueue +5. Commencer le SpriteBatch +6. Pour chaque objet trié : + - Changer de shader si nécessaire + - Ajouter au batch +7. Flush le batch +``` + +--- + +### 4.3 SpriteBatch (★ optimisation cruciale) + +**Rôle** : Accumule plusieurs sprites et les dessine en un seul draw call. + +**Problème sans batch** : +``` +Pour 1000 sprites : +- 1000 draw calls au GPU +- Très lent (changements d'état GPU coûteux) +``` + +**Solution avec batch** : +``` +Pour 1000 sprites : +- 1 draw call au GPU +- Jusqu'à 100× plus rapide ! +``` + +**Responsabilités** : +- Accumuler les données de plusieurs sprites dans un gros buffer +- Gérer un buffer de vertices (positions, UVs, couleurs) +- Gérer un buffer d'indices (ordre de dessin des triangles) +- Flush (dessiner) quand : + - Le batch est plein (ex: 10000 sprites max) + - La texture change + - Le shader change +- Transformer les sprites (appliquer rotation, scale, position) +- Calculer les UVs (coordonnées texture) + +**Propriétés importantes** : +- `maxSprites: number` - Capacité max (ex: 10000) +- `vertices: Float32Array` - Buffer CPU des vertices +- `indices: Uint16Array` - Buffer CPU des indices +- `vertexBuffer: WebGLBuffer` - Buffer GPU +- `indexBuffer: WebGLBuffer` - Buffer GPU +- `spriteCount: number` - Nombre de sprites actuellement dans le batch +- `currentTexture: Texture` - Texture actuellement liée +- `shader: Shader` - Shader utilisé par le batch + +**Structure d'un vertex** : +``` +[x, y, u, v, r, g, b, a] + │ │ │ │ └─────────┘ + │ │ │ │ Couleur (RGBA) + │ │ └──┘ + │ │ Coordonnées texture (UV) + └──┘ + Position 2D +``` + +**Méthodes principales** : +- `begin(projection: Matrix3, view: Matrix3)` - Commence un batch +- `draw(texture, transform, sourceRect, tint)` - Ajoute un sprite +- `flush()` - Envoie tout au GPU et dessine +- `end()` - Termine le batch + +**Processus d'ajout d'un sprite** : +``` +1. Vérifier si le batch est plein ou si la texture change + → Si oui, flush() +2. Calculer les 4 coins du sprite (quad) avec rotation/scale +3. Calculer les UVs (quelle partie de la texture) +4. Remplir le buffer avec les 8 vertices (4 × 2 triangles) +5. Incrémenter spriteCount + +4.4 Shader (programmes GPU) +Rôle : Programme qui s'exécute sur le GPU pour transformer et colorier. +Composition : + +Vertex Shader : Transforme les positions des sommets +Fragment Shader : Décide de la couleur de chaque pixel + +Responsabilités : + +Compiler les shaders GLSL +Linker le programme +Gérer les uniforms (variables globales) +Gérer les attributes (données par vertex) +Activer/désactiver le shader + +Propriétés importantes : + +program: WebGLProgram - Programme GPU compilé +uniforms: Map - Emplacements des uniforms +attributes: Map - Emplacements des attributes + +Méthodes principales : + +compile(vertexSource: string, fragmentSource: string) - Compile +use() - Active le shader +setUniformMatrix3(name: string, matrix: Matrix3) - Envoie une matrice +setUniformFloat(name: string, value: number) - Envoie un float +setUniformVec2(name: string, x: number, y: number) - Envoie un vec2 +setUniformTexture(name: string, texture: Texture, slot: number) - Lie une texture + +Shader sprite par défaut - Vertex : +glslattribute vec2 aPosition; // Position du vertex +attribute vec2 aTexCoord; // Coordonnées UV +attribute vec4 aColor; // Couleur de teinte + +uniform mat3 uProjection; // Matrice de projection +uniform mat3 uView; // Matrice de vue + +varying vec2 vTexCoord; // Passe au fragment shader +varying vec4 vColor; + +void main() { + // Transforme la position + vec3 pos = uProjection * uView * vec3(aPosition, 1.0); + gl_Position = vec4(pos.xy, 0.0, 1.0); + + // Passe les données au fragment shader + vTexCoord = aTexCoord; + vColor = aColor; +} +Shader sprite par défaut - Fragment : +glslprecision mediump float; + +uniform sampler2D uTexture; // La texture + +varying vec2 vTexCoord; // Reçu du vertex shader +varying vec4 vColor; + +void main() { + // Échantillonne la texture + vec4 texColor = texture2D(uTexture, vTexCoord); + + // Multiplie par la couleur de teinte + gl_FragColor = texColor * vColor; +} +``` + +--- + +### 4.5 Material (shader + paramètres) + +**Rôle** : Combine un shader avec ses paramètres (uniforms custom). + +**Responsabilités** : +- Référencer un shader +- Stocker les valeurs des uniforms +- Appliquer les uniforms au shader avant le rendu + +**Propriétés importantes** : +- `shader: Shader` - Le shader utilisé +- `uniforms: Map` - Valeurs des uniforms +- `texture: Texture` - Texture principale (optionnel) + +**Méthodes principales** : +- `setUniform(name: string, value: any)` - Définit un uniform +- `apply()` - Applique tous les uniforms au shader + +**Exemple d'utilisation** : +``` +// Créer un material pour un effet de glow +const glowMaterial = new Material(glowShader); +glowMaterial.setUniform("glowColor", new Color(1, 0.5, 0, 1)); +glowMaterial.setUniform("glowIntensity", 2.0); + +// Utiliser sur un sprite +spriteRenderer.material = glowMaterial; +``` + +--- + +### 4.6 Texture (gestion des images) + +**Rôle** : Encapsule une texture WebGL (image sur le GPU). + +**Responsabilités** : +- Charger une image depuis une URL +- Créer une texture WebGL +- Configurer les paramètres de texture (filtrage, wrapping) +- Lier la texture à un slot +- Libérer la mémoire GPU + +**Propriétés importantes** : +- `glTexture: WebGLTexture` - Texture GPU +- `width: number` - Largeur +- `height: number` - Hauteur +- `id: number` - Identifiant unique +- `filterMode: FilterMode` - LINEAR ou NEAREST +- `wrapMode: WrapMode` - REPEAT, CLAMP, MIRROR + +**Méthodes principales** : +- `loadFromImage(image: HTMLImageElement)` - Charge depuis une image +- `bind(slot: number)` - Active la texture sur un slot (0-31) +- `unbind()` - Désactive +- `dispose()` - Libère la mémoire GPU + +**Paramètres importants** : + +**Filter Mode** : +- `NEAREST` : Pixels nets (pixel art) +- `LINEAR` : Lissage (sprites HD) + +**Wrap Mode** : +- `CLAMP` : Bords figés +- `REPEAT` : Répétition (pour tiling) +- `MIRROR` : Répétition miroir + +--- + +### 4.7 Camera (viewport et transformations) + +**Rôle** : Définit quelle partie du monde est visible. + +**Responsabilités** : +- Définir la position dans le monde +- Gérer le zoom +- Gérer la rotation (effets spéciaux) +- Calculer la matrice de vue (View Matrix) +- Calculer la matrice de projection (Projection Matrix) +- Convertir coordonnées écran ↔ monde +- Suivre un objet (ex: le joueur) +- Gérer les limites de la caméra (bounds) + +**Propriétés importantes** : +- `position: Vector2` - Position dans le monde +- `zoom: number` - Niveau de zoom (1 = normal, 2 = 2× plus proche) +- `rotation: number` - Rotation en degrés +- `viewportWidth: number` - Largeur de l'écran +- `viewportHeight: number` - Hauteur de l'écran +- `bounds: Rect` - Limites de déplacement (optionnel) +- `target: GameObject` - Objet à suivre (optionnel) + +**Méthodes principales** : +- `getViewMatrix(): Matrix3` - Matrice de vue +- `getProjectionMatrix(): Matrix3` - Matrice de projection +- `screenToWorld(screenPos: Vector2): Vector2` - Convertit écran → monde +- `worldToScreen(worldPos: Vector2): Vector2` - Convertit monde → écran +- `follow(target: GameObject, smoothness: number)` - Suit un objet +- `shake(intensity: number, duration: number)` - Secoue l'écran + +**Matrices expliquées** : + +**Projection Matrix** : Convertit pixels → coordonnées NDC WebGL (-1 à 1) + +**View Matrix** : Applique position, rotation, zoom de la caméra + +**Exemple** : +``` +Objet à (1000, 500) dans le monde +Caméra à (900, 400), zoom 2 + +View Matrix appliquée : +- Translation : (1000 - 900, 500 - 400) = (100, 100) +- Zoom ×2 : (200, 200) + +Projection Matrix : +- Convertit (200, 200) pixels → coordonnées NDC + +Résultat : L'objet s'affiche au bon endroit sur l'écran +``` + +--- + +### 4.8 RenderQueue (tri et optimisation) + +**Rôle** : Optimise le rendu en triant les objets intelligemment. + +**Responsabilités** : +- Collecter tous les objets à rendre +- Les trier par : + 1. **Layer** (z-index) : Objets du fond vers le devant + 2. **Shader** : Minimise les changements de shader (coûteux) + 3. **Texture** : Minimise les changements de texture +- Culling : Ignorer les objets hors de la caméra +- Frustum culling : Ne rendre que ce qui est visible + +**Propriétés importantes** : +- `renderables: Renderable[]` - Liste des objets à rendre +- `camera: Camera` - Pour le culling + +**Méthodes principales** : +- `submit(renderable: Renderable)` - Ajoute un objet +- `sort()` - Trie intelligemment +- `render(spriteBatch: SpriteBatch)` - Rend tout +- `clear()` - Vide la queue + +**Pourquoi trier ?** + +Changer de shader ou de texture sur le GPU est **très coûteux**. + +**Sans tri** : +``` +Sprite A (texture 1, shader 1) +Sprite B (texture 2, shader 1) → Change texture +Sprite C (texture 1, shader 1) → Change texture += 2 changements de texture inutiles +``` + +**Avec tri** : +``` +Sprite A (texture 1, shader 1) +Sprite C (texture 1, shader 1) → Pas de changement +Sprite B (texture 2, shader 1) → Change texture 1 fois += Optimisé ! + +4.9 Abstraction WebGL (classes utilitaires) +GLContext +Rôle : Encapsule le contexte WebGL, simplifie l'API. +Responsabilités : + +Initialiser WebGL +Gérer les extensions WebGL +Fournir des helpers pour les opérations courantes +Gérer les états WebGL (blend mode, depth test...) + +Buffer +Rôle : Encapsule un buffer WebGL (vertex ou index). +Responsabilités : + +Créer un buffer GPU +Uploader des données (Float32Array, Uint16Array) +Lier/délier le buffer +Gérer le type (STATIC_DRAW, DYNAMIC_DRAW, STREAM_DRAW) + +Types de buffers : + +Vertex Buffer : Contient les données des vertices +Index Buffer : Contient l'ordre de dessin + +VertexArray (VAO) +Rôle : Stocke la configuration des attributs vertex. +Responsabilités : + +Configurer les attributs (position, UV, couleur...) +Lier les buffers aux attributs +Activer/désactiver le VAO + +Avantage : Évite de reconfigurer les attributs à chaque draw. +Framebuffer +Rôle : Permet de rendre dans une texture au lieu de l'écran. +Responsabilités : + +Créer un framebuffer +Attacher une texture de couleur +Attacher un depth/stencil buffer (optionnel) +Lier/délier le framebuffer + +Utilité : Post-processing, effets, render to texture. + +5. Système d'entités et composants +5.1 GameObject (entité de base) +Rôle : Tout ce qui existe dans le jeu est un GameObject. +Exemples : Joueur, ennemi, projectile, pièce, mur, déclencheur... +Responsabilités : + +Avoir un Transform (position, rotation, scale) +Contenir des Components (comportements) +Gérer une hiérarchie parent-enfant +Avoir un état actif/inactif +Avoir un nom (pour debug/recherche) +Gérer son cycle de vie (awake, start, update, destroy) + +Propriétés importantes : + +name: string - Nom de l'objet +active: boolean - Actif ou non +transform: Transform - Position, rotation, scale +`components +RéessayerSContinuer: Map` - Tous les composants + +parent: GameObject | null - Parent dans la hiérarchie +children: GameObject[] - Enfants +scene: Scene - Scène à laquelle il appartient +layer: number - Layer de rendu (z-index) +tag: string - Tag pour catégorisation (ex: "Player", "Enemy") + +Méthodes principales : + +addComponent(component: T): T - Ajoute un composant +getComponent(type: new (...args) => T): T | null - Récupère un composant +removeComponent(type: new (...args) => T) - Retire un composant +addChild(child: GameObject) - Ajoute un enfant +removeChild(child: GameObject) - Retire un enfant +setParent(parent: GameObject) - Change de parent +update(deltaTime: number) - Update tous les composants +destroy() - Détruit l'objet + +Cycle de vie : +1. Construction : new GameObject() +2. awake() : Appelé à la création +3. start() : Appelé avant la première update +4. update(deltaTime) : Appelé chaque frame +5. destroy() : Appelé à la destruction +``` + +**Hiérarchie parent-enfant** : +``` +Personnage (GameObject) +├── Transform (x: 100, y: 200) +├── SpriteRenderer +└── Enfants: + ├── Épée (GameObject) + │ ├── Transform (local: x: 20, y: 0) + │ └── SpriteRenderer + └── Bouclier (GameObject) + ├── Transform (local: x: -20, y: 0) + └── SpriteRenderer + +Si le personnage se déplace de (10, 5), +l'épée et le bouclier suivent automatiquement ! +``` + +--- + +### 5.2 Transform (position, rotation, scale) + +**Rôle** : Définit où et comment l'objet apparaît dans le monde. + +**Responsabilités** : +- Stocker position, rotation, scale +- Calculer la matrice de transformation +- Gérer les transformations locales vs monde (local space vs world space) +- Propager les changements aux enfants +- Utiliser des dirty flags pour optimiser + +**Propriétés importantes** : +- `localPosition: Vector2` - Position relative au parent +- `localRotation: number` - Rotation relative (en degrés) +- `localScale: Vector2` - Scale relatif +- `position: Vector2` (getter/setter) - Position monde +- `rotation: number` (getter/setter) - Rotation monde +- `scale: Vector2` (getter/setter) - Scale monde +- `parent: Transform | null` - Transform du parent +- `children: Transform[]` - Transforms des enfants + +**Propriétés calculées (avec cache)** : +- `worldMatrix: Matrix3` - Matrice de transformation complète +- `right: Vector2` - Vecteur droit (après rotation) +- `up: Vector2` - Vecteur haut (après rotation) +- `forward: Vector2` - Direction avant + +**Méthodes principales** : +- `translate(delta: Vector2)` - Déplace l'objet +- `rotate(angle: number)` - Tourne l'objet +- `lookAt(target: Vector2)` - Regarde vers une position +- `getWorldMatrix(): Matrix3` - Récupère la matrice (avec cache) +- `transformPoint(point: Vector2): Vector2` - Transforme un point local → monde +- `inverseTransformPoint(point: Vector2): Vector2` - Transforme un point monde → local + +**Dirty Flags (optimisation)** : +``` +private _dirty = false; +private _cachedMatrix: Matrix3; + +set position(value: Vector2) { + this._localPosition = value; + this._dirty = true; // Marque "besoin de recalculer" + this.markChildrenDirty(); // Propage aux enfants +} + +getWorldMatrix(): Matrix3 { + if (this._dirty) { + this._cachedMatrix = this.calculateMatrix(); + this._dirty = false; + } + return this._cachedMatrix; // Retourne le cache +} +``` + +**Avantage** : Économise 80% des calculs pour les objets statiques. + +--- + +### 5.3 Component (classe de base) + +**Rôle** : Classe abstraite pour tous les comportements attachables. + +**Philosophie ECS** : Composition > Héritage + +**Au lieu de** : +``` +class Enemy extends Character extends Entity { } +class FlyingEnemy extends Enemy { } +class ShootingFlyingEnemy extends FlyingEnemy { } +// Hiérarchie cauchemardesque ! +``` + +**On fait** : +``` +const enemy = new GameObject(); +enemy.addComponent(new SpriteRenderer()); +enemy.addComponent(new Health(100)); +enemy.addComponent(new AIBehavior()); +enemy.addComponent(new GroundMovement()); + +// Facilement modifiable : +enemy.removeComponent(GroundMovement); +enemy.addComponent(new FlyingMovement()); +``` + +**Responsabilités du Component** : +- Référencer son GameObject parent +- Avoir un état actif/inactif +- Implémenter le cycle de vie (awake, start, update, destroy) +- Communiquer avec d'autres components via le GameObject + +**Propriétés importantes** : +- `gameObject: GameObject` - GameObject parent +- `transform: Transform` - Shortcut vers gameObject.transform +- `enabled: boolean` - Actif ou non + +**Méthodes du cycle de vie (à override)** : +- `awake()` - Appelé à l'ajout du component +- `start()` - Appelé avant la première update +- `update(deltaTime: number)` - Appelé chaque frame +- `onDestroy()` - Appelé à la destruction + +**Méthodes utilitaires** : +- `getComponent(type): T` - Shortcut vers gameObject.getComponent() + +--- + +## 6. Composants principaux + +### 6.1 SpriteRenderer (affiche un sprite) + +**Rôle** : Composant pour afficher une image 2D. + +**Responsabilités** : +- Référencer une texture +- Définir quelle partie de la texture afficher (pour spritesheets) +- Appliquer une couleur de teinte (tint) +- Gérer le flip horizontal/vertical +- Définir le layer de rendu +- S'enregistrer auprès du renderer +- Se dessiner via le SpriteBatch + +**Propriétés importantes** : +- `texture: Texture` - L'image à afficher +- `sourceRect: Rect` - Quelle partie de la texture (pour spritesheet) +- `tint: Color` - Couleur de teinte (blanc = normal) +- `flipX: boolean` - Miroir horizontal +- `flipY: boolean` - Miroir vertical +- `layer: number` - Z-index pour le tri +- `material: Material` - Shader + uniforms custom (optionnel) +- `pivot: Vector2` - Point d'ancrage (0.5, 0.5 = centre) + +**Méthodes principales** : +- `render(spriteBatch: SpriteBatch)` - Dessine le sprite +- `setSprite(texture: Texture, sourceRect: Rect)` - Change le sprite + +**Utilisation** : +``` +const player = new GameObject(); +const renderer = player.addComponent(new SpriteRenderer()); +renderer.texture = assetLoader.getTexture("player"); +renderer.sourceRect = new Rect(0, 0, 32, 32); // Sprite de 32×32 +renderer.tint = new Color(1, 1, 1, 1); // Blanc opaque +renderer.layer = 3; +``` + +--- + +### 6.2 Animator (gère les animations) + +**Rôle** : Anime un SpriteRenderer en changeant son sourceRect. + +**Responsabilités** : +- Stocker plusieurs animations (idle, walk, jump, attack...) +- Jouer une animation +- Gérer le timing entre les frames +- Supporter les animations en boucle ou one-shot +- Déclencher des événements à certaines frames +- Transitionner entre animations + +**Propriétés importantes** : +- `animations: Map` - Toutes les animations +- `currentAnimation: Animation | null` - Animation en cours +- `currentFrame: number` - Frame actuelle +- `frameTime: number` - Temps écoulé sur la frame actuelle +- `isPlaying: boolean` - En cours de lecture +- `speed: number` - Vitesse de lecture (1 = normal, 2 = 2× plus rapide) + +**Structure d'une Animation** : +``` +class Animation { + name: string; + frames: Rect[]; // Liste des sourceRects + frameDuration: number; // Durée de chaque frame (en secondes) + loop: boolean; // Boucle ou non + events: AnimationEvent[]; // Événements (ex: "playSound" à la frame 3) +} +``` + +**Méthodes principales** : +- `addAnimation(name: string, animation: Animation)` - Ajoute une animation +- `play(name: string, restart: boolean)` - Joue une animation +- `stop()` - Arrête l'animation +- `pause()` - Met en pause +- `resume()` - Reprend +- `update(deltaTime: number)` - Avance l'animation + +**Exemple d'utilisation** : +``` +const animator = player.addComponent(new Animator()); + +// Créer une animation de marche (8 frames) +const walkAnim = new Animation("walk", [ + new Rect(0, 0, 32, 32), // Frame 0 + new Rect(32, 0, 32, 32), // Frame 1 + new Rect(64, 0, 32, 32), // Frame 2 + // ... etc +], 0.1, true); // 0.1s par frame, en boucle + +animator.addAnimation("walk", walkAnim); +animator.play("walk"); +``` + +--- + +### 6.3 RigidBody (physique) + +**Rôle** : Ajoute des propriétés physiques à un objet. + +**Responsabilités** : +- Stocker la vélocité (vitesse) +- Appliquer la gravité +- Appliquer des forces +- Gérer la masse +- Gérer le drag (friction de l'air) +- Gérer le type de body (static, kinematic, dynamic) +- Intégrer le mouvement (mise à jour de la position) + +**Propriétés importantes** : +- `velocity: Vector2` - Vitesse actuelle (pixels/seconde) +- `acceleration: Vector2` - Accélération +- `mass: number` - Masse (affecte les forces) +- `drag: number` - Friction aérienne (0-1, 0 = pas de friction) +- `gravityScale: number` - Multiplicateur de gravité (1 = normal, 0 = pas de gravité) +- `bodyType: BodyType` - Type de corps +- `useGravity: boolean` - Affecté par la gravité ou non +- `freezeRotation: boolean` - Empêche la rotation + +**Types de Body** : +- **Static** : Ne bouge jamais (murs, sol) +- **Kinematic** : Bouge mais n'est pas affecté par les forces (plateformes mobiles) +- **Dynamic** : Physique complète (joueur, ennemis, objets) + +**Méthodes principales** : +- `addForce(force: Vector2)` - Ajoute une force +- `addImpulse(impulse: Vector2)` - Ajoute une impulsion instantanée +- `setVelocity(velocity: Vector2)` - Définit la vitesse directement +- `update(deltaTime: number)` - Intègre le mouvement + +**Intégration du mouvement** : +``` +update(deltaTime: number) { + if (bodyType !== BodyType.Dynamic) return; + + // Applique la gravité + if (useGravity) { + velocity.y += Physics.gravity * gravityScale * deltaTime; + } + + // Applique la friction + velocity.x *= (1 - drag); + velocity.y *= (1 - drag); + + // Met à jour la position + transform.position.x += velocity.x * deltaTime; + transform.position.y += velocity.y * deltaTime; +} +``` + +--- + +### 6.4 Collider (détection de collision) + +**Rôle** : Définit la forme de collision d'un objet. + +**Types de Colliders** : +- **BoxCollider** : Rectangle (le plus courant en 2D) +- **CircleCollider** : Cercle +- **PolygonCollider** : Polygone custom +- **EdgeCollider** : Ligne (pour les sols) + +**Responsabilités** : +- Définir la forme de collision +- Définir si c'est un trigger (traverse les objets) ou solid +- Calculer les bounds (rectangle englobant) +- Tester la collision avec d'autres colliders +- Déclencher des événements de collision + +**Propriétés importantes (BoxCollider exemple)** : +- `size: Vector2` - Taille du rectangle +- `offset: Vector2` - Décalage par rapport au Transform +- `isTrigger: boolean` - Trigger ou solid +- `layer: number` - Layer de collision (pour filtrer) + +**Méthodes principales** : +- `getBounds(): Rect` - Rectangle englobant +- `contains(point: Vector2): boolean` - Teste si un point est dedans +- `overlaps(other: Collider): boolean` - Teste la collision avec un autre +- `onCollisionEnter(other: Collider)` - Callback de collision +- `onCollisionExit(other: Collider)` - Callback de fin de collision +- `onTriggerEnter(other: Collider)` - Callback de trigger + +**Différence Trigger vs Solid** : +- **Solid** : Empêche le passage (murs, sol) +- **Trigger** : Détecte l'entrée mais laisse passer (zone de collecte, checkpoint) + +--- + +### 6.5 ParticleEmitter (système de particules) + +**Rôle** : Émetteur de particules (feu, fumée, étincelles, pluie...). + +**Responsabilités** : +- Créer des particules +- Mettre à jour leur vie +- Recycler les particules mortes (object pooling) +- Appliquer des forces (gravité, vent) +- Rendre toutes les particules (bénéficie énormément de WebGL batch) + +**Propriétés importantes** : +- `maxParticles: number` - Nombre max de particules (ex: 5000) +- `emissionRate: number` - Particules par seconde +- `lifetime: number` - Durée de vie d'une particule (secondes) +- `startColor: Color` - Couleur initiale +- `endColor: Color` - Couleur finale (interpolation) +- `startSize: number` - Taille initiale +- `endSize: number` - Taille finale +- `startVelocity: Vector2` - Vitesse initiale (avec randomness) +- `gravity: Vector2` - Gravité des particules +- `texture: Texture` - Texture de la particule + +**Structure d'une Particule** : +``` +class Particle { + position: Vector2; + velocity: Vector2; + color: Color; + size: number; + rotation: number; + lifetime: number; + age: number; + active: boolean; +} +``` + +**Méthodes principales** : +- `emit(count: number)` - Émet X particules +- `update(deltaTime: number)` - Met à jour toutes les particules +- `render(spriteBatch: SpriteBatch)` - Dessine toutes les particules + +**Avantage WebGL** : +Avec Canvas 2D, 500 particules = lag. +Avec WebGL + batch, 5000 particules = fluide ! + +--- + +### 6.6 TilemapRenderer (affichage de tilemap) + +**Rôle** : Affiche efficacement une grille de tiles (tuiles). + +**Responsabilités** : +- Stocker une grille de tiles +- Référencer un tileset (spritesheet de tiles) +- Rendre uniquement les tiles visibles (culling) +- Optimiser avec un seul draw call pour toute la tilemap +- Supporter plusieurs layers (fond, milieu, avant-plan) + +**Propriétés importantes** : +- `tiles: number[][]` - Grille 2D d'IDs de tiles +- `tileset: Texture` - Spritesheet contenant tous les tiles +- `tileSize: number` - Taille d'un tile (ex: 16×16 pixels) +- `mapWidth: number` - Largeur en tiles +- `mapHeight: number` - Hauteur en tiles +- `layer: number` - Layer de rendu + +**Méthodes principales** : +- `setTile(x: number, y: number, tileId: number)` - Change un tile +- `getTile(x: number, y: number): number` - Récupère un tile +- `render(spriteBatch: SpriteBatch, camera: Camera)` - Rend la tilemap +- `worldToTile(worldPos: Vector2): Vector2` - Convertit monde → tile +- `tileToWorld(tilePos: Vector2): Vector2` - Convertit tile → monde + +**Optimisation** : +``` +// Au lieu de dessiner TOUS les tiles : +for (let y = 0; y < mapHeight; y++) { + for (let x = 0; x < mapWidth; x++) { + drawTile(x, y); // 1000× 1000 = 1 million de tiles ! + } +} + +// On dessine seulement les tiles visibles : +const visibleArea = camera.getVisibleTileArea(); +for (let y = visibleArea.top; y < visibleArea.bottom; y++) { + for (let x = visibleArea.left; x < visibleArea.right; x++) { + drawTile(x, y); // Seulement ~400 tiles ! + } +} +``` + +--- + +## 7. Systèmes auxiliaires + +### 7.1 InputManager (clavier, souris, tactile) + +**Rôle** : Centralise toutes les entrées utilisateur. + +**Responsabilités** : +- Écouter les événements du navigateur (keydown, keyup, mousedown...) +- Stocker l'état actuel de toutes les touches/boutons +- Différencier les états : + - **Down** : Pressé cette frame + - **Held** : Maintenu + - **Up** : Relâché cette frame +- Gérer la souris (position, clics, molette) +- Gérer le tactile (touches, gestures) +- Réinitialiser les états "down" et "up" chaque frame + +**Propriétés importantes** : +- `keys: Map` - État de toutes les touches +- `mousePosition: Vector2` - Position de la souris +- `mouseButtons: Map` - État des boutons souris +- `touches: Touch[]` - Points de touche actifs +- `wheelDelta: number` - Déplacement de la molette + +**États possibles** : +``` +enum KeyState { + Up, // Pas pressé + Down, // Pressé cette frame + Held, // Maintenu + Released // Relâché cette frame +} +``` + +**Méthodes principales** : +- `isKeyDown(key: string): boolean` - Touche pressée cette frame +- `isKeyHeld(key: string): boolean` - Touche maintenue +- `isKeyUp(key: string): boolean` - Touche relâchée cette frame +- `isMouseButtonDown(button: number): boolean` - Bouton souris pressé +- `getMousePosition(): Vector2` - Position souris +- `getMouseWorldPosition(camera: Camera): Vector2` - Position monde +- `update()` - Reset des états down/up (appelé chaque frame) + +**Exemple d'utilisation** : +``` +// Dans le script du joueur : +update(deltaTime: number) { + // Déplacement + if (input.isKeyHeld("ArrowRight")) { + this.moveRight(); + } + + // Saut (une seule fois par pression) + if (input.isKeyDown("Space")) { + this.jump(); + } + + // Tir (en maintenant) + if (input.isMouseButtonHeld(0)) { + this.shoot(); + } +} +``` + +--- + +### 7.2 AudioManager (sons et musique) + +**Rôle** : Gère tous les sons du jeu. + +**Responsabilités** : +- Charger les fichiers audio +- Jouer des sons (sound effects) +- Jouer de la musique en boucle +- Gérer le volume global et par catégorie +- Mettre en pause/reprendre +- Gérer les priorités (si trop de sons jouent) +- Créer un pool de sources audio (pour jouer le même son plusieurs fois) + +**Propriétés importantes** : +- `audioContext: AudioContext` - Context Web Audio API +- `sounds: Map` - Sons chargés +- `musicSource: AudioBufferSourceNode` - Source de la musique +- `masterVolume: number` - Volume global (0-1) +- `musicVolume: number` - Volume musique (0-1) +- `sfxVolume: number` - Volume effets sonores (0-1) +- `currentMusic: string` - Musique en cours + +**Méthodes principales** : +- `loadSound(name: string, url: string)` - Charge un son +- `playSound(name: string, volume?: number, loop?: boolean)` - Joue un son +- `playMusic(name: string, volume?: number)` - Joue une musique +- `stopMusic()` - Arrête la musique +- `pauseMusic()` - Met la musique en pause +- `resumeMusic()` - Reprend la musique +- `setMasterVolume(volume: number)` - Volume global +- `stopAllSounds()` - Arrête tous les sons + +**Catégories de sons** : +- **SFX** : Sons courts et répétitifs (saut, tir, collecte) +- **Music** : Musique de fond en boucle +- **Ambiance** : Sons d'ambiance (vent, pluie) + +**Optimisation** : Pool de sources audio pour éviter de créer trop d'instances. + +--- + +### 7.3 AssetLoader (chargement des ressources) + +**Rôle** : Charge tous les fichiers externes (images, sons, JSON...). + +**Responsabilités** : +- Télécharger les assets depuis des URLs +- Les mettre en cache +- Fournir un système de préloading avec progression +- Gérer les erreurs de chargement +- Créer les objets Texture, AudioBuffer... +- Libérer la mémoire des assets non utilisés + +**Propriétés importantes** : +- `textures: Map` - Textures chargées +- `sounds: Map` - Sons chargés +- `data: Map` - Données JSON chargées +- `loadingProgress: number` - Progression (0-1) +- `totalAssets: number` - Nombre total d'assets +- `loadedAssets: number` - Nombre d'assets chargés + +**Méthodes principales** : +- `loadTexture(name: string, url: string): Promise` - Charge une texture +- `loadSound(name: string, url: string): Promise` - Charge un son +- `loadJSON(name: string, url: string): Promise` - Charge du JSON +- `loadAll(manifest: AssetManifest): Promise` - Charge plusieurs assets +- `getTexture(name: string): Texture` - Récupère une texture +- `getSound(name: string): AudioBuffer` - Récupère un son +- `unload(name: string)` - Libère un asset + +**Manifest d'assets** : +``` +const manifest = { + textures: [ + { name: "player", url: "assets/player.png" }, + { name: "enemy", url: "assets/enemy.png" }, + { name: "tileset", url: "assets/tileset.png" } + ], + sounds: [ + { name: "jump", url: "assets/jump.mp3" }, + { name: "shoot", url: "assets/shoot.mp3" } + ], + data: [ + { name: "level1", url: "assets/level1.json" } + ] +}; + +await assetLoader.loadAll(manifest); +// Tous les assets sont prêts ! +``` + +**Événements** : +- `onProgress(progress: number)` - Progression du chargement +- `onComplete()` - Tous les assets chargés +- `onError(error: Error)` - Erreur de chargement + +--- + +### 7.4 PhysicsSystem (système de physique) + +**Rôle** : Met à jour tous les RigidBody et gère la gravité. + +**Responsabilités** : +- Appliquer la gravité globale +- Mettre à jour les vélocités +- Intégrer le mouvement (position += velocity × deltaTime) +- Gérer les contraintes (limites de vitesse) +- Optimiser avec spatial partitioning + +**Propriétés importantes** : +- `gravity: Vector2` - Gravité globale (ex: (0, 980) pixels/s²) +- `rigidBodies: RigidBody[]` - Tous les corps physiques +- `maxVelocity: number` - Vitesse max (pour éviter les tunneling) +- `iterations: number` - Nombre d'itérations (précision) + +**Méthodes principales** : +- `register(rigidBody: RigidBody)` - Enregistre un corps +- `unregister(rigidBody: RigidBody)` - Désenregistre +- `update(deltaTime: number)` - Met à jour tous les corps +- `setGravity(gravity: Vector2)` - Change la gravité + +**Fixed Timestep** : +``` +// La physique doit être mise à jour avec un timestep fixe +// pour être déterministe + +private accumulator = 0; +private fixedDeltaTime = 1/60; // 60 FPS + +update(deltaTime: number) { + this.accumulator += deltaTime; + + // Plusieurs updates si nécessaire + while (this.accumulator >= this.fixedDeltaTime) { + this.fixedUpdate(this.fixedDeltaTime); + this.accumulator -= this.fixedDeltaTime; + } +} +``` + +--- + +### 7.5 CollisionSystem (détection et résolution) + +**Rôle** : Détecte et résout toutes les collisions. + +**Responsabilités** : +- Détecter les collisions entre tous les Colliders +- Résoudre les collisions (empêcher la traversée) +- Calculer les normales de collision +- Déclencher les callbacks de collision +- Optimiser avec spatial partitioning (Quadtree) +- Gérer les layers de collision (filtres) + +**Propriétés importantes** : +- `colliders: Collider[]` - Tous les colliders +- `quadtree: Quadtree` - Structure spatiale pour optimisation +- `collisionMatrix: boolean[][]` - Quels layers collident entre eux + +**Méthodes principales** : +- `register(collider: Collider)` - Enregistre un collider +- `unregister(collider: Collider)` - Désenregistre +- `update()` - Détecte toutes les collisions +- `checkCollision(a: Collider, b: Collider): CollisionInfo | null` - Teste 2 colliders +- `resolveCollision(info: CollisionInfo)` - Résout une collision + +**Algorithmes de détection** : + +**AABB vs AABB (Box vs Box)** : +``` +function aabbVsAabb(a: BoxCollider, b: BoxCollider): boolean { + return a.left < b.right && + a.right > b.left && + a.top < b.bottom && + a.bottom > b.top; +} +``` + +**Circle vs Circle** : +``` +function circleVsCircle(a: CircleCollider, b: CircleCollider): boolean { + const distance = Vector2.distance(a.center, b.center); + return distance < (a.radius + b.radius); +} +``` + +**Résolution de collision** : +``` +// Sépare les deux objets +const overlap = calculateOverlap(collisionInfo); +const normal = collisionInfo.normal; + +// Déplace les objets pour qu'ils ne se chevauchent plus +objA.position -= normal * overlap * 0.5; +objB.position += normal * overlap * 0.5; +``` + +**Optimisation avec Quadtree** : + +Sans Quadtree : O(n²) = 1000 objets = 499,500 tests +Avec Quadtree : O(n log n) = 1000 objets = ~10,000 tests + +--- + +## 8. Mathématiques + +### 8.1 Vector2 (vecteur 2D) + +**Rôle** : Représente un point ou une direction en 2D. + +**Propriétés** : +- `x: number` +- `y: number` + +**Méthodes principales** : +- `add(other: Vector2): Vector2` - Addition +- `subtract(other: Vector2): Vector2` - Soustraction +- `multiply(scalar: number): Vector2` - Multiplication par un scalaire +- `divide(scalar: number): Vector2` - Division +- `length(): number` - Longueur du vecteur (magnitude) +- `normalize(): Vector2` - Vecteur de longueur 1 +- `dot(other: Vector2): number` - Produit scalaire +- `distance(other: Vector2): number` - Distance entre deux points +- `lerp(other: Vector2, t: number): Vector2` - Interpolation linéaire +- `rotate(angle: number): Vector2` - Rotation +- `angle(): number` - Angle du vecteur + +**Méthodes statiques** : +- `Vector2.zero` - (0, 0) +- `Vector2.one` - (1, 1) +- `Vector2.up` - (0, -1) +- `Vector2.down` - (0, 1) +- `Vector2.left` - (-1, 0) +- `Vector2.right` - (1, 0) + +**Exemples d'utilisation** : +``` +// Déplacement +position = position.add(velocity.multiply(deltaTime)); + +// Direction vers un point +const direction = target.subtract(position).normalize(); + +// Interpolation (smooth movement) +position = position.lerp(target, 0.1); + +8.2 Matrix3 (matrice 3×3) +Rôle : Représente une transformation 2D (translation, rotation, scale). +Structure : +RéessayerSContinuer[a, c, tx] +[b, d, ty] +[0, 0, 1 ] + +a, b, c, d : rotation et scale +tx, ty : translation +``` + +**Responsabilités** : +- Combiner plusieurs transformations en une seule +- Appliquer une transformation à un point +- Calculer l'inverse d'une matrice +- Créer des matrices de projection (orthographique) + +**Méthodes principales** : +- `identity(): Matrix3` - Matrice identité +- `translate(x: number, y: number): Matrix3` - Translation +- `rotate(angle: number): Matrix3` - Rotation +- `scale(x: number, y: number): Matrix3` - Scale +- `multiply(other: Matrix3): Matrix3` - Multiplication de matrices +- `transformPoint(point: Vector2): Vector2` - Applique la transformation +- `inverse(): Matrix3` - Matrice inverse +- `orthographic(left, right, bottom, top): Matrix3` - Projection orthographique + +**Méthodes statiques** : +- `Matrix3.identity()` - Matrice identité +- `Matrix3.translation(x, y)` - Matrice de translation +- `Matrix3.rotation(angle)` - Matrice de rotation +- `Matrix3.scaling(x, y)` - Matrice de scale + +**Composition de transformations** : +``` +// Pour transformer un objet avec translation, rotation, scale : +const matrix = Matrix3.identity() + .translate(position.x, position.y) + .rotate(rotation) + .scale(scale.x, scale.y); + +// Applique la transformation à un point +const transformedPoint = matrix.transformPoint(localPoint); +``` + +**Important** : L'ordre des opérations compte ! +``` +translate → rotate → scale ≠ rotate → translate → scale +``` + +**Projection orthographique** : +``` +// Convertit coordonnées pixel (0, 0) → (800, 600) +// En coordonnées NDC (-1, -1) → (1, 1) +const projection = Matrix3.orthographic(0, 800, 600, 0); +``` + +--- + +### 8.3 Rect (rectangle) + +**Rôle** : Représente un rectangle (pour bounds, collisions, UVs...). + +**Propriétés** : +- `x: number` - Position X +- `y: number` - Position Y +- `width: number` - Largeur +- `height: number` - Hauteur + +**Propriétés calculées** : +- `left: number` - Bord gauche (x) +- `right: number` - Bord droit (x + width) +- `top: number` - Bord haut (y) +- `bottom: number` - Bord bas (y + height) +- `center: Vector2` - Centre du rectangle +- `size: Vector2` - (width, height) + +**Méthodes principales** : +- `contains(point: Vector2): boolean` - Teste si un point est dedans +- `overlaps(other: Rect): boolean` - Teste si deux rectangles se chevauchent +- `intersection(other: Rect): Rect | null` - Calcule l'intersection +- `union(other: Rect): Rect` - Calcule l'union (plus petit rectangle contenant les deux) +- `expand(amount: number): Rect` - Agrandi le rectangle + +**Utilisation** : +``` +// Bounds d'un sprite +const bounds = new Rect( + transform.position.x - sprite.width / 2, + transform.position.y - sprite.height / 2, + sprite.width, + sprite.height +); + +// Test de collision simple +if (playerBounds.overlaps(enemyBounds)) { + // Collision ! +} + +// Source rect pour spritesheet (UV) +const sourceRect = new Rect(0, 0, 32, 32); // Sprite 32×32 en haut à gauche +``` + +--- + +### 8.4 Color (couleur RGBA) + +**Rôle** : Représente une couleur avec alpha (transparence). + +**Propriétés** : +- `r: number` - Rouge (0-1) +- `g: number` - Vert (0-1) +- `b: number` - Bleu (0-1) +- `a: number` - Alpha (0-1, 0 = transparent, 1 = opaque) + +**Méthodes principales** : +- `lerp(other: Color, t: number): Color` - Interpolation de couleurs +- `multiply(other: Color): Color` - Multiplication de couleurs +- `toHex(): string` - Convertit en hex (#RRGGBBAA) +- `toRGBA(): string` - Convertit en rgba(r, g, b, a) + +**Couleurs prédéfinies** : +- `Color.white` - (1, 1, 1, 1) +- `Color.black` - (0, 0, 0, 1) +- `Color.red` - (1, 0, 0, 1) +- `Color.green` - (0, 1, 0, 1) +- `Color.blue` - (0, 0, 1, 0) +- `Color.transparent` - (0, 0, 0, 0) + +**Utilisation** : +``` +// Tint rouge sur un sprite +spriteRenderer.tint = new Color(1, 0, 0, 1); + +// Fade in progressif +spriteRenderer.tint.a = 0; +// Chaque frame : +spriteRenderer.tint.a += deltaTime; // Fade in sur 1 seconde + +// Interpolation de couleur (ex: dégâts) +const damageColor = Color.red; +const normalColor = Color.white; +spriteRenderer.tint = normalColor.lerp(damageColor, damageFlash); +``` + +--- + +## 9. Optimisations avancées + +### 9.1 ObjectPool (réutilisation d'objets) + +**Rôle** : Évite de créer/détruire des objets en les réutilisant. + +**Problème** : +``` +// MAUVAIS : Crée un objet à chaque tir +function shoot() { + const bullet = new Bullet(); // Allocation mémoire + bullets.push(bullet); +} + +// Destruction +bullet.destroy(); // Garbage collection = pause du jeu ! +``` + +**Solution avec pool** : +``` +// BON : Réutilise les objets +function shoot() { + const bullet = bulletPool.get(); // Récupère un objet existant + bullet.reset(); // Réinitialise + bullet.active = true; +} + +// "Destruction" +bullet.active = false; +bulletPool.release(bullet); // Remet dans le pool +``` + +**Responsabilités** : +- Créer une réserve d'objets à l'avance +- Fournir des objets disponibles +- Reprendre les objets libérés +- Agrandir le pool si nécessaire + +**Propriétés importantes** : +- `pool: T[]` - Objets disponibles +- `factory: () => T` - Fonction pour créer de nouveaux objets +- `initialSize: number` - Taille initiale du pool +- `maxSize: number` - Taille max (optionnel) + +**Méthodes principales** : +- `get(): T` - Récupère un objet du pool +- `release(obj: T)` - Remet un objet dans le pool +- `clear()` - Vide le pool +- `prewarm(count: number)` - Pré-crée des objets + +**Gains de performance** : +- Jusqu'à 10× plus rapide pour les jeux avec beaucoup d'objets +- Élimine les pauses du garbage collector +- Réduit la fragmentation mémoire + +**Quand l'utiliser** : +- Projectiles (bullets, flèches...) +- Particules +- Ennemis (spawn/despawn fréquent) +- Effets visuels temporaires + +--- + +### 9.2 Quadtree (spatial partitioning) + +**Rôle** : Structure de données qui divise l'espace en zones pour optimiser les recherches. + +**Problème** : +``` +// Test de collision naïf : O(n²) +for (let i = 0; i < objects.length; i++) { + for (let j = i + 1; j < objects.length; j++) { + if (checkCollision(objects[i], objects[j])) { + // Collision + } + } +} +// 1000 objets = 499,500 tests ! +``` + +**Solution avec Quadtree** : +``` +// O(n log n) +quadtree.clear(); +for (const obj of objects) { + quadtree.insert(obj); +} + +for (const obj of objects) { + const nearby = quadtree.query(obj.bounds); + for (const other of nearby) { + if (checkCollision(obj, other)) { + // Collision + } + } +} +// 1000 objets = ~10,000 tests +``` + +**Structure** : +``` +Quadtree Node +├── bounds: Rect (zone couverte) +├── capacity: number (objets max avant subdivision) +├── objects: Object[] (objets dans ce nœud) +└── subdivisions: [NW, NE, SW, SE] (4 enfants) + +Division de l'espace : +┌─────────┬─────────┐ +│ NW │ NE │ +│ │ │ +├─────────┼─────────┤ +│ SW │ SE │ +│ │ │ +└─────────┴─────────┘ +``` + +**Responsabilités** : +- Insérer des objets +- Subdiviser quand un nœud est plein +- Requêter les objets dans une zone +- Se vider et se reconstruire chaque frame (pour objets dynamiques) + +**Propriétés importantes** : +- `bounds: Rect` - Zone couverte par ce nœud +- `capacity: number` - Nombre max d'objets avant subdivision +- `maxDepth: number` - Profondeur max de l'arbre +- `objects: Object[]` - Objets dans ce nœud +- `divided: boolean` - Si subdivisé ou non +- `children: QuadtreeNode[]` - 4 enfants (NW, NE, SW, SE) + +**Méthodes principales** : +- `insert(obj: Object): boolean` - Insère un objet +- `subdivide()` - Divise le nœud en 4 +- `query(range: Rect): Object[]` - Récupère tous les objets dans une zone +- `clear()` - Vide l'arbre + +**Algorithme d'insertion** : +``` +insert(obj): + 1. Si l'objet n'est pas dans les bounds → return false + 2. Si le nœud n'est pas plein et pas subdivisé → ajouter l'objet + 3. Si le nœud est plein → subdiviser + 4. Insérer l'objet dans les enfants appropriés +``` + +**Gains** : +- Collision : O(n²) → O(n log n) +- Recherche de voisins : O(n) → O(log n) +- Culling (objets hors caméra) : très rapide + +--- + +### 9.3 Spatial Hashing (alternative au Quadtree) + +**Rôle** : Division de l'espace en grille uniforme. + +**Différence avec Quadtree** : +- **Quadtree** : Adaptatif (plus de détails où il y a plus d'objets) +- **Spatial Hash** : Grille fixe (plus simple, parfois plus rapide) + +**Responsabilités** : +- Diviser l'espace en cellules de taille fixe +- Hacher la position d'un objet pour trouver sa cellule +- Récupérer rapidement tous les objets d'une cellule + +**Structure** : +``` +Grid de cellules : +┌───┬───┬───┬───┐ +│ 0 │ 1 │ 2 │ 3 │ +├───┼───┼───┼───┤ +│ 4 │ 5 │ 6 │ 7 │ +├───┼───┼───┼───┤ +│ 8 │ 9 │10 │11 │ +└───┴───┴───┴───┘ + +Chaque cellule contient une liste d'objets +``` + +**Propriétés importantes** : +- `cellSize: number` - Taille d'une cellule +- `cells: Map` - Grille (hash → objets) + +**Méthodes principales** : +- `insert(obj: Object)` - Insère un objet +- `remove(obj: Object)` - Retire un objet +- `query(bounds: Rect): Object[]` - Récupère objets dans une zone +- `hash(x: number, y: number): number` - Calcule l'index de cellule +- `clear()` - Vide la grille + +**Fonction de hachage** : +``` +hash(x: number, y: number): number { + const cellX = Math.floor(x / this.cellSize); + const cellY = Math.floor(y / this.cellSize); + return cellX + cellY * 10000; // Combinaison unique +} +``` + +**Quand utiliser Spatial Hash vs Quadtree** : +- **Spatial Hash** : Objets uniformément répartis, taille similaire +- **Quadtree** : Objets concentrés en zones, tailles variées + +--- + +### 9.4 Dirty Flags (recalcul conditionnel) + +**Rôle** : Ne recalculer que ce qui a changé. + +**Principe** : +``` +class Transform { + private _position = new Vector2(0, 0); + private _dirty = false; + private _cachedWorldMatrix: Matrix3; + + set position(value: Vector2) { + this._position = value; + this._dirty = true; // Marque "besoin de recalculer" + this.propagateDirtyToChildren(); // Propage aux enfants + } + + getWorldMatrix(): Matrix3 { + if (this._dirty) { + // Recalcule seulement si nécessaire + this._cachedWorldMatrix = this.calculateWorldMatrix(); + this._dirty = false; + } + return this._cachedWorldMatrix; + } +} +``` + +**Où utiliser** : +- **Transform** : Matrice de transformation +- **Bounds** : Rectangle englobant +- **Mesh** : Données géométriques +- **Render** : Batch de sprites + +**Gains** : +- Économise 80-90% des calculs pour objets statiques +- Réduit la charge CPU significativement + +--- + +### 9.5 Canvas Layering (multi-canvas) + +**Rôle** : Séparer les éléments statiques et dynamiques sur plusieurs canvas. + +**Principe** : +``` + + + +``` + +**Avantages** : +- Arrière-plan statique : dessiné 1 fois au lieu de 60 fois/seconde +- UI : dessinée seulement quand elle change +- Performance : jusqu'à 3× plus rapide + +**Inconvénients** : +- Plus complexe à gérer +- Moins utile avec WebGL (qui est déjà très rapide) + +**Quand utiliser** : +- Jeux avec beaucoup de décors statiques +- UI complexe qui change rarement +- Performance critique sur appareils faibles + +--- + +### 9.6 Frustum Culling (ne rendre que le visible) + +**Rôle** : Ne dessiner que les objets visibles par la caméra. + +**Principe** : +``` +render(scene: Scene, camera: Camera) { + const frustum = camera.getViewFrustum(); // Zone visible + + for (const obj of scene.objects) { + // Test si l'objet est visible + if (!frustum.contains(obj.bounds)) { + continue; // Skip les objets hors écran + } + + // Rend seulement les objets visibles + obj.render(spriteBatch); + } +} +``` + +**Gains** : +- Monde 10000×10000 pixels, caméra 800×600 +- Sans culling : dessine 1000 objets +- Avec culling : dessine ~50 objets +- 20× plus rapide ! + +**Méthodes** : +- **AABB Test** : Teste si le bounds de l'objet intersecte le frustum +- **Sphere Test** : Teste avec un cercle englobant (plus rapide, moins précis) + +--- + +### 9.7 Level of Detail (LOD) + +**Rôle** : Utiliser des versions simplifiées pour les objets lointains. + +**Principe** : +``` +const distanceToCamera = Vector2.distance(obj.position, camera.position); + +if (distanceToCamera < 200) { + obj.render(highDetailSprite); // Proche : haute qualité +} else if (distanceToCamera < 500) { + obj.render(mediumDetailSprite); // Moyen : qualité moyenne +} else { + obj.render(lowDetailSprite); // Loin : basse qualité +} +``` + +**Applications 2D** : +- Animations : moins de frames pour objets lointains +- Particules : moins de particules pour effets lointains +- Détails : masquer petits détails sur objets lointains + +--- + +## 10. Structure de fichiers complète +``` +project-root/ +│ +├── src/ +│ │ +│ ├── core/ +│ │ ├── Engine.ts // Point d'entrée, orchestrateur +│ │ ├── GameLoop.ts // Boucle principale +│ │ ├── Scene.ts // Scène de jeu +│ │ ├── SceneManager.ts // Gestion des scènes +│ │ └── Time.ts // Gestion du temps +│ │ +│ ├── rendering/ +│ │ ├── WebGLRenderer.ts // Coordonnateur du rendu +│ │ ├── SpriteBatch.ts // Batch rendering (★ crucial) +│ │ ├── Shader.ts // Wrapper de shader +│ │ ├── Material.ts // Shader + uniforms +│ │ ├── Texture.ts // Gestion des textures +│ │ ├── Camera.ts // Caméra 2D +│ │ ├── RenderQueue.ts // File de rendu triée +│ │ └── Layer.ts // Système de layers +│ │ +│ ├── webgl/ +│ │ ├── GLContext.ts // Abstraction WebGL +│ │ ├── Buffer.ts // Vertex/Index buffers +│ │ ├── VertexArray.ts // VAO +│ │ ├── Framebuffer.ts // Render to texture +│ │ └── GLUtils.ts // Utilitaires WebGL +│ │ +│ ├── entities/ +│ │ ├── GameObject.ts // Entité de base +│ │ ├── Transform.ts // Position, rotation, scale +│ │ └── Component.ts // Classe de base des composants +│ │ +│ ├── components/ +│ │ ├── SpriteRenderer.ts // Affiche un sprite +│ │ ├── Animator.ts // Gère les animations +│ │ ├── RigidBody.ts // Physique +│ │ ├── BoxCollider.ts // Collision rectangle +│ │ ├── CircleCollider.ts // Collision cercle +│ │ ├── PolygonCollider.ts // Collision polygone +│ │ ├── ParticleEmitter.ts // Système de particules +│ │ ├── TilemapRenderer.ts // Affichage tilemap +│ │ ├── AudioSource.ts // Source audio +│ │ └── Camera2D.ts // Component caméra +│ │ +│ ├── systems/ +│ │ ├── PhysicsSystem.ts // Mise à jour physique +│ │ ├── CollisionSystem.ts // Détection/résolution +│ │ ├── AnimationSystem.ts // Mise à jour animations +│ │ └── ParticleSystem.ts // Mise à jour particules +│ │ +│ ├── input/ +│ │ ├── InputManager.ts // Gestion entrées +│ │ ├── Keyboard.ts // Clavier +│ │ ├── Mouse.ts // Souris +│ │ └── Touch.ts // Tactile +│ │ +│ ├── audio/ +│ │ ├── AudioManager.ts // Gestion audio +│ │ ├── AudioSource.ts // Source audio +│ │ └── AudioListener.ts // Écouteur audio +│ │ +│ ├── assets/ +│ │ ├── AssetLoader.ts // Chargement ressources +│ │ ├── TextureAtlas.ts // Atlas de textures +│ │ └── SpriteSheet.ts // Spritesheet +│ │ +│ ├── math/ +│ │ ├── Vector2.ts // Vecteur 2D +│ │ ├── Vector3.ts // Vecteur 3D (pour couleurs RGB) +│ │ ├── Matrix3.ts // Matrice 3×3 +│ │ ├── Rect.ts // Rectangle +│ │ ├── Circle.ts // Cercle +│ │ ├── Color.ts // Couleur RGBA +│ │ ├── MathUtils.ts // Utilitaires mathématiques +│ │ └── Easing.ts // Fonctions d'easing +│ │ +│ ├── utils/ +│ │ ├── ObjectPool.ts // Pool d'objets +│ │ ├── EventBus.ts // Système d'événements +│ │ ├── Quadtree.ts // Spatial partitioning +│ │ ├── SpatialHash.ts // Grille spatiale +│ │ ├── Debug.ts // Outils de debug +│ │ └── Logger.ts // Logging +│ │ +│ ├── physics/ +│ │ ├── Physics.ts // Constantes physiques +│ │ ├── Collision.ts // Détection de collision +│ │ ├── CollisionInfo.ts // Info de collision +│ │ └── Manifold.ts // Manifold de collision +│ │ +│ ├── animation/ +│ │ ├── Animation.ts // Données d'animation +│ │ ├── AnimationClip.ts // Clip d'animation +│ │ └── Keyframe.ts // Keyframe +│ │ +│ ├── shaders/ +│ │ ├── sprite.vert.glsl // Vertex shader sprite +│ │ ├── sprite.frag.glsl // Fragment shader sprite +│ │ ├── particle.vert.glsl // Vertex shader particule +│ │ ├── particle.frag.glsl // Fragment shader particule +│ │ └── postprocess.frag.glsl // Post-processing +│ │ +│ └── index.ts // Point d'entrée TypeScript +│ +├── assets/ +│ ├── images/ // Images du jeu +│ ├── sounds/ // Sons et musique +│ ├── data/ // JSON, XML, etc. +│ └── fonts/ // Polices (si besoin) +│ +├── examples/ // Exemples d'utilisation +│ ├── basic-sprite/ +│ ├── platformer/ +│ └── particle-effects/ +│ +├── dist/ // Build de production +│ +├── tsconfig.json // Configuration TypeScript +├── package.json // Dépendances npm +└── README.md // Documentation +``` + +--- + +## 11. Flow d'exécution complet + +### 11.1 Démarrage du moteur +``` +1. index.html charge le script + ↓ +2. Création de l'Engine + const engine = new Engine(800, 600); + ↓ +3. Engine.initialize() + - Crée le canvas WebGL + - Initialise WebGLRenderer + - Initialise InputManager + - Initialise AudioManager + - Initialise AssetLoader + - Crée SceneManager + - Crée GameLoop + ↓ +4. Chargement des assets + await assetLoader.loadAll(manifest); + ↓ +5. Création de la première scène + const mainScene = new MainScene(); + sceneManager.registerScene("main", mainScene); + ↓ +6. Chargement de la scène + sceneManager.loadScene("main"); + ↓ +7. Démarrage de la GameLoop + engine.start(); + ↓ +8. La boucle tourne ! +``` + +--- + +### 11.2 Une frame complète (60 FPS) +``` +requestAnimationFrame(timestamp) + ↓ +1. Calcul du deltaTime + deltaTime = (timestamp - lastTimestamp) / 1000 + Time.deltaTime = deltaTime + ↓ +2. Input Update + inputManager.update() + - Reset des états "down" et "up" + ↓ +3. Fixed Update (physique) + accumulator += deltaTime + while (accumulator >= fixedDeltaTime) { + physicsSystem.fixedUpdate(fixedDeltaTime) + - Applique gravité + - Intègre vélocités + accumulator -= fixedDeltaTime + } + ↓ +4. Update (logique) + scene.update(deltaTime) + - Pour chaque GameObject actif: + - Pour chaque Component actif: + - component.update(deltaTime) + ↓ +5. Collision Detection + collisionSystem.update() + - Reconstruit le Quadtree + - Détecte les collisions + - Résout les collisions + - Déclenche les callbacks + ↓ +6. Animation Update + animationSystem.update(deltaTime) + - Avance les animations + - Change les sprites + ↓ +7. Camera Update + camera.follow(player) // Si suivi activé + camera.update(deltaTime) + ↓ +8. Render + renderer.render(scene, camera) + + 8.1. Clear + gl.clear(COLOR_BUFFER_BIT) + + 8.2. Collecte des renderables + renderables = scene.getAllRenderables() + + 8.3. Frustum culling + visibleObjects = cullObjects(renderables, camera.frustum) + + 8.4. Soumission à la RenderQueue + for (obj of visibleObjects) { + renderQueue.submit(obj) + } + + 8.5. Tri de la RenderQueue + renderQueue.sort() + - Par layer + - Par shader + - Par texture + + 8.6. Begin SpriteBatch + spriteBatch.begin(camera.projection, camera.view) + + 8.7. Rendu + currentShader = null + currentTexture = null + + for (renderable of renderQueue) { + // Change shader si nécessaire + if (renderable.shader !== currentShader) { + spriteBatch.flush() + renderable.shader.use() + currentShader = renderable.shader + } + + // Ajoute au batch + renderable.render(spriteBatch) + } + + 8.8. End SpriteBatch + spriteBatch.end() // Flush final + + 8.9. Post-processing (optionnel) + postProcessing.render(frameTexture) + + 8.10. Debug Draw (si activé) + debugRenderer.drawColliders() + debugRenderer.drawFPS() + ↓ +9. Audio Update + audioManager.update(deltaTime) + - Update 3D audio positions + ↓ +10. Cleanup + scene.cleanupDestroyedObjects() + renderQueue.clear() + ↓ +11. Prochaine frame + requestAnimationFrame(nextFrame) +``` + +--- + +### 11.3 Ajout d'un GameObject à la scène +``` +const player = new GameObject("Player"); + ↓ +1. Ajout de composants + const transform = player.transform; // Existe déjà + transform.position = new Vector2(400, 300); + + const sprite = player.addComponent(new SpriteRenderer()); + sprite.texture = assetLoader.getTexture("player"); + + const rigidBody = player.addComponent(new RigidBody()); + rigidBody.mass = 1; + + const collider = player.addComponent(new BoxCollider()); + collider.size = new Vector2(32, 48); + ↓ +2. Ajout à la scène + scene.addGameObject(player); + ↓ +3. Cycle de vie + player.awake() + - Appelle awake() sur tous les composants + ↓ +4. Premier update + player.start() + - Appelle start() sur tous les composants + ↓ +5. Enregistrement aux systèmes + - physicsSystem.register(rigidBody) + - collisionSystem.register(collider) + - renderQueue submit automatique chaque frame + ↓ +6. Updates réguliers + Chaque frame: + - rigidBody.update(deltaTime) + - sprite.render(spriteBatch) + +11.4 Gestion d'une collision +1. CollisionSystem détecte un overlap + collider1.overlaps(collider2) = true + ↓ +2. Calcul de l'info de collision + info = calculateCollisionInfo(collider1, collider2) + - normal : direction de séparation + - penetration : profondeur de pénétration + - contactPoint : point de contact + ↓ +3. Résolution physique (si solid) + if (!collider1.isTrigger && !collider2.isTrigger) { + resolveCollision(info) + - Sépare les objets + - Applique impulse (rebond) + } + ↓ +4. Callbacks + collider1.onCollisionEnter(collider2) + collider2.onCollisionEnter(collider1) + + Ou si trigger: + collider1.onTriggerEnter(collider2) + ↓ +5. Dans le script du joueur + onCollisionEnter(other: Collider) { + if (other.gameObject.tag === "Enemy") { + this.takeDamage(10); + }RéessayerSContinuer if (other.gameObject.tag === "Coin") { + this.collectCoin(); + other.gameObject.destroy(); + } + } +``` + +--- + +### 11.5 Changement de scène +``` +1. Appel du changement + sceneManager.loadScene("Level2"); + ↓ +2. Déchargement de la scène actuelle + if (currentScene) { + currentScene.onUnload() + - Appelle onDestroy() sur tous les GameObjects + - Désenregistre tous les systèmes + - Libère les ressources temporaires + } + ↓ +3. Chargement de la nouvelle scène + const newScene = scenes.get("Level2"); + ↓ +4. Initialisation de la scène + newScene.onLoad() + - Crée les GameObjects + - Configure la caméra + - Charge les assets spécifiques + ↓ +5. Activation + currentScene = newScene; + ↓ +6. Premier update de la nouvelle scène + - Tous les GameObjects awake() + - Tous les GameObjects start() + ↓ +7. La scène est active + Updates et rendu normaux +``` + +--- + +## 12. Extensions possibles + +### 12.1 Post-Processing + +**Rôle** : Appliquer des effets visuels sur l'image finale. + +**Exemples d'effets** : +- **Bloom** : Glow lumineux +- **Blur** : Flou +- **Vignette** : Assombrissement des bords +- **Color Grading** : Correction de couleur +- **Chromatic Aberration** : Distorsion des couleurs +- **CRT Effect** : Effet écran cathodique +- **Pixelate** : Pixelisation + +**Architecture** : +``` +PostProcessingStack +├── effects: PostEffect[] +└── framebuffers: Framebuffer[] + +PostEffect (classe de base) +├── shader: Shader +└── render(source: Texture, target: Framebuffer) +``` + +**Pipeline** : +``` +1. Rend la scène dans un Framebuffer (pas l'écran) + ↓ +2. Pour chaque effet : + - Applique l'effet + - Source : texture précédente + - Target : nouveau framebuffer + ↓ +3. Rend le résultat final à l'écran +``` + +**Exemple - Effet Bloom** : +``` +1. Rend la scène +2. Extract bright pixels (threshold) +3. Blur horizontal +4. Blur vertical +5. Combine avec l'image originale +``` + +--- + +### 12.2 Lighting 2D + +**Rôle** : Système d'éclairage dynamique 2D. + +**Types de lumières** : +- **Point Light** : Lumière omnidirectionnelle (torche, lampe) +- **Spot Light** : Lumière conique (lampe de poche) +- **Directional Light** : Lumière directionnelle (soleil) +- **Ambient Light** : Lumière ambiante globale + +**Architecture** : +``` +LightingSystem +├── lights: Light[] +├── lightBuffer: Framebuffer (texture des lumières) +└── normalMap: Texture (pour normal mapping) + +Light (classe de base) +├── position: Vector2 +├── color: Color +├── intensity: number +└── radius: number +``` + +**Pipeline** : +``` +1. Rend la scène normalement + ↓ +2. Rend les lumières dans un framebuffer séparé + - Clear en noir (pas de lumière) + - Additive blending + - Chaque lumière dessine son influence + ↓ +3. Combine scène + lumières + - Multiplie les couleurs + - fragment = sceneColor * lightColor +Fragment Shader pour lumière : +glsl// Calcul de l'atténuation +float distance = length(lightPosition - fragPosition); +float attenuation = 1.0 / (1.0 + distance * distance); + +// Couleur finale +vec3 lightContribution = lightColor * intensity * attenuation; +gl_FragColor = vec4(lightContribution, 1.0); + +12.3 Normal Mapping +Rôle : Ajouter des détails d'éclairage sans géométrie supplémentaire. +Principe : + +Une normal map stocke des normales de surface dans une texture +Permet de simuler du relief +La lumière réagit comme si la surface avait du relief + +Texture normale : + +R = Normal X (-1 à 1 encodé en 0-1) +G = Normal Y +B = Normal Z (toujours positif en 2D) + +Fragment Shader : +glsl// Échantillonne la normal map +vec3 normal = texture2D(normalMap, vUV).rgb; +normal = normalize(normal * 2.0 - 1.0); // Décode 0-1 → -1 à 1 + +// Calcul de la lumière +vec3 lightDir = normalize(lightPosition - fragPosition); +float diffuse = max(dot(normal, lightDir), 0.0); + +// Couleur finale +vec3 color = baseColor * diffuse * lightColor; +``` + +--- + +### 12.4 Particle System GPU + +**Rôle** : Système de particules calculé entièrement sur le GPU. + +**Avantages** : +- 100 000+ particules sans problème +- Pas de transfert CPU → GPU chaque frame +- Mise à jour parallèle massive + +**Architecture** : +``` +GPUParticleSystem +├── particleBuffer: Buffer (positions, vélocités, etc.) +├── computeShader: Shader (calcul sur GPU, WebGL 2) +└── renderShader: Shader (rendu instanced) +``` + +**WebGL 2 - Transform Feedback** : +``` +// Les particules se mettent à jour sur le GPU +1. Vertex shader lit les données de particule +2. Applique la physique (gravité, forces) +3. Transform feedback écrit les nouvelles données +4. Pas besoin de les renvoyer au CPU ! +``` + +**Rendu instanced** : +``` +// Dessine toutes les particules en 1 draw call +gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, particleCount); +// 6 vertices pour un quad, répété particleCount fois +``` + +--- + +### 12.5 Sprite Batching avancé + +**Optimisations supplémentaires** : + +#### A. Multi-Texture Batching +``` +// Au lieu de flusher à chaque changement de texture, +// utiliser un texture array et passer l'index + +Fragment Shader: +uniform sampler2D textures[8]; // 8 textures en même temps +varying float textureIndex; + +void main() { + // Sélectionne la bonne texture + vec4 color; + if (textureIndex < 0.5) color = texture2D(textures[0], vUV); + else if (textureIndex < 1.5) color = texture2D(textures[1], vUV); + // etc... +} +``` + +#### B. Texture Atlas +``` +// Combine plusieurs textures en une seule grande texture +// Avantage : 1 seule texture pour tout le jeu ! + +Atlas 2048×2048: +┌─────┬─────┬─────┬─────┐ +│ Hero│Enemy│Coin │ ... │ +├─────┼─────┼─────┼─────┤ +│Tile1│Tile2│ ... │ ... │ +└─────┴─────┴─────┴─────┘ + +// Les UVs sont ajustés pour pointer vers la bonne région +``` + +#### C. Instanced Rendering +``` +// Dessine beaucoup d'objets identiques très rapidement +gl.drawElementsInstanced( + gl.TRIANGLES, + 6, // 6 indices par sprite + gl.UNSIGNED_SHORT, + 0, + spriteCount // Nombre d'instances +); + +// Vertex shader reçoit un attribut "instanceID" +// qui permet de différencier chaque instance +``` + +--- + +### 12.6 Tilemap Chunking + +**Rôle** : Optimiser les très grandes tilemaps. + +**Problème** : +``` +Tilemap 1000×1000 = 1 million de tiles +Même avec culling, trop de draw calls +``` + +**Solution - Chunking** : +``` +Diviser la tilemap en chunks 16×16 + +Chunk: +┌────────────────┐ +│ 16×16 tiles │ +│ = 1 draw call │ +└────────────────┘ + +1000×1000 tiles = 62×62 chunks +Seuls les chunks visibles sont rendus (~9 chunks) +``` + +**Architecture** : +``` +TilemapChunk +├── tiles: number[] (16×16 = 256 tiles) +├── vertexBuffer: Buffer (pré-calculé) +├── dirty: boolean +└── bounds: Rect + +Tilemap +├── chunks: TilemapChunk[][] +└── chunkSize: number +``` + +**Optimisation** : +``` +// Les chunks sont pré-bakés en buffers GPU +// Quand un tile change : +chunk.dirty = true; + +// Au rendu, si dirty : +chunk.rebuildMesh(); +chunk.uploadToGPU(); +chunk.dirty = false; +``` + +--- + +### 12.7 Tweening / Animation System + +**Rôle** : Animer des propriétés facilement (position, scale, couleur...). + +**Exemples** : +``` +// Déplace l'objet vers (100, 100) en 2 secondes +Tween.to(obj.transform.position, { x: 100, y: 100 }, 2.0) + .easing(Easing.easeOutQuad) + .onComplete(() => console.log("Arrived!")); + +// Scale pulse +Tween.to(obj.transform.scale, { x: 1.5, y: 1.5 }, 0.5) + .yoyo(true) + .repeat(Infinity); + +// Fade out +Tween.to(sprite.tint, { a: 0 }, 1.0); +``` + +**Architecture** : +``` +TweenManager +├── tweens: Tween[] +└── update(deltaTime) + +Tween +├── target: any (objet à animer) +├── properties: string[] (propriétés à animer) +├── from: number[] (valeurs de départ) +├── to: number[] (valeurs d'arrivée) +├── duration: number +├── elapsed: number +├── easing: EasingFunction +├── yoyo: boolean +├── repeat: number +└── callbacks: { onUpdate, onComplete } +``` + +**Fonctions d'easing** : +``` +Linear, EaseInQuad, EaseOutQuad, EaseInOutQuad, +EaseInCubic, EaseOutCubic, EaseInOutCubic, +EaseInElastic, EaseOutElastic, EaseInBounce, EaseOutBounce +``` + +--- + +### 12.8 State Machine + +**Rôle** : Gérer les états d'un personnage/ennemi (idle, walk, jump, attack...). + +**Architecture** : +``` +StateMachine +├── currentState: State +├── states: Map +└── transition(stateName: string) + +State (classe de base) +├── onEnter() +├── onExit() +├── update(deltaTime) +└── handleInput(input) +``` + +**Exemple - État du joueur** : +``` +class IdleState extends State { + update(deltaTime) { + if (input.isKeyDown("Space")) { + stateMachine.transition("jump"); + } + if (input.isKeyHeld("ArrowRight")) { + stateMachine.transition("walk"); + } + } +} + +class WalkState extends State { + update(deltaTime) { + player.moveRight(); + + if (!input.isKeyHeld("ArrowRight")) { + stateMachine.transition("idle"); + } + if (input.isKeyDown("Space")) { + stateMachine.transition("jump"); + } + } +} + +class JumpState extends State { + onEnter() { + player.rigidBody.addImpulse(new Vector2(0, -500)); + } + + update(deltaTime) { + if (player.isGrounded()) { + stateMachine.transition("idle"); + } + } +} +``` + +--- + +### 12.9 Pathfinding (A*) + +**Rôle** : Trouver le chemin le plus court entre deux points. + +**Algorithme A*** : +``` +1. Open list : nœuds à explorer +2. Closed list : nœuds déjà explorés +3. Pour chaque nœud : + - f = g + h + - g : coût depuis le départ + - h : heuristique vers l'arrivée +4. Explore le nœud avec le plus petit f +5. Reconstruit le chemin à la fin +``` + +**Architecture** : +``` +Pathfinder +├── grid: Node[][] (grille de nœuds) +├── findPath(start: Vector2, goal: Vector2): Vector2[] +└── heuristic(a: Node, b: Node): number + +Node +├── position: Vector2 +├── walkable: boolean +├── g: number (cost from start) +├── h: number (heuristic to goal) +├── f: number (g + h) +└── parent: Node +``` + +**Utilisation** : +``` +// Trouve le chemin +const path = pathfinder.findPath( + enemy.position, + player.position +); + +// Suit le chemin +if (path.length > 0) { + const nextPoint = path[0]; + enemy.moveTowards(nextPoint); + + if (Vector2.distance(enemy.position, nextPoint) < 5) { + path.shift(); // Passe au point suivant + } +} +``` + +--- + +### 12.10 Save System + +**Rôle** : Sauvegarder et charger la progression du joueur. + +**Architecture** : +``` +SaveSystem +├── save(key: string, data: any) +├── load(key: string): any +├── delete(key: string) +└── exists(key: string): boolean +``` + +**Implémentation avec localStorage** : +``` +class SaveSystem { + save(key: string, data: any): void { + const json = JSON.stringify(data); + localStorage.setItem(key, json); + } + + load(key: string): any { + const json = localStorage.getItem(key); + return json ? JSON.parse(json) : null; + } +} + +// Utilisation +const saveData = { + level: 5, + health: 80, + coins: 150, + position: { x: 400, y: 300 }, + inventory: ["sword", "shield", "potion"] +}; + +saveSystem.save("player_progress", saveData); + +// Chargement +const loaded = saveSystem.load("player_progress"); +if (loaded) { + player.level = loaded.level; + player.health = loaded.health; + player.position = new Vector2(loaded.position.x, loaded.position.y); +} +``` + +--- + +### 12.11 UI System + +**Rôle** : Système d'interface utilisateur (menus, HUD, dialogues...). + +**Architecture** : +``` +UIManager +├── canvas: UICanvas +├── elements: UIElement[] +└── render() + +UIElement (classe de base) +├── position: Vector2 +├── size: Vector2 +├── visible: boolean +├── render(context) +└── handleInput(input) + +Types d'éléments : +├── UIButton +├── UIText +├── UIImage +├── UIPanel +├── UISlider +├── UIProgressBar +└── UIDialog +``` + +**Système d'événements UI** : +``` +button.onClick = () => { + console.log("Button clicked!"); +}; + +button.onHover = () => { + button.color = Color.yellow; +}; +``` + +**Layouts** : +``` +UIPanel (container) +├── layout: Layout (Horizontal, Vertical, Grid) +└── children: UIElement[] + +// Auto-arrange les éléments enfants +panel.layout = new VerticalLayout({ spacing: 10 }); +``` + +--- + +### 12.12 Dialogue System + +**Rôle** : Gérer les dialogues avec les NPCs. + +**Architecture** : +``` +DialogueManager +├── currentDialogue: Dialogue +├── showDialogue(dialogue: Dialogue) +└── advance() + +Dialogue +├── lines: DialogueLine[] +├── currentLine: number +└── speaker: string + +DialogueLine +├── text: string +├── speaker: string +├── choices: Choice[] (pour branching) +└── onComplete: () => void +Format de dialogue : +json{ + "id": "quest_start", + "lines": [ + { + "speaker": "Old Man", + "text": "Greetings, traveler!" + }, + { + "speaker": "Old Man", + "text": "I need your help to find my lost cat.", + "choices": [ + { "text": "Sure, I'll help!", "next": "quest_accept" }, + { "text": "Not interested.", "next": "quest_decline" } + ] + } + ] +} +``` + +--- + +### 12.13 Cutscene System + +**Rôle** : Séquences scriptées (cinématiques). + +**Architecture** : +``` +CutsceneManager +├── currentCutscene: Cutscene +├── play(cutscene: Cutscene) +└── update(deltaTime) + +Cutscene +├── timeline: CutsceneAction[] +├── currentTime: number +└── duration: number + +CutsceneAction +├── startTime: number +├── duration: number +└── execute() + +Types d'actions : +├── MoveAction (déplace un objet) +├── DialogueAction (affiche un dialogue) +├── CameraAction (déplace la caméra) +├── AnimationAction (joue une animation) +└── SoundAction (joue un son) +``` + +**Exemple** : +``` +const cutscene = new Cutscene([ + new CameraAction(0, 2, camera, new Vector2(500, 300)), // 0-2s + new DialogueAction(2, 3, "Hello!"), // 2-5s + new MoveAction(3, 2, player, new Vector2(600, 300)), // 3-5s + new SoundAction(5, 0.1, "explosion") // 5s +]); + +cutsceneManager.play(cutscene); +``` + +--- + +### 12.14 Multiplayer (WebSockets) + +**Rôle** : Synchronisation réseau pour le multijoueur. + +**Architecture** : +``` +NetworkManager +├── socket: WebSocket +├── localPlayer: GameObject +├── remotePlayers: Map +├── send(message: any) +└── onMessage(callback) + +NetworkedObject +├── networkId: string +├── owner: string +└── sync(data: any) +``` + +**Messages réseau** : +``` +// Connexion +{ type: "connect", playerId: "abc123" } + +// Position update +{ type: "position", playerId: "abc123", x: 100, y: 200 } + +// Action +{ type: "shoot", playerId: "abc123", direction: { x: 1, y: 0 } } +``` + +**Interpolation** : +``` +// Smooth les mouvements des autres joueurs +remotePlayer.targetPosition = receivedPosition; + +update(deltaTime) { + // Interpole vers la position reçue + remotePlayer.position = remotePlayer.position.lerp( + remotePlayer.targetPosition, + 0.1 + ); +} + +13. Patterns de code recommandés +13.1 Singleton (pour managers) +typescriptclass InputManager { + private static instance: InputManager; + + private constructor() {} + + static getInstance(): InputManager { + if (!this.instance) { + this.instance = new InputManager(); + } + return this.instance; + } +} + +// Utilisation +const input = InputManager.getInstance(); + +13.2 Factory (pour création d'objets) +typescriptclass GameObjectFactory { + static createPlayer(position: Vector2): GameObject { + const player = new GameObject("Player"); + player.transform.position = position; + + const sprite = player.addComponent(new SpriteRenderer()); + sprite.texture = AssetLoader.getTexture("player"); + + const rb = player.addComponent(new RigidBody()); + rb.mass = 1; + + const collider = player.addComponent(new BoxCollider()); + collider.size = new Vector2(32, 48); + + return player; + } + + static createEnemy(position: Vector2, type: string): GameObject { + // ... + } +} + +// Utilisation +const player = GameObjectFactory.createPlayer(new Vector2(100, 100)); +scene.addGameObject(player); + +13.3 Observer (pour événements) +typescriptclass EventBus { + private listeners = new Map>(); + + on(event: string, callback: Function): void { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + this.listeners.get(event)!.add(callback); + } + + off(event: string, callback: Function): void { + this.listeners.get(event)?.delete(callback); + } + + emit(event: string, data?: any): void { + this.listeners.get(event)?.forEach(cb => cb(data)); + } +} + +// Utilisation +eventBus.on("player:died", (data) => { + console.log("Game Over!"); + sceneManager.loadScene("GameOver"); +}); + +eventBus.emit("player:died", { score: 1500 }); + +13.4 Command (pour actions annulables) +typescriptinterface Command { + execute(): void; + undo(): void; +} + +class MoveCommand implements Command { + constructor( + private obj: GameObject, + private delta: Vector2 + ) {} + + execute(): void { + this.obj.transform.position.add(this.delta); + } + + undo(): void { + this.obj.transform.position.subtract(this.delta); + } +} + +class CommandManager { + private history: Command[] = []; + private current = -1; + + execute(command: Command): void { + command.execute(); + this.history = this.history.slice(0, this.current + 1); + this.history.push(command); + this.current++; + } + + undo(): void { + if (this.current >= 0) { + this.history[this.current].undo(); + this.current--; + } + } + + redo(): void { + if (this.current < this.history.length - 1) { + this.current++; + this.history[this.current].execute(); + } + } +} + +14. Checklist de développement +Phase 1 : Core (2-3 semaines) + + Structure de fichiers + TypeScript setup (tsconfig.json) + Classe Engine de base + GameLoop avec requestAnimationFrame + Time management (deltaTime) + Scene et SceneManager + Math (Vector2, Matrix3, Rect, Color) + +Phase 2 : WebGL Foundation (2-3 semaines) + + GLContext wrapper + Buffer (Vertex et Index) + Shader compilation et linking + Texture loading + Shader sprite de base (vertex + fragment) + Tests de rendu simple (triangle, quad) + +Phase 3 : Rendering System (3-4 semaines) + + WebGLRenderer + SpriteBatch (★ crucial) + Camera 2D avec matrices + Material system + RenderQueue avec tri + Frustum culling + Tests de performance (10000 sprites) + +Phase 4 : Entity Component System (2 semaines) + + GameObject + Transform avec hiérarchie + Component de base + SpriteRenderer component + Animator component + Tests d'intégration + +Phase 5 : Input et Audio (1 semaine) + + InputManager (clavier, souris) + Touch support + AudioManager + Tests sur différents navigateurs + +Phase 6 : Physics (2-3 semaines) + + RigidBody component + Collider (Box, Circle) + PhysicsSystem + CollisionSystem + Quadtree pour optimisation + Tests de performance + +Phase 7 : Assets et Utilities (1-2 semaines) + + AssetLoader + ObjectPool + EventBus + Debug utilities + Logger + +Phase 8 : Optimisations (1-2 semaines) + + Profiling tools + Dirty flags sur Transform + Batch optimization avancée + Memory profiling + +Phase 9 : Extensions (optionnel) + + Post-processing + Lighting 2D + Particle system GPU + UI System + Dialogue system + Save system + +Phase 10 : Documentation et Tests (1-2 semaines) + + Documentation API complète + Exemples d'utilisation + Tests unitaires + Tutoriels + + +15. Conseils finaux +Performance + +Profilez tôt et souvent avec performance.mark() et performance.measure() +Mesurez avant d'optimiser - Ne devinez pas où sont les bottlenecks +Le SpriteBatch est crucial - C'est là que 90% des gains se font +Object pooling pour tout ce qui spawn/despawn fréquemment +Quadtree seulement si vous avez beaucoup d'objets (>500) + +Code Quality + +TypeScript strict mode activé dès le début +Interfaces pour tout (IRenderable, IUpdatable, ICollidable...) +Composition > Héritage - ECS pattern +Single Responsibility Principle - Une classe = une responsabilité +Don't Repeat Yourself - Factorisez le code répété + +Debugging + +Console.log n'est pas suffisant - Utilisez le debugger +Visual debugging : Dessinez les colliders, le Quadtree, les forces... +Stats en temps réel : FPS, draw calls, objets actifs +Assertions : Vérifiez vos hypothèses +Hot reload : Configurez un bundler avec watch mode + +Architecture + +Commencez simple, ajoutez de la complexité seulement si nécessaire +Testez chaque système indépendamment avant de les intégrer +Gardez les systèmes découplés via interfaces et événements +Documentez les décisions importantes dans le code +Faites des exemples pour chaque feature + +WebGL Spécifique + +Minimisez les state changes (shader, texture, blending) +Batch, batch, batch - C'est la clé +N'oubliez pas de bind/unbind les buffers et textures +Gérez les erreurs WebGL avec gl.getError() +Testez sur différents GPU - Les performances varient énormément + + +16. Ressources utiles +Documentation + +MDN WebGL : Documentation complète +WebGL Fundamentals : Tutoriels excellents +Learn WebGL : Autre ressource de qualité + +Inspiration + +PixiJS : Moteur 2D WebGL (code source à étudier) +Phaser : Framework de jeu 2D +Three.js : Pour comprendre l'architecture + +Outils + +Spector.js : Debugger WebGL +Chrome DevTools : Performance profiling +Texture Packer : Créer des texture atlases +Tiled : Éditeur de tilemap + + +Voilà ! Vous avez maintenant une documentation complète de l'architecture d'un moteur de jeu 2D avec WebGL en TypeScript natif, sans librairie externe. +Résumé des points clés : + +WebGL pour exploiter le GPU et avoir des performances 10-100× meilleures +SpriteBatch pour dessiner 10000+ sprites en 1 draw call +ECS (Entity Component System) pour une architecture évolutive +Quadtree pour optimiser les collisions +Object Pooling pour éviter le garbage collector +Architecture modulaire pour faciliter la maintenance + +Bonne chance pour votre moteur de jeu ! 🎮 \ No newline at end of file diff --git a/docs/PROMPT.md b/docs/PROMPT.md new file mode 100644 index 0000000..1ae3f39 --- /dev/null +++ b/docs/PROMPT.md @@ -0,0 +1,19 @@ +Je développe un moteur de jeu 2D WebGL en TypeScript selon la documentation CORE.MD. +État actuel : +Setup complet (pnpm, Vite, TypeScript strict) +Math : Vector2, Color, Rect, Matrix3 +WebGL : GLContext, Shader, Buffer, Texture +Core : Time, GameLoop, Scene, SceneManager, Engine +Rendering : Camera, WebGLRenderer, SpriteBatch +index.ts utilise Engine comme point d'entrée +À faire (selon CORE.MD, phase 4) : +GameObject : entité de base avec Transform, hiérarchie parent-enfant, cycle de vie (awake, start, update, destroy) +Transform : position/rotation/scale locales et monde, matrice worldMatrix avec dirty flags, hiérarchie +Component : classe abstraite de base, référence au GameObject, cycle de vie +Principes : +TypeScript strict mode +ECS (composition, pas héritage) +Utiliser deltaTime pour tous les mouvements +SpriteBatch pour performance +Dirty flags pour optimiser +Continue le développement étape par étape, code les implémentations une par une, teste la compilation à chaque étape, et mets à jour DEV_LOG.md après chaque ajout. \ No newline at end of file diff --git a/index.html b/index.html index 0d2ce3f..e8bb59b 100644 --- a/index.html +++ b/index.html @@ -1,84 +1,66 @@ - - - - - - - - - - - Document - - -
-
- -

- CastleStorm & © 2023,
- 411 Inc Ltd. All rights reserved. -

-
- + + + + CastleStorm Engine + + + +
+ +
+
FPS: 0
+
CastleStorm Engine v0.1.0
- - +
+ + + + diff --git a/js/Core/functions/reset.js b/js/Core/functions/reset.js deleted file mode 100644 index bb44665..0000000 --- a/js/Core/functions/reset.js +++ /dev/null @@ -1,32 +0,0 @@ - - -export function resetGame(gameVariables, canvas) { - gameVariables.character.model = null; - gameVariables.projectiles = []; - gameVariables.enemies = []; - gameVariables.loots = []; - gameVariables.character.keyPresses = { - z: false, - q: false, - s: false, - d: false, - }; - gameVariables.playerLevel = { - cap: 200, - currentXp: 0, - currentLevel: 1 - }; - gameVariables.playerHealth = { - cap: 100, - maxHealth: 100, - currentHealth: 100 - } - gameVariables.character.attack = 10; - gameVariables.character.armor = 0; - gameVariables.isLooping = false; - gameVariables.intervalInstances.forEach((interval) => { - clearInterval(interval); - }); - gameVariables.intervalInstances.length = 0; -} - diff --git a/js/Core/loader.js b/js/Core/loader.js deleted file mode 100644 index 645120f..0000000 --- a/js/Core/loader.js +++ /dev/null @@ -1,23 +0,0 @@ -export async function loadImages(paths) { - const images = []; - - for (let i = 0, l = paths.length; i < l; i++) { - images[i] = await loadImage(paths[i]); - console.log(paths[i] + " chargé"); - } - return images; -} - -async function loadImage(path) { - const image = new Image(); - image.src = path; - - try { - await image.decode(); - } catch (error) { - console.log(error); - return null; - } - - return image; -} \ No newline at end of file diff --git a/js/Core/physics/collision.js b/js/Core/physics/collision.js deleted file mode 100644 index e69de29..0000000 diff --git a/js/Core/physics/movement.js b/js/Core/physics/movement.js deleted file mode 100644 index 6496d5e..0000000 --- a/js/Core/physics/movement.js +++ /dev/null @@ -1,46 +0,0 @@ -export function move(game) { - // faire que le personnage ne puisse pas sortir de la map - const character = game.character.model; - if (game.character.inputs.z) { - if (game.character.model.targetY > 0) { - game.character.model.targetY -= game.character.model.speed; - } else { - game.character.model.targetY = 0; - } - - } else if (game.character.inputs.s) { - if (game.character.model.targetY < window.innerHeight) { - game.character.model.targetY += game.character.model.speed; - } else { - game.character.model.targetY = window.innerHeight; - } - } - if (game.character.inputs.q) { - if (game.character.model.targetX > 0) { - game.character.model.targetX -= game.character.model.speed; - } else { - game.character.model.targetX = 0; - } - } else if (game.character.inputs.d) { - if (game.character.model.targetX < window.innerWidth) { - game.character.model.targetX += game.character.model.speed; - } else { - game.character.model.targetX = window.innerWidth; - } - } -} - -export function dash(game) { - // on doit faire le dash dans la direction du curseur dans un rayon de 20px max - if (game.character.inputs[" "]) { - console.log("dash"); - } -} - -export function keyDownListener(event, game) { - game.character.inputs[event.key] = true; -} -export function keyUpListener(event, game) { - game.character.inputs[event.key] = false; -} - diff --git a/js/Core/physics/shoot.js b/js/Core/physics/shoot.js deleted file mode 100644 index 1bbec45..0000000 --- a/js/Core/physics/shoot.js +++ /dev/null @@ -1,9 +0,0 @@ - -export function shoot(e, game, projectileClass) { - const angle = Math.atan2(e.clientY - game.character.model.y, e.clientX - game.character.model.x); - const velocity = { - x: Math.cos(angle) * 20, - y: Math.sin(angle) * 20, - } - game.projectiles.push(new projectileClass(game.character.model.x, game.character.model.y, 5, velocity, "red")); -} diff --git a/js/Core/physics/spawn.js b/js/Core/physics/spawn.js deleted file mode 100644 index 81dc395..0000000 --- a/js/Core/physics/spawn.js +++ /dev/null @@ -1,33 +0,0 @@ - - -export function spawnEnemies(canvas, game, enemyClass) { - if (game.isLooping === false) { - return; - } - return setInterval(() => { - const randomRadius = Math.random() * 30 + 10; - const randomSpeed = Math.random() * 6 + 1; - const randomColor = `hsl(${Math.random() * 360}, 50%, 50%)`; - const randomHealth = Math.random() * 30 + 20; - let x; - let y; - if (Math.random() < 0.5) { - x = Math.random() < 0.5 ? 0 - randomRadius : canvas.width + randomRadius; - y = Math.random() * canvas.height; - } else { - x = Math.random() * canvas.width; - y = Math.random() < 0.5 ? 0 - randomRadius : canvas.height + randomRadius; - } - const enemy = new enemyClass( - x, - y, - randomRadius, - randomColor, - randomHealth, - { x: 0, y: 0 }, - randomSpeed, - 'ranged' - ); - game.enemies.push(enemy); - }, 3000); -} diff --git a/js/Core/ui/drawUI.js b/js/Core/ui/drawUI.js deleted file mode 100644 index 3597019..0000000 --- a/js/Core/ui/drawUI.js +++ /dev/null @@ -1,74 +0,0 @@ -export function drawCharacterHpBar(context, game) { - context.beginPath(); - context.fillStyle = "red"; - context.rect( - game.character.model.x - 20, - game.character.model.y - 30, - game.playerHealth.currentHealth / game.playerHealth.maxHealth * 40, - 5 - ); - context.fill(); - context.closePath(); - context.beginPath(); - context.strokeStyle = "black"; - context.lineWidth = 1; - context.rect( - game.character.model.x - 20, - game.character.model.y - 30, - 40, - 5 - ); - context.stroke(); - context.closePath(); -} - -export function drawHealthBar(context, game) { - // On dessine la barre de vie du joueur en haut à gauche avec les valeurs actuelles de vie et de vie maximale, - // au centre de la barre de vie - context.beginPath(); - context.fillStyle = "red"; - context.rect(10, 10, game.playerHealth.currentHealth / game.playerHealth.maxHealth * 200, 20); - context.fill(); - context.closePath(); - context.beginPath(); - context.strokeStyle = "black"; - context.lineWidth = 2; - context.rect(10, 10, 200, 20); - context.stroke(); - context.closePath(); - context.beginPath(); - context.font = "12px Arial"; - context.fillStyle = "black"; - context.textAlign = "center"; - context.fillText(game.playerHealth.currentHealth + "/" + game.playerHealth.maxHealth, 110, 25); - context.closePath(); - -} - -export function drawXpBar(context, game) { - context.beginPath(); - context.fillStyle = "lightgreen"; - context.rect(10, 50, game.playerLevel.currentXp / game.playerLevel.cap * 200, 20); - context.fill(); - context.closePath(); - context.beginPath(); - context.strokeStyle = "black"; - context.lineWidth = 2; - context.rect(10, 50, 200, 20); - context.stroke(); - context.closePath(); - context.beginPath(); - context.font = "12px Arial"; - context.fillStyle = "black"; - context.textAlign = "center"; - context.fillText(Math.round(game.playerLevel.currentXp) + "/" + Math.round(game.playerLevel.cap), 110, 65); - context.closePath(); -} - -export function drawPlayerStats(context, game) { - context.beginPath(); - // On affiche l'image de la fenêtre de statistiques du joueur qui fait 256x256 en haut a droite - context.drawImage(game.gui.playerStats, window.innerWidth - 260, -20, 256, 256); - context.closePath(); -} - diff --git a/js/Core/vars/game.js b/js/Core/vars/game.js deleted file mode 100644 index 17592db..0000000 --- a/js/Core/vars/game.js +++ /dev/null @@ -1,94 +0,0 @@ -export const game = { - isLooping: false, - playerLevel: { - cap: 200, - currentXp: 0, - currentLevel: 1 - }, - playerHealth: { - maxHealth: 100, - currentHealth: 100 - }, - character : { - sprites: { - up: null, - upLeft: null, - upRight: null, - down: null, - downLeft: null, - downRight: null, - left: null, - right: null, - }, - statistics: { - level: { - xp: 0, - cap: 200, - current: 1 - }, - health: { - max: 100, - current: 100 - }, - armor: 0, - attack: 10, - speed: 5, - }, - model: null, - money: 0, - attack: 10, - armor: 0, - inputs: { - z: false, - q: false, - s: false, - d: false, - click: true, - }, - isHit: false, - isMoving: false, - }, - projectiles: [], - enemies: [], - loots: { - sprites: { - potion: null, - money: { - coin: null, - pile: null, - bag: null - } - }, - instances: [] - }, - gui: { - playerStats: null, - }, - intervalInstances: [], - mousePos: { - x: 0, - y: 0 - } -} - -export const characterSprites = [ - "assets/img/character/up/Character_Up.png", - "assets/img/character/up/Character_UpLeft.png", - "assets/img/character/up/Character_UpRight.png", - "assets/img/character/down/Character_Down.png", - "assets/img/character/down/Character_DownLeft.png", - "assets/img/character/down/Character_DownRight.png", - "assets/img/character/left/Character_Left.png", - "assets/img/character/right/Character_Right.png", -]; - -export const lootSprites = [ - "assets/img/loots/health/potion.png", - "assets/img/loots/money/coin.png", - "assets/img/loots/money/pile.png", - "assets/img/loots/money/bag.png", -] - -export const uiSprites = [ - "assets/img/gui/gui.png", -] \ No newline at end of file diff --git a/js/Entities/Character.js b/js/Entities/Character.js deleted file mode 100644 index 5de0e38..0000000 --- a/js/Entities/Character.js +++ /dev/null @@ -1,123 +0,0 @@ - -import Entity from "./Entity.js"; -export default class Character extends Entity { - constructor(x, y, radius) { - super(x, y, radius); - this.speed = 5; - this.targetX = this.x; - this.targetY = this.y; - this.frame = 0; - this.sprite = 0; - } - - draw(context, game) { - context.beginPath(); - context.drawImage(this.sprite, this.frame * 32, 0, 32, 32, this.x - 32, this.y - 32, 32 * 2, 32 * 2); - context.closePath(); - - ////////////////////////// DEBUG //////////////////////////// - // dessin de la cible de destination du personnage (pour le debug) - context.beginPath(); - context.arc(this.targetX, this.targetY, 5, 0, Math.PI * 2, false); - context.fillStyle = 'red'; - context.fill(); - context.closePath(); - // dessin de la direction du personnage en fonction de la cible (pour le debug) - context.beginPath(); - context.moveTo(this.x, this.y); - context.lineTo(this.targetX, this.targetY); - context.strokeStyle = 'blue'; - context.stroke(); - context.closePath(); - // dessin de la ligne de tir du personnage en fonction du curseur (pour le debug) et un arc de cercle pour le curseur - context.beginPath(); - context.moveTo(this.x, this.y); - context.lineTo(game.mousePos.x, game.mousePos.y); - context.strokeStyle = 'red'; - context.stroke(); - context.closePath(); - // dessin de l'arc de cercle pour le curseur de 360° (pour le debug) - context.beginPath(); - context.arc(game.mousePos.x, game.mousePos.y, 5, 0, Math.PI * 2, false); - context.fillStyle = 'red'; - context.fill(); - context.closePath(); - - - - } - - update(context, game) { - this.updateSprite(context, game); - this.draw(context, game); - const dx = this.targetX - this.x; - const dy = this.targetY - this.y; - if (dx !== 0 || dy !== 0) { - const angle = Math.atan2(dy, dx); - const velocity = { - x: Math.cos(angle) * this.speed, - y: Math.sin(angle) * this.speed - } - if (Math.abs(dx) < Math.abs(velocity.x)) { - this.x = this.targetX; - } else { - this.x += velocity.x; - } - if (Math.abs(dy) < Math.abs(velocity.y)) { - this.y = this.targetY; - } else { - this.y += velocity.y; - } - } - } - - updateSprite(context, game) { - // on doit vérifier la position du curseur par rapport au personnage pour savoir si on doit changer le sprite - // on doit avoir 8 sprites différents pour les 8 directions possibles - // on doit donc vérifier si le curseur est dans un des 8 angles de 45° autour du personnage - let angle = Math.atan2(game.mousePos.y - this.y, game.mousePos.x - this.x); - if (angle < 0) { - angle += Math.PI * 2; - } - // on a l'angle entre le personnage et le curseur - // on doit maintenant vérifier dans quel angle de 45° on se trouve - // on va donc vérifier si l'angle est compris entre 0 et 45°, 45° et 90°, etc... - - if (angle >= 0 && angle < Math.PI / 8) { - this.sprite = game.character.sprites.right; - } else if (angle >= Math.PI / 8 && angle < Math.PI * 3 / 8) { - this.sprite = game.character.sprites.downRight; - } else if (angle >= Math.PI * 3 / 8 && angle < Math.PI * 5 / 8) { - this.sprite = game.character.sprites.down; - } else if (angle >= Math.PI * 5 / 8 && angle < Math.PI * 7 / 8) { - this.sprite = game.character.sprites.downLeft; - } else if (angle >= Math.PI * 7 / 8 && angle < Math.PI * 9 / 8) { - this.sprite = game.character.sprites.left; - } else if (angle >= Math.PI * 9 / 8 && angle < Math.PI * 11 / 8) { - this.sprite = game.character.sprites.upLeft; - } else if (angle >= Math.PI * 11 / 8 && angle < Math.PI * 13 / 8) { - this.sprite = game.character.sprites.up; - } else if (angle >= Math.PI * 13 / 8 && angle < Math.PI * 15 / 8) { - this.sprite = game.character.sprites.upRight; - } else if (angle >= Math.PI * 15 / 8 && angle < Math.PI * 2) { - this.sprite = game.character.sprites.right; - } - - // on doit mtn vérifier si on est en train de bouger ou pas - // si on est en train de bouger, on doit afficher l'animation de marche sinon on doit afficher l'animation d'arrêt - - if (this.targetX !== this.x || this.targetY !== this.y) { - this.frame += 1; - if (this.frame >= 4) { - this.frame = 0; - } - } else { - this.frame = 0; - } - - } - - - - -} diff --git a/js/Entities/Enemy.js b/js/Entities/Enemy.js deleted file mode 100644 index 07b4d7b..0000000 --- a/js/Entities/Enemy.js +++ /dev/null @@ -1,84 +0,0 @@ -import Entity from "./Entity.js"; -import Projectile from "./Projectile.js"; -export default class Enemy extends Entity { - constructor(x, y, radius, color, health, velocity, speed, behavior) { - super(x, y, radius); - this.color = color; // Couleur de l'ennemi - this.health = health; // Points de vie de l'ennemi - this.velocity = velocity; - this.speed = speed; // Vitesse de l'ennemi - this.dx = 0; // Différence de position entre l'ennemi et le joueur - this.dy = 0; // Différence de position entre l'ennemi et le joueur - this.angle = 0; // Angle entre l'ennemi et le joueur - this.behavior = behavior // Comportement de l'ennemi - } - - draw(context) { - context.beginPath(); - context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false) - context.fillStyle = this.color; - context.fill(); - context.closePath(); - context.beginPath(); - context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false) - context.strokeStyle = "black"; - context.stroke(); - context.closePath(); - - } - - update(context, game) { - this.draw(context); - const distance = Math.hypot(this.dx, this.dy) - switch (this.behavior) { - case 'follow': - this.updateAngle(game); - this.updateSpeed(); - this.x += this.velocity.x; - this.y += this.velocity.y; - // Si l'ennemi est trop proche du joueur, il recule - // Cette condition est nécessaire pour éviter que l'ennemi ne se colle au joueur - if (distance < this.radius + game.character.model.radius - 3) { - this.x -= this.velocity.x; - this.y -= this.velocity.y; - } - break; - case 'ranged': - this.updateAngle(game); - this.updateSpeed(); - setTimeout(() => { - this.shoot(game, Projectile); - }, 1000); - this.x += this.velocity.x; - this.y += this.velocity.y; - if (distance < this.radius + game.character.model.radius + 300) { - this.x -= this.velocity.x; - this.y -= this.velocity.y; - } - break; - } - } - - shoot(game, Projectile) { - const velocity = { - x: Math.cos(this.angle) * 20, - y: Math.sin(this.angle) * 20, - } - const projectile = new Projectile(this.x , this.y , 5, velocity, 'black'); - game.projectiles.push(projectile); - } - - updateAngle(game) { - this.dx = game.character.model.x - this.x; - this.dy = game.character.model.y - this.y; - this.angle = Math.atan2(this.dy, this.dx); - } - - updateSpeed() { - this.velocity.x = Math.cos(this.angle) * this.speed; - this.velocity.y = Math.sin(this.angle) * this.speed; - } - - - -} diff --git a/js/Entities/Entity.js b/js/Entities/Entity.js deleted file mode 100644 index 495fe07..0000000 --- a/js/Entities/Entity.js +++ /dev/null @@ -1,9 +0,0 @@ -export default class Entity { - constructor(x, y, radius) { - this.x = x; - this.y = y; - this.radius = radius; - } - - -} \ No newline at end of file diff --git a/js/Entities/Loot.js b/js/Entities/Loot.js deleted file mode 100644 index c783b16..0000000 --- a/js/Entities/Loot.js +++ /dev/null @@ -1,27 +0,0 @@ - -import Entity from "./Entity.js"; -export default class Loot extends Entity { - constructor(x, y, type, radius) { - super(x, y, radius); - this.type = type; - } - - draw(context, game) { - if (this.type === 'health') { - // Dessine une potion de vie ayant sa hitbox au centre de l'image - context.beginPath(); - context.drawImage(game.loots.sprites.potion, 0, 0, 32, 32, this.x - 16, this.y - 16, 32 * 1.2, 32 * 1.2); - context.closePath(); - } else - if (this.type === 'money') { - // Dessine des pièces d'or - context.beginPath(); - context.drawImage(game.loots.sprites.money.coin, 0, 0, 32, 32, this.x - 16, this.y - 16, 32 * 1.5, 32 * 1.5); - } - } - - update(context, game) { - this.draw(context, game); - } - -} \ No newline at end of file diff --git a/js/Entities/Projectile.js b/js/Entities/Projectile.js deleted file mode 100644 index 90a11d0..0000000 --- a/js/Entities/Projectile.js +++ /dev/null @@ -1,26 +0,0 @@ - -import Entity from "./Entity.js"; -export default class Projectile extends Entity { - constructor(x, y, radius, velocity, color) { - super(x, y, radius); - this.velocity = velocity; - this.color = color; - } - - draw(context) { - context.beginPath(); - context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false) - context.fillStyle = this.color; - context.fill(); - context.closePath(); - } - - update(context) { - this.draw(context); - this.x = this.x + this.velocity.x; - this.y = this.y + this.velocity.y; - } - - - -} \ No newline at end of file diff --git a/js/main.js b/js/main.js deleted file mode 100644 index 0a62b66..0000000 --- a/js/main.js +++ /dev/null @@ -1,224 +0,0 @@ -import Character from "./Entities/Character.js"; -import Enemy from "./Entities/Enemy.js"; -import Loot from "./Entities/Loot.js"; -import Projectile from "./Entities/Projectile.js"; -import {game, characterSprites, lootSprites} from "./Core/vars/game.js"; -import {resetGame} from "./Core/functions/reset.js"; -import {drawCharacterHpBar, drawHealthBar, drawPlayerStats, drawXpBar} from "./Core/ui/drawUI.js"; -import {spawnEnemies} from "./Core/physics/spawn.js"; -import {shoot} from "./Core/physics/shoot.js"; -import {move, dash, keyDownListener, keyUpListener} from "./Core/physics/movement.js"; -import {loadImages} from "./Core/loader.js"; - - -function drawGameIU(context, game) { - drawXpBar(context, game); - drawHealthBar(context, game); - drawCharacterHpBar(context, game); - drawPlayerStats(context, game); -} - -function clearCanvas(context, canvas) { - context.clearRect(0, 0, canvas.width, canvas.height); -} - - -function updateMousePos(e, game) { - game.mousePos = { - x: e.clientX, - y: e.clientY, - } -} - - -// Initialisation du canvas html et du contexte 2d -const canvas = document.getElementById("myCanvas"); -const context = canvas.getContext("2d"); -// On définit la taille du canvas en fonction de la taille de la fenêtre -canvas.width = window.innerWidth; -canvas.height = window.innerHeight -// On désactive l'anti-aliasing pour avoir des sprites pixelisés -context.imageSmoothingEnabled = false; -// On cache le canvas tant que le joueur n'a pas cliqué sur le bouton "Start" -canvas.style.display = "none" - - -// Initialisation du menu de démarrage du jeu et du bouton "Commencez" -const startMenu = document.getElementById("startMenu"); -const startButton = document.getElementById("startButton"); - - - -let up, upLeft, upRight, down, downLeft, downRight, left, right; -[up, upLeft, upRight, down, downLeft, downRight, left, right] = await loadImages(characterSprites); -let potion, coin, pile, bag; -[potion, coin, pile, bag] = await loadImages(lootSprites); -let quiPlayerStats; -[quiPlayerStats] = await loadImages(["assets/img/gui/gui.png"]); -game.gui.playerStats = quiPlayerStats; -console.log("Sprites chargés"); -game.character.sprites = { - up: up, - upLeft: upLeft, - upRight: upRight, - down: down, - downLeft: downLeft, - downRight: downRight, - left: left, - right: right -} -game.loots.sprites = { - potion: potion, - money: { - coin: coin, - pile: pile, - bag: bag - } -} -// On ajoute un évènement sur le bouton "Commencez" pour lancer le jeu -startButton.addEventListener("click", () => { - // On cache le menu de démarrage et on affiche le canvas - startMenu.style.display = "none"; - canvas.style.display = "block"; - // Apparition du personnage - game.character.model = new Character(canvas.width / 2, canvas.height / 2, 10); - // On lance le jeu - gameLoop(); - game.isLooping = true; - // Apparition des ennemis - game.intervalInstances.push(spawnEnemies(canvas, game, Enemy)); - // On ajoute des évènements sur les touches du clavier - window.onmousemove = (e) => game.isLooping ? updateMousePos(e, game) : null; - window.addEventListener('keydown', (e) => game.isLooping ? keyDownListener(e, game) : null); - window.addEventListener('keyup', (e) => game.isLooping ? keyUpListener(e, game) : null); - document.addEventListener("mousedown", (e) => - { - if (game.isLooping) { - game.character.inputs.click = true; - shoot(e, game, Projectile) - } - }); - document.addEventListener("mouseup", (e) => - { - game.character.inputs.click = false; - if (game.isLooping) { - game.character.inputs.click = false; - shoot(e, game, Projectile) - } - }); -}) - - - - - - - -let requestId = 0; - -function gameLoop() { - try { - requestId = requestAnimationFrame(gameLoop); - clearCanvas(context, canvas); - // On ajoute l'événement de déplacement du personnage - move(game); - // On ajoute la mécanique de dash - dash(game); - // On actualise le personnage - game.character.model.update(context, game); - // On actualise les projectiles - game.projectiles.forEach((projectile) => { - projectile.update(context); - }); - // Pour chaque loot - game.loots.instances.forEach((loot, index) => { - // On actualise le loot - loot.update(context, game); - // On vérifie si le personnage est en collision avec le loot - const distance = Math.hypot(game.character.model.x - loot.x, game.character.model.y - loot.y); - // Si le personnage est en collision avec le loot - if (distance - loot.radius - game.character.model.radius < 1) { - console.log("collision") - game.loots.instances.splice(index, 1); - // On vérifie le type de loot - if (loot.type === "health") { - // On ajoute de la vie au joueur - game.playerHealth.currentHealth += 20; - if (game.playerHealth.currentHealth > game.playerHealth.maxHealth) { - game.playerHealth.currentHealth = game.playerHealth.maxHealth; - } - } else if (loot.type === "money") { - game.character.money += Math.floor(Math.random() * 10) + 1; - } - } - }); - // Pour chaque ennemi - game.enemies.forEach((enemy, index) => { - // On actualise l'ennemi - enemy.update(context, game); - // On vérifie si le personnage est en collision avec l'ennemi - const distance = Math.hypot( - game.character.model.x - enemy.x, - game.character.model.y - enemy.y - ); - if (enemy.behavior === "ranged") { - - } - // Si le personnage est en collision avec l'ennemi - if (distance - enemy.radius - game.character.model.radius < 1) { - // On vérifie si le personnage n'est pas déjà en train de se faire attaquer - if (!game.character.model.isHit) { - // Sinon, on retire des points de vie au personnage - game.playerHealth.currentHealth -= 20 - game.character.armor; - game.character.model.isHit = true; - setTimeout(() => { - if (game.character.model) game.character.model.isHit = false; - }, 500); - } - // On vérifie si le personnage n'a plus de points de vie - if (game.playerHealth.currentHealth <= 0) { - // Si oui, on arrête la boucle de jeu et on affiche le menu de départ - cancelAnimationFrame(requestId); - canvas.style.display = "none"; - startMenu.style.display = "flex"; - resetGame(game); - } - } - // Pour chaque projectile - game.projectiles.forEach((projectile, projectileIndex) => { - // On vérifie si le projectile est en collision avec l'ennemi - const distance = Math.hypot(projectile.x - enemy.x, projectile.y - enemy.y); - if (distance - enemy.radius - projectile.radius < 1) { - // Si oui, on retire les points de vie de l'ennemi - enemy.health -= game.character.attack; - // game.projectiles.splice(projectileIndex, 1); - enemy.color = "red"; - setTimeout(() => { - enemy.color = "green" - }, 100); - // Si les points de vie de l'ennemi sont inférieurs ou égaux à 0 on le supprime - if (enemy.health <= 0) { - const type = Math.random() > 0.5 ? "health" : "money"; - game.enemies.splice(index, 1); - game.playerLevel.currentXp += enemy.radius; - game.loots.instances.push(new Loot(enemy.x, enemy.y, type, 5)); - // On vérifie si le joueur a assez d'expérience pour passer au niveau supérieur - if (game.playerLevel.currentXp >= game.playerLevel.cap) { - game.playerLevel.currentXp = 0; - game.playerLevel.cap *= 1.5; - game.playerLevel.currentLevel += 1; - game.playerHealth.maxHealth += 5; - game.character.attack += 1; - game.character.armor += 1; - } - } - } - }); - }); - drawGameIU(context, game); - } catch (e) { - cancelAnimationFrame(requestId); - } -} - - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5f7cc5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,578 @@ +{ + "name": "castlestorm-engine", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "castlestorm-engine", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.10.0", + "rimraf": "^5.0.5", + "typescript": "^5.3.3" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9130f8b --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "castlestorm-engine", + "version": "0.1.0", + "description": "Moteur de jeu 2D avec WebGL en TypeScript", + "main": "dist/index.js", + "type": "module", + "scripts": { + "build": "tsc && vite build", + "watch": "tsc --watch", + "dev": "vite", + "preview": "vite preview", + "clean": "rimraf dist" + }, + "keywords": [ + "game-engine", + "webgl", + "typescript", + "2d", + "ecs" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.10.0", + "rimraf": "^5.0.5", + "typescript": "^5.3.3", + "vite": "^5.0.0" + }, + "dependencies": {} +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7b010c2 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,843 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^20.10.0 + version: 20.19.24 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.0 + version: 5.4.21(@types/node@20.19.24) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@20.19.24': + resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + + '@rollup/rollup-android-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.5': + optional: true + + '@rollup/rollup-darwin-x64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.5': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@20.19.24': + dependencies: + undici-types: 6.21.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + lru-cache@10.4.3: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + nanoid@3.3.11: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rimraf@5.0.10: + dependencies: + glob: 10.4.5 + + rollup@4.52.5: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 + fsevents: 2.3.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + vite@5.4.21(@types/node@20.19.24): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.5 + optionalDependencies: + '@types/node': 20.19.24 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01.png b/public/Retro Inventory/Retro Inventory/Original/Health_01.png new file mode 100644 index 0000000..62e9c94 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar01.png new file mode 100644 index 0000000..7b12b56 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar02.png new file mode 100644 index 0000000..9d96b8d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar03.png new file mode 100644 index 0000000..aeaff91 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_01_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02.png b/public/Retro Inventory/Retro Inventory/Original/Health_02.png new file mode 100644 index 0000000..4464273 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar01.png new file mode 100644 index 0000000..50a62b7 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar02.png new file mode 100644 index 0000000..7dd79cb Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar03.png new file mode 100644 index 0000000..ca72174 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_02_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03.png b/public/Retro Inventory/Retro Inventory/Original/Health_03.png new file mode 100644 index 0000000..96e21d7 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar01.png new file mode 100644 index 0000000..d8d38b3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar02.png new file mode 100644 index 0000000..c15e930 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar03.png new file mode 100644 index 0000000..084c518 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_03_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04.png b/public/Retro Inventory/Retro Inventory/Original/Health_04.png new file mode 100644 index 0000000..a469c07 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar01.png new file mode 100644 index 0000000..aba9976 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar02.png new file mode 100644 index 0000000..9fc5389 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar03.png new file mode 100644 index 0000000..41e6d72 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar04.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar04.png new file mode 100644 index 0000000..b94dd14 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar05.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar05.png new file mode 100644 index 0000000..61b53ae Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar05.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar06.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar06.png new file mode 100644 index 0000000..389bbaa Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Bar06.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue.png new file mode 100644 index 0000000..0b1087b Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue_Clear.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue_Clear.png new file mode 100644 index 0000000..adcbc6c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Blue_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red.png new file mode 100644 index 0000000..307bf04 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red_Clear.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red_Clear.png new file mode 100644 index 0000000..05e1c73 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Red_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow.png new file mode 100644 index 0000000..de6b104 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow_Clear.png b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow_Clear.png new file mode 100644 index 0000000..627fd28 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_04_Heart_Yellow_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_05_01.png b/public/Retro Inventory/Retro Inventory/Original/Health_05_01.png new file mode 100644 index 0000000..268fb66 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_05_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_05_02.png b/public/Retro Inventory/Retro Inventory/Original/Health_05_02.png new file mode 100644 index 0000000..ba9dd25 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_05_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Health_05_03.png b/public/Retro Inventory/Retro Inventory/Original/Health_05_03.png new file mode 100644 index 0000000..f5ed7ee Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Health_05_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue.png new file mode 100644 index 0000000..004b317 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_1.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_1.png new file mode 100644 index 0000000..d529fd3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_2.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_2.png new file mode 100644 index 0000000..6ff5915 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_3.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_3.png new file mode 100644 index 0000000..d0845de Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_4.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_4.png new file mode 100644 index 0000000..3f00299 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Blue_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange.png new file mode 100644 index 0000000..e0b6749 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_1.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_1.png new file mode 100644 index 0000000..32e8998 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_2.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_2.png new file mode 100644 index 0000000..46aa9ea Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_3.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_3.png new file mode 100644 index 0000000..70ed442 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_4.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_4.png new file mode 100644 index 0000000..db0f7be Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Orange_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red.png new file mode 100644 index 0000000..e335e05 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Red.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red_1.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_1.png new file mode 100644 index 0000000..2dd2ea3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red_2.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_2.png new file mode 100644 index 0000000..d9241d8 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red_3.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_3.png new file mode 100644 index 0000000..bb67295 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Heart_Red_4.png b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_4.png new file mode 100644 index 0000000..d0df223 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Heart_Red_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts.png b/public/Retro Inventory/Retro Inventory/Original/Hearts.png new file mode 100644 index 0000000..28354f2 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_1.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_1.png new file mode 100644 index 0000000..9ffdd01 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_2.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_2.png new file mode 100644 index 0000000..e476446 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_3.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_3.png new file mode 100644 index 0000000..b79546d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_4.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_4.png new file mode 100644 index 0000000..e61d8c3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_5.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_5.png new file mode 100644 index 0000000..f6d7db6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Blue_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png new file mode 100644 index 0000000..635491a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png new file mode 100644 index 0000000..5fac4a9 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_3.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_3.png new file mode 100644 index 0000000..ec343ef Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png new file mode 100644 index 0000000..43c7b9f Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_5.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_5.png new file mode 100644 index 0000000..f6d7db6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Red_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_1.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_1.png new file mode 100644 index 0000000..b9b962a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_2.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_2.png new file mode 100644 index 0000000..75168e3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_3.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_3.png new file mode 100644 index 0000000..80ccbde Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_4.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_4.png new file mode 100644 index 0000000..db98f2c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_5.png b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_5.png new file mode 100644 index 0000000..f6d7db6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Hearts_Yellow_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory.png b/public/Retro Inventory/Retro Inventory/Original/Inventory.png new file mode 100644 index 0000000..afb0139 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_9Slices.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_9Slices.png new file mode 100644 index 0000000..d87ac06 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_9Slices.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_01.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_01.png new file mode 100644 index 0000000..b7846d0 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_02.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_02.png new file mode 100644 index 0000000..c2439a4 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_03.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_03.png new file mode 100644 index 0000000..5e1af9e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_04.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_04.png new file mode 100644 index 0000000..c5da475 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Example_04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_1.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_1.png new file mode 100644 index 0000000..f9ff3df Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_10.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_10.png new file mode 100644 index 0000000..d4d9bae Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_10.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_2.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_2.png new file mode 100644 index 0000000..d8ed9be Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_3.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_3.png new file mode 100644 index 0000000..864a347 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_4.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_4.png new file mode 100644 index 0000000..4de41ca Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_5.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_5.png new file mode 100644 index 0000000..8ce4aa1 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_6.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_6.png new file mode 100644 index 0000000..1a2733d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_6.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_7.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_7.png new file mode 100644 index 0000000..340a899 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_7.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_8.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_8.png new file mode 100644 index 0000000..9ac30eb Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_8.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_9.png b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_9.png new file mode 100644 index 0000000..318c166 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Inventory_Slot_9.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings.png b/public/Retro Inventory/Retro Inventory/Original/Settings.png new file mode 100644 index 0000000..5392dfd Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Bar01.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar01.png new file mode 100644 index 0000000..efcca13 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Bar02.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar02.png new file mode 100644 index 0000000..8be6b0a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Bar03.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar03.png new file mode 100644 index 0000000..5494241 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Cross01.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross01.png new file mode 100644 index 0000000..70512e9 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Cross02.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross02.png new file mode 100644 index 0000000..b3753bb Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Original/Settings_Cross03.png b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross03.png new file mode 100644 index 0000000..541ffbd Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Original/Settings_Cross03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Read Me.txt b/public/Retro Inventory/Retro Inventory/Read Me.txt new file mode 100644 index 0000000..fc21e0f --- /dev/null +++ b/public/Retro Inventory/Retro Inventory/Read Me.txt @@ -0,0 +1,31 @@ +Dear valued customer, + +Thank you for purchasing my asset pack! +It means a lot to me that you chose to include my work in your creative projects. +I put a lot of time and effort into creating high-quality assets that can help +bring your vision to life, and I'm glad that you found value in my work. + +I hope that my assets will be useful for your game development or design work, +and that they will help you achieve the desired results. +If you have any questions or feedback, please don't hesitate to reach out to me. +I am always open to hearing from my customers and improving my work. + +Thank you again for your support and for choosing to work with my asset pack. +I hope it exceeds your expectations and helps you create amazing things. + +Best regards, +ElvGames + + + +License: + You are free to use, personal or commercial projects. + You can edit/modify to fit your game. + Credits to ElvGames. + +You cannot: + Sell this asset pack, not even modified. + Claim this asset is yours. + +Follow me on Twitter! +https://twitter.com/ElvGames \ No newline at end of file diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01.png new file mode 100644 index 0000000..709447a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar01.png new file mode 100644 index 0000000..b0478b2 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar02.png new file mode 100644 index 0000000..7b06a7a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar03.png new file mode 100644 index 0000000..1816161 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_01_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02.png new file mode 100644 index 0000000..a9b1683 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar01.png new file mode 100644 index 0000000..d3e7394 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar02.png new file mode 100644 index 0000000..0cf6a5c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar03.png new file mode 100644 index 0000000..ecf367f Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_02_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03.png new file mode 100644 index 0000000..7bef017 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar01.png new file mode 100644 index 0000000..7826cc4 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar02.png new file mode 100644 index 0000000..b7e3915 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar03.png new file mode 100644 index 0000000..d5bab14 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_03_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04.png new file mode 100644 index 0000000..147afb3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar01.png new file mode 100644 index 0000000..f8e3996 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar02.png new file mode 100644 index 0000000..80edbab Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar03.png new file mode 100644 index 0000000..b8a8255 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar04.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar04.png new file mode 100644 index 0000000..934daee Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar05.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar05.png new file mode 100644 index 0000000..bdb1871 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar05.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar06.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar06.png new file mode 100644 index 0000000..7d6becd Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Bar06.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue.png new file mode 100644 index 0000000..6b77340 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue_Clear.png new file mode 100644 index 0000000..ba7bd10 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Blue_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red.png new file mode 100644 index 0000000..c1c6a0e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red_Clear.png new file mode 100644 index 0000000..2487ce3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Red_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow.png new file mode 100644 index 0000000..9efe57d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow_Clear.png new file mode 100644 index 0000000..f0a23b5 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_04_Heart_Yellow_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_01.png new file mode 100644 index 0000000..8ec0dcb Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_02.png new file mode 100644 index 0000000..1ff5c3e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_03.png new file mode 100644 index 0000000..efa503e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Health_05_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue.png new file mode 100644 index 0000000..c723a0d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_1.png new file mode 100644 index 0000000..0559a5d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_2.png new file mode 100644 index 0000000..23601c7 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_3.png new file mode 100644 index 0000000..bfe8a38 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_4.png new file mode 100644 index 0000000..3e3afc6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Blue_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange.png new file mode 100644 index 0000000..3bf7513 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_1.png new file mode 100644 index 0000000..7a960fe Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_2.png new file mode 100644 index 0000000..f493f45 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_3.png new file mode 100644 index 0000000..39d0c64 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_4.png new file mode 100644 index 0000000..c11a477 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Orange_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red.png new file mode 100644 index 0000000..b218732 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_1.png new file mode 100644 index 0000000..874859c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_2.png new file mode 100644 index 0000000..a07d400 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_3.png new file mode 100644 index 0000000..ec626ce Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_4.png new file mode 100644 index 0000000..957c8d0 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Heart_Red_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts.png new file mode 100644 index 0000000..40fdddc Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_1.png new file mode 100644 index 0000000..d65ec2d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_2.png new file mode 100644 index 0000000..984f09e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_3.png new file mode 100644 index 0000000..4dbc216 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_4.png new file mode 100644 index 0000000..9099284 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_5.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_5.png new file mode 100644 index 0000000..03987ff Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Blue_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_1.png new file mode 100644 index 0000000..9d96a42 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_2.png new file mode 100644 index 0000000..7147409 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_3.png new file mode 100644 index 0000000..eebcf6d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_4.png new file mode 100644 index 0000000..0203554 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_5.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_5.png new file mode 100644 index 0000000..03987ff Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Red_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_1.png new file mode 100644 index 0000000..75e283f Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_2.png new file mode 100644 index 0000000..3893ab6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_3.png new file mode 100644 index 0000000..5c18573 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_4.png new file mode 100644 index 0000000..cebd850 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_5.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_5.png new file mode 100644 index 0000000..03987ff Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Hearts_Yellow_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory.png new file mode 100644 index 0000000..61be73c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_9Slices.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_9Slices.png new file mode 100644 index 0000000..60b10b7 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_9Slices.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_01.png new file mode 100644 index 0000000..f2f6b4a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_02.png new file mode 100644 index 0000000..b3de71c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_03.png new file mode 100644 index 0000000..2293e91 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_04.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_04.png new file mode 100644 index 0000000..8fd2830 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Example_04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_1.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_1.png new file mode 100644 index 0000000..5e9b605 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_10.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_10.png new file mode 100644 index 0000000..0426bf3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_10.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_2.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_2.png new file mode 100644 index 0000000..a6f9d72 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_3.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_3.png new file mode 100644 index 0000000..5d127da Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_4.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_4.png new file mode 100644 index 0000000..d8de4c6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_5.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_5.png new file mode 100644 index 0000000..33ad839 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_6.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_6.png new file mode 100644 index 0000000..f276615 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_6.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_7.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_7.png new file mode 100644 index 0000000..e0d0c59 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_7.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_8.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_8.png new file mode 100644 index 0000000..b1bbb59 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_8.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_9.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_9.png new file mode 100644 index 0000000..dd5b4d7 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Inventory_Slot_9.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings.png new file mode 100644 index 0000000..d7638c1 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar01.png new file mode 100644 index 0000000..9c8e5e6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar02.png new file mode 100644 index 0000000..01ed45c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar03.png new file mode 100644 index 0000000..3c25e18 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross01.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross01.png new file mode 100644 index 0000000..28cdbdc Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross02.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross02.png new file mode 100644 index 0000000..5a16f97 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross03.png b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross03.png new file mode 100644 index 0000000..3499599 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 2x/Settings_Cross03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01.png new file mode 100644 index 0000000..a3a25ae Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar01.png new file mode 100644 index 0000000..47ab898 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar02.png new file mode 100644 index 0000000..0a5e59d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar03.png new file mode 100644 index 0000000..a836c95 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_01_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02.png new file mode 100644 index 0000000..ed52db0 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar01.png new file mode 100644 index 0000000..31db6a4 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar02.png new file mode 100644 index 0000000..8a92027 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar03.png new file mode 100644 index 0000000..6274ef5 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_02_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03.png new file mode 100644 index 0000000..e62c971 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar01.png new file mode 100644 index 0000000..4d947c8 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar02.png new file mode 100644 index 0000000..a05161d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar03.png new file mode 100644 index 0000000..d8fb498 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_03_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04.png new file mode 100644 index 0000000..d96dc50 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar01.png new file mode 100644 index 0000000..6406030 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar02.png new file mode 100644 index 0000000..ca4f509 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar03.png new file mode 100644 index 0000000..7c52e47 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar04.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar04.png new file mode 100644 index 0000000..0d185d8 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar05.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar05.png new file mode 100644 index 0000000..30ef662 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar05.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar06.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar06.png new file mode 100644 index 0000000..70ce48f Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Bar06.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue.png new file mode 100644 index 0000000..8e2658f Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue_Clear.png new file mode 100644 index 0000000..21cf128 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Blue_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red.png new file mode 100644 index 0000000..0c95baf Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red_Clear.png new file mode 100644 index 0000000..93c462e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Red_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow.png new file mode 100644 index 0000000..2c61cf5 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow_Clear.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow_Clear.png new file mode 100644 index 0000000..5662103 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_04_Heart_Yellow_Clear.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_01.png new file mode 100644 index 0000000..70fc9ff Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_02.png new file mode 100644 index 0000000..21436a6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_03.png new file mode 100644 index 0000000..6070bdc Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Health_05_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue.png new file mode 100644 index 0000000..0d97e44 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_1.png new file mode 100644 index 0000000..e6b7bdf Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_2.png new file mode 100644 index 0000000..28654c4 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_3.png new file mode 100644 index 0000000..d846fa4 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_4.png new file mode 100644 index 0000000..f615832 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Blue_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange.png new file mode 100644 index 0000000..0dfbe36 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_1.png new file mode 100644 index 0000000..851666d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_2.png new file mode 100644 index 0000000..5508646 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_3.png new file mode 100644 index 0000000..eaf9046 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_4.png new file mode 100644 index 0000000..0ba0793 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Orange_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red.png new file mode 100644 index 0000000..2ccb166 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_1.png new file mode 100644 index 0000000..7a09d95 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_2.png new file mode 100644 index 0000000..a39ca33 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_3.png new file mode 100644 index 0000000..1bceb27 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_4.png new file mode 100644 index 0000000..2e7e12b Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Heart_Red_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts.png new file mode 100644 index 0000000..c7907fb Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_1.png new file mode 100644 index 0000000..8b78fa2 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_2.png new file mode 100644 index 0000000..208db9d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_3.png new file mode 100644 index 0000000..a1b5060 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_4.png new file mode 100644 index 0000000..bf3de11 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_5.png new file mode 100644 index 0000000..464c225 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Blue_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_1.png new file mode 100644 index 0000000..0b68781 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_2.png new file mode 100644 index 0000000..720d0b8 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_3.png new file mode 100644 index 0000000..ff3add3 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_4.png new file mode 100644 index 0000000..7b0f717 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_5.png new file mode 100644 index 0000000..464c225 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Red_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_1.png new file mode 100644 index 0000000..050405e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_2.png new file mode 100644 index 0000000..508491b Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_3.png new file mode 100644 index 0000000..a844d25 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_4.png new file mode 100644 index 0000000..8b0f707 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_5.png new file mode 100644 index 0000000..464c225 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Hearts_Yellow_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory.png new file mode 100644 index 0000000..20b95f9 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_9Slices.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_9Slices.png new file mode 100644 index 0000000..910730e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_9Slices.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_01.png new file mode 100644 index 0000000..d26b3ca Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_02.png new file mode 100644 index 0000000..7357b31 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_03.png new file mode 100644 index 0000000..8ca1e23 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_04.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_04.png new file mode 100644 index 0000000..02eb4aa Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Example_04.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_1.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_1.png new file mode 100644 index 0000000..4d1d1f1 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_1.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_10.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_10.png new file mode 100644 index 0000000..68de5de Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_10.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_2.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_2.png new file mode 100644 index 0000000..d94cf43 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_2.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_3.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_3.png new file mode 100644 index 0000000..5b89a4c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_3.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_4.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_4.png new file mode 100644 index 0000000..20a9bec Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_4.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_5.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_5.png new file mode 100644 index 0000000..6f99eb8 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_5.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_6.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_6.png new file mode 100644 index 0000000..d545a0d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_6.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_7.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_7.png new file mode 100644 index 0000000..6de658d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_7.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_8.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_8.png new file mode 100644 index 0000000..2d99d3c Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_8.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_9.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_9.png new file mode 100644 index 0000000..a60be77 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Inventory_Slot_9.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings.png new file mode 100644 index 0000000..62caade Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar01.png new file mode 100644 index 0000000..0878de6 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar02.png new file mode 100644 index 0000000..bfe7e4e Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar03.png new file mode 100644 index 0000000..0ebfa91 Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Bar03.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross01.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross01.png new file mode 100644 index 0000000..80c9eee Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross01.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross02.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross02.png new file mode 100644 index 0000000..109261a Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross02.png differ diff --git a/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross03.png b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross03.png new file mode 100644 index 0000000..abb3e3d Binary files /dev/null and b/public/Retro Inventory/Retro Inventory/Scaled 3x/Settings_Cross03.png differ diff --git a/assets/img/character/down/Character_Down.png b/public/TopDownCharacter/Character/Character_Down.png similarity index 100% rename from assets/img/character/down/Character_Down.png rename to public/TopDownCharacter/Character/Character_Down.png diff --git a/assets/img/character/down/Character_DownLeft.png b/public/TopDownCharacter/Character/Character_DownLeft.png similarity index 100% rename from assets/img/character/down/Character_DownLeft.png rename to public/TopDownCharacter/Character/Character_DownLeft.png diff --git a/assets/img/character/down/Character_DownRight.png b/public/TopDownCharacter/Character/Character_DownRight.png similarity index 100% rename from assets/img/character/down/Character_DownRight.png rename to public/TopDownCharacter/Character/Character_DownRight.png diff --git a/assets/img/character/left/Character_Left.png b/public/TopDownCharacter/Character/Character_Left.png similarity index 100% rename from assets/img/character/left/Character_Left.png rename to public/TopDownCharacter/Character/Character_Left.png diff --git a/assets/img/character/right/Character_Right.png b/public/TopDownCharacter/Character/Character_Right.png similarity index 100% rename from assets/img/character/right/Character_Right.png rename to public/TopDownCharacter/Character/Character_Right.png diff --git a/assets/img/character/Character_RollDown.png b/public/TopDownCharacter/Character/Character_RollDown.png similarity index 100% rename from assets/img/character/Character_RollDown.png rename to public/TopDownCharacter/Character/Character_RollDown.png diff --git a/assets/img/character/Character_RollDownLeft.png b/public/TopDownCharacter/Character/Character_RollDownLeft.png similarity index 100% rename from assets/img/character/Character_RollDownLeft.png rename to public/TopDownCharacter/Character/Character_RollDownLeft.png diff --git a/assets/img/character/Character_RollDownRight.png b/public/TopDownCharacter/Character/Character_RollDownRight.png similarity index 100% rename from assets/img/character/Character_RollDownRight.png rename to public/TopDownCharacter/Character/Character_RollDownRight.png diff --git a/assets/img/character/Character_RollLeft.png b/public/TopDownCharacter/Character/Character_RollLeft.png similarity index 100% rename from assets/img/character/Character_RollLeft.png rename to public/TopDownCharacter/Character/Character_RollLeft.png diff --git a/assets/img/character/Character_RollRight.png b/public/TopDownCharacter/Character/Character_RollRight.png similarity index 100% rename from assets/img/character/Character_RollRight.png rename to public/TopDownCharacter/Character/Character_RollRight.png diff --git a/assets/img/character/Character_RollUp.png b/public/TopDownCharacter/Character/Character_RollUp.png similarity index 100% rename from assets/img/character/Character_RollUp.png rename to public/TopDownCharacter/Character/Character_RollUp.png diff --git a/assets/img/character/Character_RollUpLeft.png b/public/TopDownCharacter/Character/Character_RollUpLeft.png similarity index 100% rename from assets/img/character/Character_RollUpLeft.png rename to public/TopDownCharacter/Character/Character_RollUpLeft.png diff --git a/assets/img/character/Character_RollUpRight.png b/public/TopDownCharacter/Character/Character_RollUpRight.png similarity index 100% rename from assets/img/character/Character_RollUpRight.png rename to public/TopDownCharacter/Character/Character_RollUpRight.png diff --git a/assets/img/character/Character_SlashDownLeft.png b/public/TopDownCharacter/Character/Character_SlashDownLeft.png similarity index 100% rename from assets/img/character/Character_SlashDownLeft.png rename to public/TopDownCharacter/Character/Character_SlashDownLeft.png diff --git a/assets/img/character/Character_SlashDownRight.png b/public/TopDownCharacter/Character/Character_SlashDownRight.png similarity index 100% rename from assets/img/character/Character_SlashDownRight.png rename to public/TopDownCharacter/Character/Character_SlashDownRight.png diff --git a/assets/img/character/Character_SlashUpLeft.png b/public/TopDownCharacter/Character/Character_SlashUpLeft.png similarity index 100% rename from assets/img/character/Character_SlashUpLeft.png rename to public/TopDownCharacter/Character/Character_SlashUpLeft.png diff --git a/assets/img/character/Character_SlashUpRight.png b/public/TopDownCharacter/Character/Character_SlashUpRight.png similarity index 100% rename from assets/img/character/Character_SlashUpRight.png rename to public/TopDownCharacter/Character/Character_SlashUpRight.png diff --git a/assets/img/character/up/Character_Up.png b/public/TopDownCharacter/Character/Character_Up.png similarity index 100% rename from assets/img/character/up/Character_Up.png rename to public/TopDownCharacter/Character/Character_Up.png diff --git a/assets/img/character/up/Character_UpLeft.png b/public/TopDownCharacter/Character/Character_UpLeft.png similarity index 100% rename from assets/img/character/up/Character_UpLeft.png rename to public/TopDownCharacter/Character/Character_UpLeft.png diff --git a/assets/img/character/up/Character_UpRight.png b/public/TopDownCharacter/Character/Character_UpRight.png similarity index 100% rename from assets/img/character/up/Character_UpRight.png rename to public/TopDownCharacter/Character/Character_UpRight.png diff --git a/assets/img/character/Weapon/Sword_DownLeft.png b/public/TopDownCharacter/Weapon/Sword_DownLeft.png similarity index 100% rename from assets/img/character/Weapon/Sword_DownLeft.png rename to public/TopDownCharacter/Weapon/Sword_DownLeft.png diff --git a/assets/img/character/Weapon/Sword_DownRight.png b/public/TopDownCharacter/Weapon/Sword_DownRight.png similarity index 100% rename from assets/img/character/Weapon/Sword_DownRight.png rename to public/TopDownCharacter/Weapon/Sword_DownRight.png diff --git a/assets/img/character/Weapon/Sword_UpLeft.png b/public/TopDownCharacter/Weapon/Sword_UpLeft.png similarity index 100% rename from assets/img/character/Weapon/Sword_UpLeft.png rename to public/TopDownCharacter/Weapon/Sword_UpLeft.png diff --git a/assets/img/character/Weapon/Sword_UpRight.png b/public/TopDownCharacter/Weapon/Sword_UpRight.png similarity index 100% rename from assets/img/character/Weapon/Sword_UpRight.png rename to public/TopDownCharacter/Weapon/Sword_UpRight.png diff --git a/src/engine/animation/Animation.ts b/src/engine/animation/Animation.ts new file mode 100644 index 0000000..a9fe81f --- /dev/null +++ b/src/engine/animation/Animation.ts @@ -0,0 +1,66 @@ +/** + * Animation - Données d'une animation sprite + * Contient les frames et les paramètres d'animation + */ + +import { Rect } from '../math/Rect'; + +/** + * Événement déclenché à une frame spécifique de l'animation + */ +export interface AnimationEvent { + frame: number; + event: string; + data?: any; +} + +export class Animation { + public readonly name: string; + public readonly frames: Rect[]; + public readonly frameDuration: number; // Durée de chaque frame en secondes + public readonly loop: boolean; + public readonly events: AnimationEvent[]; + + constructor( + name: string, + frames: Rect[], + frameDuration: number = 0.1, + loop: boolean = true, + events: AnimationEvent[] = [] + ) { + if (frames.length === 0) { + throw new Error('Animation doit avoir au moins une frame'); + } + + this.name = name; + this.frames = frames; + this.frameDuration = frameDuration; + this.loop = loop; + this.events = events; + } + + /** + * Durée totale de l'animation (en secondes) + */ + get duration(): number { + return this.frames.length * this.frameDuration; + } + + /** + * Nombre de frames + */ + get frameCount(): number { + return this.frames.length; + } + + /** + * Récupère la frame à l'index donné + */ + getFrame(index: number): Rect { + if (index < 0 || index >= this.frames.length) { + throw new Error(`Index de frame invalide: ${index}`); + } + return this.frames[index]!; + } +} + diff --git a/src/engine/assets/AssetLoader.ts b/src/engine/assets/AssetLoader.ts new file mode 100644 index 0000000..1431a23 --- /dev/null +++ b/src/engine/assets/AssetLoader.ts @@ -0,0 +1,224 @@ +/** + * AssetLoader - Système centralisé de chargement d'assets + * Gère le cache, le préchargement et la progression + */ + +import { Texture } from '../rendering/Texture'; + +export interface AssetManifest { + textures?: Array<{ name: string; url: string }>; + json?: Array<{ name: string; url: string }>; +} + +export interface LoadingProgress { + total: number; + loaded: number; + percentage: number; + current?: string; // Nom de l'asset en cours de chargement +} + +export type ProgressCallback = (progress: LoadingProgress) => void; + +export class AssetLoader { + private _gl: WebGLRenderingContext | WebGL2RenderingContext; + private _textures: Map = new Map(); + private _json: Map = new Map(); + + private _loadingPromises: Map> = new Map(); + + constructor(gl: WebGLRenderingContext | WebGL2RenderingContext) { + this._gl = gl; + } + + /** + * Charge une texture et la met en cache + */ + async loadTexture(name: string, url: string): Promise { + // Vérifie si déjà chargée + if (this._textures.has(name)) { + return this._textures.get(name)!; + } + + // Vérifie si en cours de chargement + if (this._loadingPromises.has(`texture:${name}`)) { + return await this._loadingPromises.get(`texture:${name}`)! as Texture; + } + + // Lance le chargement + const promise = Texture.loadFromURL(this._gl, url).then((texture) => { + this._textures.set(name, texture); + this._loadingPromises.delete(`texture:${name}`); + return texture; + }).catch((error) => { + this._loadingPromises.delete(`texture:${name}`); + throw new Error(`Échec du chargement de la texture "${name}": ${error}`); + }); + + this._loadingPromises.set(`texture:${name}`, promise); + return await promise; + } + + /** + * Charge un fichier JSON et le met en cache + */ + async loadJSON(name: string, url: string): Promise { + // Vérifie si déjà chargé + if (this._json.has(name)) { + return this._json.get(name)! as T; + } + + // Vérifie si en cours de chargement + if (this._loadingPromises.has(`json:${name}`)) { + return await this._loadingPromises.get(`json:${name}`)! as T; + } + + // Lance le chargement + const promise = fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); + }) + .then((data: T) => { + this._json.set(name, data); + this._loadingPromises.delete(`json:${name}`); + return data; + }) + .catch((error) => { + this._loadingPromises.delete(`json:${name}`); + throw new Error(`Échec du chargement du JSON "${name}": ${error}`); + }); + + this._loadingPromises.set(`json:${name}`, promise); + return await promise; + } + + /** + * Précharge plusieurs assets avec progression + */ + async loadAll(manifest: AssetManifest, onProgress?: ProgressCallback): Promise { + const allAssets: Array<{ name: string; type: 'texture' | 'json'; url: string }> = []; + + if (manifest.textures) { + for (const tex of manifest.textures) { + allAssets.push({ name: tex.name, type: 'texture', url: tex.url }); + } + } + + if (manifest.json) { + for (const json of manifest.json) { + allAssets.push({ name: json.name, type: 'json', url: json.url }); + } + } + + const total = allAssets.length; + let loaded = 0; + + // Lance tous les chargements en parallèle + const promises = allAssets.map(async (asset) => { + try { + if (asset.type === 'texture') { + await this.loadTexture(asset.name, asset.url); + } else { + await this.loadJSON(asset.name, asset.url); + } + + loaded++; + if (onProgress) { + onProgress({ + total, + loaded, + percentage: loaded / total, + current: asset.name + }); + } + } catch (error) { + console.error(`Erreur lors du chargement de ${asset.name}:`, error); + // Continue quand même + loaded++; + if (onProgress) { + onProgress({ + total, + loaded, + percentage: loaded / total, + current: asset.name + }); + } + } + }); + + await Promise.all(promises); + } + + /** + * Récupère une texture par son nom + */ + getTexture(name: string): Texture | null { + return this._textures.get(name) || null; + } + + /** + * Récupère des données JSON par nom + */ + getJSON(name: string): T | null { + return (this._json.get(name) as T) || null; + } + + /** + * Vérifie si une texture est chargée + */ + hasTexture(name: string): boolean { + return this._textures.has(name); + } + + /** + * Vérifie si un JSON est chargé + */ + hasJSON(name: string): boolean { + return this._json.has(name); + } + + /** + * Libère une texture de la mémoire + */ + unloadTexture(name: string): void { + const texture = this._textures.get(name); + if (texture) { + texture.dispose(); + this._textures.delete(name); + } + } + + /** + * Libère toutes les ressources + */ + dispose(): void { + // Libère toutes les textures + for (const texture of this._textures.values()) { + texture.dispose(); + } + this._textures.clear(); + + // Nettoie les JSON + this._json.clear(); + + // Nettoie les promesses en cours + this._loadingPromises.clear(); + } + + /** + * Liste toutes les textures chargées + */ + get loadedTextures(): string[] { + return Array.from(this._textures.keys()); + } + + /** + * Liste tous les JSON chargés + */ + get loadedJSONs(): string[] { + return Array.from(this._json.keys()); + } +} + diff --git a/src/engine/assets/SpriteSheet.ts b/src/engine/assets/SpriteSheet.ts new file mode 100644 index 0000000..726517c --- /dev/null +++ b/src/engine/assets/SpriteSheet.ts @@ -0,0 +1,153 @@ +/** + * SpriteSheet - Gestion d'un spritesheet (image avec plusieurs sprites) + * Découpe une texture en plusieurs sprites avec dimensions uniformes + */ + +import { Texture } from '../rendering/Texture'; +import { Rect } from '../math/Rect'; + +export interface SpriteInfo { + name: string; + x: number; + y: number; + width: number; + height: number; +} + +export class SpriteSheet { + private _texture: Texture; + private _sprites: Map = new Map(); + private _spriteWidth: number; + private _spriteHeight: number; + private _columns: number; + private _rows: number; + + /** + * Crée un SpriteSheet avec une grille uniforme + * @param texture Texture source + * @param spriteWidth Largeur d'un sprite + * @param spriteHeight Hauteur d'un sprite + */ + constructor(texture: Texture, spriteWidth: number, spriteHeight: number) { + this._texture = texture; + this._spriteWidth = spriteWidth; + this._spriteHeight = spriteHeight; + this._columns = Math.floor(texture.width / spriteWidth); + this._rows = Math.floor(texture.height / spriteHeight); + + // Crée automatiquement les sprites de la grille + this._createGridSprites(); + } + + /** + * Crée les sprites à partir d'une configuration JSON + * @param sprites Liste des sprites avec leurs positions + */ + createSprites(sprites: SpriteInfo[]): void { + for (const sprite of sprites) { + const rect = new Rect( + sprite.x, + sprite.y, + sprite.width, + sprite.height + ); + this._sprites.set(sprite.name, rect); + } + } + + /** + * Récupère le Rect d'un sprite par son nom + */ + getSprite(name: string): Rect | null { + return this._sprites.get(name) || null; + } + + /** + * Récupère le Rect d'un sprite par son index dans la grille (0-based) + */ + getSpriteByIndex(index: number): Rect | null { + const column = index % this._columns; + const row = Math.floor(index / this._columns); + return this.getSpriteByGridPosition(column, row); + } + + /** + * Récupère le Rect d'un sprite par sa position dans la grille (0-based) + */ + getSpriteByGridPosition(column: number, row: number): Rect | null { + if (column < 0 || column >= this._columns || row < 0 || row >= this._rows) { + return null; + } + + const x = column * this._spriteWidth; + const y = row * this._spriteHeight; + return new Rect(x, y, this._spriteWidth, this._spriteHeight); + } + + /** + * Texture source + */ + get texture(): Texture { + return this._texture; + } + + /** + * Largeur d'un sprite + */ + get spriteWidth(): number { + return this._spriteWidth; + } + + /** + * Hauteur d'un sprite + */ + get spriteHeight(): number { + return this._spriteHeight; + } + + /** + * Nombre de colonnes dans la grille + */ + get columns(): number { + return this._columns; + } + + /** + * Nombre de lignes dans la grille + */ + get rows(): number { + return this._rows; + } + + /** + * Nombre total de sprites dans la grille + */ + get totalSprites(): number { + return this._columns * this._rows; + } + + /** + * Liste tous les noms de sprites + */ + get spriteNames(): string[] { + return Array.from(this._sprites.keys()); + } + + /** + * Crée automatiquement les sprites de la grille uniforme + */ + private _createGridSprites(): void { + for (let row = 0; row < this._rows; row++) { + for (let col = 0; col < this._columns; col++) { + const x = col * this._spriteWidth; + const y = row * this._spriteHeight; + const rect = new Rect(x, y, this._spriteWidth, this._spriteHeight); + + // Nom par défaut : "sprite_0_0", "sprite_0_1", etc. + const name = `sprite_${row}_${col}`; + this._sprites.set(name, rect); + } + } + } +} + diff --git a/src/engine/assets/TextureAtlas.ts b/src/engine/assets/TextureAtlas.ts new file mode 100644 index 0000000..53ae2ba --- /dev/null +++ b/src/engine/assets/TextureAtlas.ts @@ -0,0 +1,121 @@ +/** + * TextureAtlas - Gestion d'un atlas de textures + * Utilise un fichier JSON pour définir les régions de chaque sprite dans l'atlas + */ + +import { Texture } from '../rendering/Texture'; +import { Rect } from '../math/Rect'; + +export interface AtlasFrame { + frame: { x: number; y: number; w: number; h: number }; + rotated?: boolean; + trimmed?: boolean; + spriteSourceSize?: { x: number; y: number; w: number; h: number }; + sourceSize?: { w: number; h: number }; + pivot?: { x: number; y: number }; +} + +export interface AtlasJSON { + frames: Record; + meta?: { + image?: string; + size?: { w: number; h: number }; + format?: string; + scale?: number; + }; +} + +export class TextureAtlas { + private _texture: Texture; + private _frames: Map = new Map(); + private _atlasData: AtlasJSON; + + /** + * Crée un TextureAtlas à partir d'une texture et d'un JSON d'atlas + * @param texture Texture source de l'atlas + * @param atlasData Données JSON de l'atlas (format compatible TexturePacker, etc.) + */ + constructor(texture: Texture, atlasData: AtlasJSON) { + this._texture = texture; + this._atlasData = atlasData; + this._parseAtlasData(); + } + + /** + * Récupère le Rect d'un sprite par son nom + */ + getSprite(name: string): Rect | null { + return this._frames.get(name) || null; + } + + /** + * Vérifie si un sprite existe dans l'atlas + */ + hasSprite(name: string): boolean { + return this._frames.has(name); + } + + /** + * Texture source de l'atlas + */ + get texture(): Texture { + return this._texture; + } + + /** + * Données JSON de l'atlas + */ + get atlasData(): AtlasJSON { + return this._atlasData; + } + + /** + * Liste tous les noms de sprites dans l'atlas + */ + get spriteNames(): string[] { + return Array.from(this._frames.keys()); + } + + /** + * Nombre de sprites dans l'atlas + */ + get spriteCount(): number { + return this._frames.size; + } + + /** + * Parse les données JSON de l'atlas + */ + private _parseAtlasData(): void { + if (!this._atlasData.frames) { + console.warn('Atlas JSON ne contient pas de "frames"'); + return; + } + + for (const [name, frameData] of Object.entries(this._atlasData.frames)) { + const frame = frameData.frame; + const rect = new Rect(frame.x, frame.y, frame.w, frame.h); + this._frames.set(name, rect); + } + } + + /** + * Crée un TextureAtlas depuis un AssetLoader + * Charge automatiquement la texture et le JSON + * @param assetLoader AssetLoader avec les assets chargés + * @param textureName Nom de la texture dans l'AssetLoader + * @param jsonName Nom du JSON dans l'AssetLoader + */ + static fromAssetLoader(assetLoader: { getTexture(name: string): Texture | null; getJSON(name: string): any }, textureName: string, jsonName: string): TextureAtlas | null { + const texture = assetLoader.getTexture(textureName); + const json = assetLoader.getJSON(jsonName) as AtlasJSON | null; + + if (!texture || !json) { + console.error(`Impossible de créer TextureAtlas: texture "${textureName}" ou JSON "${jsonName}" manquant`); + return null; + } + + return new TextureAtlas(texture, json); + } +} + diff --git a/src/engine/assets/index.ts b/src/engine/assets/index.ts new file mode 100644 index 0000000..d8161a9 --- /dev/null +++ b/src/engine/assets/index.ts @@ -0,0 +1,8 @@ +/** + * Exports du système Asset Management + */ + +export { AssetLoader, type AssetManifest, type LoadingProgress, type ProgressCallback } from './AssetLoader'; +export { SpriteSheet, type SpriteInfo } from './SpriteSheet'; +export { TextureAtlas, type AtlasJSON, type AtlasFrame } from './TextureAtlas'; + diff --git a/src/engine/components/Animator.ts b/src/engine/components/Animator.ts new file mode 100644 index 0000000..51d5a6e --- /dev/null +++ b/src/engine/components/Animator.ts @@ -0,0 +1,196 @@ +/** + * Animator - Gère les animations de sprite + * Anime un SpriteRenderer en changeant son sourceRect + */ + +import { Component } from '../entities/Component'; +import { SpriteRenderer } from './SpriteRenderer'; +import { Animation } from '../animation/Animation'; + +export class Animator extends Component { + private _animations: Map = new Map(); + private _currentAnimation: Animation | null = null; + private _currentFrame: number = 0; + private _frameTime: number = 0; // Temps écoulé sur la frame actuelle + private _isPlaying: boolean = false; + private _speed: number = 1.0; // Multiplicateur de vitesse (1 = normal, 2 = 2× plus rapide) + private _spriteRenderer: SpriteRenderer | null = null; + + /** + * Animation actuellement en cours + */ + get currentAnimation(): Animation | null { + return this._currentAnimation; + } + + /** + * Frame actuelle de l'animation + */ + get currentFrame(): number { + return this._currentFrame; + } + + /** + * Indique si l'animation est en cours de lecture + */ + get isPlaying(): boolean { + return this._isPlaying; + } + + /** + * Vitesse de lecture (1 = normal, 2 = 2× plus rapide, 0.5 = 2× plus lent) + */ + get speed(): number { + return this._speed; + } + + set speed(value: number) { + this._speed = Math.max(0, value); + } + + /** + * Initialise l'Animator et trouve le SpriteRenderer + */ + awake(): void { + // Cherche le SpriteRenderer sur le même GameObject + this._spriteRenderer = this.getComponent(SpriteRenderer); + + if (!this._spriteRenderer) { + console.warn(`Animator sur "${this.gameObject.name}" ne trouve pas de SpriteRenderer`); + } + } + + /** + * Ajoute une animation à l'animator + */ + addAnimation(name: string, animation: Animation): void { + this._animations.set(name, animation); + } + + /** + * Joue une animation + * @param name Nom de l'animation + * @param restart Si true, redémarre même si c'est déjà l'animation en cours + */ + play(name: string, restart: boolean = false): void { + const animation = this._animations.get(name); + + if (!animation) { + console.warn(`Animation "${name}" introuvable`); + return; + } + + // Si c'est la même animation et qu'on ne veut pas redémarrer, on continue + if (this._currentAnimation === animation && !restart && this._isPlaying) { + return; + } + + this._currentAnimation = animation; + this._currentFrame = 0; + this._frameTime = 0; + this._isPlaying = true; + + // Met à jour le sprite immédiatement + this._updateSpriteFrame(); + } + + /** + * Arrête l'animation + */ + stop(): void { + this._isPlaying = false; + } + + /** + * Met en pause + */ + pause(): void { + this._isPlaying = false; + } + + /** + * Reprend l'animation + */ + resume(): void { + if (this._currentAnimation) { + this._isPlaying = true; + } + } + + /** + * Met à jour l'animation + */ + update(deltaTime: number): void { + if (!this._isPlaying || !this._currentAnimation || !this._spriteRenderer) { + return; + } + + // Avance le temps + this._frameTime += deltaTime * this._speed; + const frameDuration = this._currentAnimation.frameDuration; + + // Passe à la frame suivante si nécessaire + while (this._frameTime >= frameDuration) { + this._frameTime -= frameDuration; + this._currentFrame++; + + // Vérifie si on est à la fin de l'animation + if (this._currentFrame >= this._currentAnimation.frameCount) { + if (this._currentAnimation.loop) { + // Boucle : retourne au début + this._currentFrame = 0; + } else { + // One-shot : reste sur la dernière frame et arrête + this._currentFrame = this._currentAnimation.frameCount - 1; + this._isPlaying = false; + break; + } + } + + // Déclenche les événements à cette frame + this._triggerEvents(this._currentFrame); + } + + // Met à jour le sprite + this._updateSpriteFrame(); + } + + /** + * Met à jour le sourceRect du SpriteRenderer avec la frame actuelle + */ + private _updateSpriteFrame(): void { + if (!this._currentAnimation || !this._spriteRenderer) { + return; + } + + const frame = this._currentAnimation.getFrame(this._currentFrame); + this._spriteRenderer.sourceRect = frame; + } + + /** + * Déclenche les événements à une frame spécifique + */ + private _triggerEvents(frame: number): void { + if (!this._currentAnimation) { + return; + } + + for (const event of this._currentAnimation.events) { + if (event.frame === frame) { + // Dispatch l'événement (pour l'instant juste un log, peut être étendu avec EventBus) + console.log(`Animation event: ${event.event} at frame ${frame}`, event.data); + // TODO: Intégrer avec EventBus quand il sera créé + } + } + } + + /** + * Nettoie le composant + */ + onDestroy(): void { + this._animations.clear(); + this._currentAnimation = null; + this._spriteRenderer = null; + } +} + diff --git a/src/engine/components/SpriteRenderer.ts b/src/engine/components/SpriteRenderer.ts new file mode 100644 index 0000000..b6380cf --- /dev/null +++ b/src/engine/components/SpriteRenderer.ts @@ -0,0 +1,159 @@ +/** + * SpriteRenderer - Composant pour afficher un sprite 2D + * Utilise SpriteBatch pour le rendu optimisé + */ + +import { Component } from '../entities/Component'; +import { Texture } from '../rendering/Texture'; +import { Rect } from '../math/Rect'; +import { Color } from '../math/Color'; +import { Vector2 } from '../math/Vector2'; +import { SpriteBatch } from '../rendering/SpriteBatch'; + +export class SpriteRenderer extends Component { + private _texture: Texture | null = null; + private _sourceRect: Rect | null = null; + private _tint: Color = Color.white; + private _flipX: boolean = false; + private _flipY: boolean = false; + private _layer: number = 0; + private _pivot: Vector2 = new Vector2(0.5, 0.5); // 0.5, 0.5 = centre + + /** + * Texture à afficher + */ + get texture(): Texture | null { + return this._texture; + } + + set texture(value: Texture | null) { + this._texture = value; + // Si sourceRect n'est pas défini, utilise toute la texture + if (value && !this._sourceRect) { + this._sourceRect = new Rect(0, 0, value.width, value.height); + } + } + + /** + * Quelle partie de la texture afficher (pour spritesheets) + */ + get sourceRect(): Rect | null { + return this._sourceRect; + } + + set sourceRect(value: Rect | null) { + this._sourceRect = value; + } + + /** + * Couleur de teinte (blanc = normal, modifie les couleurs du sprite) + */ + get tint(): Color { + return this._tint; + } + + set tint(value: Color) { + this._tint = value; + } + + /** + * Miroir horizontal + */ + get flipX(): boolean { + return this._flipX; + } + + set flipX(value: boolean) { + this._flipX = value; + } + + /** + * Miroir vertical + */ + get flipY(): boolean { + return this._flipY; + } + + set flipY(value: boolean) { + this._flipY = value; + } + + /** + * Layer de rendu (z-index pour le tri) + */ + get layer(): number { + return this._layer; + } + + set layer(value: number) { + this._layer = value; + } + + /** + * Point d'ancrage (0, 0 = haut-gauche, 0.5, 0.5 = centre, 1, 1 = bas-droite) + */ + get pivot(): Vector2 { + return this._pivot; + } + + set pivot(value: Vector2) { + this._pivot = value; + } + + /** + * Définit le sprite (texture + sourceRect en une seule fois) + */ + setSprite(texture: Texture, sourceRect?: Rect): void { + this._texture = texture; + if (sourceRect) { + this._sourceRect = sourceRect; + } else { + this._sourceRect = new Rect(0, 0, texture.width, texture.height); + } + } + + /** + * Rend le sprite via SpriteBatch + * @internal - Appelé par le système de rendu + */ + render(spriteBatch: SpriteBatch): void { + if (!this._texture || !this.gameObject.active || !this.enabled) { + return; + } + + const transform = this.transform; + const position = transform.position; + const rotation = transform.rotation * Math.PI / 180; // Convertit degrés → radians + const scale = transform.scale; + + // Calcule le sourceRect effectif (avec flip) + let sourceRect = this._sourceRect; + if (!sourceRect) { + sourceRect = new Rect(0, 0, this._texture.width, this._texture.height); + } + + // Gère le flip en inversant sourceRect si nécessaire + // Note: Le flip peut aussi être géré dans le shader ou en ajustant les UVs + // Pour l'instant, on laisse SpriteBatch gérer avec les matrices de transformation + + spriteBatch.draw( + this._texture, + position, + sourceRect, + this._tint, + rotation, + scale, + this._pivot + ); + } + + /** + * Nettoie le composant + */ + onDestroy(): void { + // Les textures sont gérées par le cache du renderer, pas besoin de les libérer ici + this._texture = null; + this._sourceRect = null; + } +} + diff --git a/src/engine/core/Engine.ts b/src/engine/core/Engine.ts new file mode 100644 index 0000000..d383c82 --- /dev/null +++ b/src/engine/core/Engine.ts @@ -0,0 +1,401 @@ +/** + * Engine - Orchestrateur principal + * Point d'entrée, initialise et coordonne tous les systèmes + */ + +import { GLContext } from '../webgl/GLContext'; +import { WebGLRenderer } from '../rendering/WebGLRenderer'; +import { SceneManager } from './SceneManager'; +import { Scene } from './Scene'; +import { GameLoop } from './GameLoop'; +import { InputManager } from '../input/InputManager'; +import { PhysicsSystem } from '../physics/PhysicsSystem'; +import { AssetLoader } from '../assets/AssetLoader'; +import { DebugPanel } from '../debug/DebugPanel'; + +export class Engine { + private _canvas: HTMLCanvasElement; + private _glContext: GLContext; + private _renderer: WebGLRenderer; + private _sceneManager: SceneManager; + private _inputManager: InputManager; + private _physicsSystem: PhysicsSystem; + private _assetLoader: AssetLoader; + private _gameLoop: GameLoop; + private _debugPanel: DebugPanel; + private _isRunning: boolean = false; + private _isPaused: boolean = false; + private _frameCount: number = 0; + private _lastFpsUpdate: number = 0; + private _currentFps: number = 0; + + constructor(canvasId: string | HTMLCanvasElement, width: number = 800, height: number = 600) { + // Récupère ou crée le canvas + if (typeof canvasId === 'string') { + const canvas = document.getElementById(canvasId) as HTMLCanvasElement; + if (!canvas) { + throw new Error(`Canvas "${canvasId}" introuvable`); + } + this._canvas = canvas; + } else { + this._canvas = canvasId; + } + + // Initialise GLContext + this._glContext = new GLContext(); + if (!this._glContext.initialize(this._canvas)) { + throw new Error('Échec de l\'initialisation WebGL'); + } + + this._glContext.resize(width, height); + + // Crée le renderer + this._renderer = new WebGLRenderer(this._glContext); + + // Crée le SceneManager + this._sceneManager = new SceneManager(); + + // Crée l'InputManager + this._inputManager = new InputManager(); + + // Crée le PhysicsSystem + this._physicsSystem = new PhysicsSystem(); + + // Crée l'AssetLoader + this._assetLoader = new AssetLoader(this._glContext.gl); + + // Crée le DebugPanel + this._debugPanel = new DebugPanel(); + + // Crée la GameLoop + this._gameLoop = new GameLoop(); + this._setupGameLoop(); + } + + /** + * Configure la GameLoop avec les callbacks + */ + private _setupGameLoop(): void { + // Update (logique de jeu) + this._gameLoop.onUpdate((deltaTime: number, currentTime: number) => { + if (!this._isPaused) { + // Update la scène EN PREMIER pour que les composants puissent détecter Down + this._sceneManager.update(deltaTime); + + // Update Debug Panel + this._updateDebugPanel(deltaTime, currentTime); + } + + // Update InputManager EN DERNIER pour préparer les états pour la prochaine frame + // (Down -> Held, Released -> Up) + this._inputManager.update(); + }); + + // Fixed Update (physique) + this._gameLoop.onFixedUpdate((_fixedDeltaTime: number) => { + if (!this._isPaused) { + const activeScene = this._sceneManager.getActiveScene(); + if (activeScene) { + this._physicsSystem.update(activeScene); + } + } + }); + + // Render + this._gameLoop.onRender(() => { + if (!this._isPaused) { + this._sceneManager.render(this._renderer); + } + }); + } + + /** + * Met à jour le panel de debug avec les informations courantes + */ + private _updateDebugPanel(deltaTime: number, currentTime: number): void { + // Calcule le FPS + this._frameCount++; + if (currentTime - this._lastFpsUpdate >= 1000) { + this._currentFps = this._frameCount; + this._frameCount = 0; + this._lastFpsUpdate = currentTime; + } + + const activeScene = this._sceneManager.getActiveScene(); + if (!activeScene) return; + + // Collecte les informations de base + const debugInfo: any = { + fps: this._currentFps, + deltaTime: deltaTime, + entityCount: activeScene.gameObjects.length, + cameraPosition: { + x: activeScene.camera.position.x, + y: activeScene.camera.position.y + }, + mouseScreenPos: { + x: this._inputManager.getMousePosition().x, + y: this._inputManager.getMousePosition().y + }, + mouseWorldPos: { + x: this._inputManager.getMouseWorldPosition(activeScene.camera).x, + y: this._inputManager.getMouseWorldPosition(activeScene.camera).y + } + }; + + // Cherche le joueur dans la scène + const player = activeScene.findGameObject('Player'); + if (player) { + debugInfo.playerPosition = { + x: player.transform.position.x, + y: player.transform.position.y + }; + + // Récupère les composants par nom de classe + const components = player.getComponents(); + + // Cherche Rigidbody2D + const rigidbody = components.find(c => c.constructor.name === 'Rigidbody2D'); + if (rigidbody && 'velocity' in rigidbody) { + debugInfo.playerVelocity = { + x: (rigidbody as any).velocity.x, + y: (rigidbody as any).velocity.y + }; + } + + // Cherche PlayerController + const controller = components.find(c => c.constructor.name === 'PlayerController'); + if (controller) { + // Utilise les getters publics + if ('direction' in controller) { + debugInfo.playerDirection = (controller as any).direction; + } + + // État du joueur (priorité: attacking > dashing > walking > idle) + const states = []; + if ('isPlayerAttacking' in controller && (controller as any).isPlayerAttacking) { + states.push('attacking'); + } else if ('isPlayerDashing' in controller && (controller as any).isPlayerDashing) { + states.push('dashing'); + } else if ('isPlayerMoving' in controller && (controller as any).isPlayerMoving) { + states.push('walking'); + } + debugInfo.playerState = states.length > 0 ? states.join(', ') : 'idle'; + } + + // Cherche HealthComponent + const healthComponent = components.find(c => c.constructor.name === 'HealthComponent'); + if (healthComponent) { + if ('health' in healthComponent && 'maxHealth' in healthComponent) { + debugInfo.playerHealth = { + current: (healthComponent as any).health, + max: (healthComponent as any).maxHealth + }; + } + } + } + + // Compte les colliders actifs + let activeColliders = 0; + activeScene.gameObjects.forEach(go => { + if (go.active) { + const colliders = go.getComponents().filter(c => + c.constructor.name.includes('Collider') + ); + activeColliders += colliders.length; + } + }); + debugInfo.activeColliders = activeColliders; + + this._debugPanel.setInfo(debugInfo); + this._debugPanel.update(currentTime); + } + + /** + * Initialise le moteur (charge les ressources, etc.) + */ + async initialize(): Promise { + // Initialise le renderer + if (!this._renderer.initialize()) { + console.error('Échec de l\'initialisation du renderer'); + return false; + } + + // Initialise l'InputManager + this._inputManager.initialize(this._canvas); + + console.log('Engine initialisé'); + return true; + } + + /** + * Démarre le moteur + */ + start(): void { + if (this._isRunning) { + console.warn('Engine déjà en cours d\'exécution'); + return; + } + + this._isRunning = true; + this._isPaused = false; + this._gameLoop.start(); + console.log('Engine démarré'); + } + + /** + * Arrête le moteur + */ + stop(): void { + if (!this._isRunning) { + return; + } + + this._isRunning = false; + this._gameLoop.stop(); + console.log('Engine arrêté'); + } + + /** + * Met en pause + */ + pause(): void { + if (!this._isRunning || this._isPaused) { + return; + } + + this._isPaused = true; + console.log('Engine en pause'); + } + + /** + * Reprend + */ + resume(): void { + if (!this._isRunning || !this._isPaused) { + return; + } + + this._isPaused = false; + console.log('Engine repris'); + } + + /** + * Charge une scène + */ + loadScene(name: string): boolean { + return this._sceneManager.loadScene(name); + } + + /** + * Enregistre une scène + */ + registerScene(name: string, scene: Scene): void { + this._sceneManager.registerScene(name, scene); + } + + /** + * Récupère la scène active + */ + getActiveScene(): Scene | null { + return this._sceneManager.getActiveScene(); + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this._glContext.resize(width, height); + this._renderer.resize(width, height); + + // Met à jour la caméra de la scène active + const activeScene = this._sceneManager.getActiveScene(); + if (activeScene) { + activeScene.camera.resize(width, height); + } + } + + /** + * Canvas HTML + */ + get canvas(): HTMLCanvasElement { + return this._canvas; + } + + /** + * Renderer + */ + get renderer(): WebGLRenderer { + return this._renderer; + } + + /** + * SceneManager + */ + get sceneManager(): SceneManager { + return this._sceneManager; + } + + /** + * InputManager + */ + get input(): InputManager { + return this._inputManager; + } + + /** + * PhysicsSystem + */ + get physics(): PhysicsSystem { + return this._physicsSystem; + } + + /** + * AssetLoader + */ + get assets(): AssetLoader { + return this._assetLoader; + } + + /** + * DebugPanel + */ + get debug(): DebugPanel { + return this._debugPanel; + } + + /** + * GameLoop + */ + get gameLoop(): GameLoop { + return this._gameLoop; + } + + /** + * État du moteur + */ + get isRunning(): boolean { + return this._isRunning; + } + + /** + * État de pause + */ + get isPaused(): boolean { + return this._isPaused; + } + + /** + * Nettoie toutes les ressources + */ + dispose(): void { + this.stop(); + this._inputManager.dispose(); + this._assetLoader.dispose(); + this._sceneManager.dispose(); + this._renderer.dispose(); + this._debugPanel.dispose(); + // GLContext se nettoie automatiquement + } +} + diff --git a/src/engine/core/GameLoop.ts b/src/engine/core/GameLoop.ts new file mode 100644 index 0000000..d8e9aeb --- /dev/null +++ b/src/engine/core/GameLoop.ts @@ -0,0 +1,152 @@ +/** + * GameLoop - Boucle de jeu + * Cœur battant du moteur, appelle update() et render() en boucle + */ + +import { Time } from './Time'; + +export type UpdateCallback = (deltaTime: number, currentTime: number) => void; +export type RenderCallback = () => void; +export type FixedUpdateCallback = (fixedDeltaTime: number) => void; + +export class GameLoop { + private _isRunning: boolean = false; + private _lastTime: number = 0; + private _targetFPS: number = 60; + private _targetFrameTime: number = 1000 / 60; // 16.67ms pour 60 FPS + + // Fixed timestep pour la physique + private _fixedDeltaTime: number = 1 / 60; // 60 FPS fixe pour la physique + private _accumulator: number = 0; + + // Callbacks + private _updateCallback: UpdateCallback | null = null; + private _fixedUpdateCallback: FixedUpdateCallback | null = null; + private _renderCallback: RenderCallback | null = null; + + /** + * État de la boucle + */ + get isRunning(): boolean { + return this._isRunning; + } + + /** + * FPS cible (optionnel, pour limiter) + */ + get targetFPS(): number { + return this._targetFPS; + } + + set targetFPS(value: number) { + this._targetFPS = value; + this._targetFrameTime = 1000 / value; + } + + /** + * DeltaTime fixe pour la physique (en secondes) + */ + get fixedDeltaTime(): number { + return this._fixedDeltaTime; + } + + set fixedDeltaTime(value: number) { + this._fixedDeltaTime = value; + } + + /** + * Définit le callback pour la logique de jeu (update) + */ + onUpdate(callback: UpdateCallback): void { + this._updateCallback = callback; + } + + /** + * Définit le callback pour la physique (fixedUpdate) + */ + onFixedUpdate(callback: FixedUpdateCallback): void { + this._fixedUpdateCallback = callback; + } + + /** + * Définit le callback pour le rendu + */ + onRender(callback: RenderCallback): void { + this._renderCallback = callback; + } + + /** + * Démarre la boucle de jeu + */ + start(): void { + if (this._isRunning) { + console.warn('GameLoop déjà en cours d\'exécution'); + return; + } + + this._isRunning = true; + this._lastTime = performance.now(); + Time.reset(); + + this._loop(); + } + + /** + * Arrête la boucle de jeu + */ + stop(): void { + this._isRunning = false; + console.log('GameLoop arrêté'); + } + + /** + * Boucle principale + */ + private _loop = (): void => { + if (!this._isRunning) { + return; + } + + const currentTime = performance.now(); + const deltaTimeMs = currentTime - this._lastTime; + const deltaTimeSeconds = deltaTimeMs / 1000; + + // Limite les FPS si nécessaire (capping) + if (deltaTimeMs < this._targetFrameTime) { + requestAnimationFrame(this._loop); + return; + } + + // Met à jour Time + Time.update(deltaTimeSeconds); + + // Fixed timestep pour la physique + this._accumulator += deltaTimeSeconds; + + // Exécute plusieurs fixedUpdates si nécessaire + const maxFixedSteps = 5; // Limite pour éviter le spiral of death + let fixedSteps = 0; + + while (this._accumulator >= this._fixedDeltaTime && fixedSteps < maxFixedSteps) { + if (this._fixedUpdateCallback) { + this._fixedUpdateCallback(this._fixedDeltaTime); + } + this._accumulator -= this._fixedDeltaTime; + fixedSteps++; + } + + // Update normal (logique de jeu) + if (this._updateCallback) { + this._updateCallback(Time.deltaTime, currentTime); + } + + // Render + if (this._renderCallback) { + this._renderCallback(); + } + + this._lastTime = currentTime; + requestAnimationFrame(this._loop); + }; +} + diff --git a/src/engine/core/Scene.ts b/src/engine/core/Scene.ts new file mode 100644 index 0000000..31afdd7 --- /dev/null +++ b/src/engine/core/Scene.ts @@ -0,0 +1,226 @@ +/** + * Scene - Scène de jeu + * Contient tous les GameObjects d'un niveau/écran + */ + +import { WebGLRenderer } from '../rendering/WebGLRenderer'; +import { Camera } from '../rendering/Camera'; +import { GameObject } from '../entities/GameObject'; +import { SpriteRenderer } from '../components/SpriteRenderer'; +import { UICanvas } from '../ui/UICanvas'; + +export class Scene { + private _name: string; + private _gameObjects: GameObject[] = []; + private _camera: Camera; + private _uiCanvas: UICanvas | null = null; + private _isLoaded: boolean = false; + + constructor(name: string, viewportWidth: number = 800, viewportHeight: number = 600) { + this._name = name; + this._camera = new Camera(viewportWidth, viewportHeight); + } + + /** + * Nom de la scène + */ + get name(): string { + return this._name; + } + + /** + * Caméra de la scène + */ + get camera(): Camera { + return this._camera; + } + + /** + * UICanvas de la scène (interface utilisateur) + */ + get uiCanvas(): UICanvas | null { + return this._uiCanvas; + } + + /** + * Crée ou récupère le UICanvas + */ + createUICanvas(width?: number, height?: number): UICanvas { + if (!this._uiCanvas) { + const w = width || this._camera.viewportWidth; + const h = height || this._camera.viewportHeight; + this._uiCanvas = new UICanvas(w, h); + } + return this._uiCanvas; + } + + /** + * État de chargement + */ + get isLoaded(): boolean { + return this._isLoaded; + } + + /** + * Appelé au chargement de la scène + */ + onLoad(): void { + this._isLoaded = true; + } + + /** + * Appelé au déchargement de la scène + */ + onUnload(): void { + // Nettoie tous les GameObjects + for (const obj of this._gameObjects) { + obj.destroy(); + } + this._gameObjects.length = 0; + + // Nettoie le UICanvas + this._uiCanvas = null; + + this._isLoaded = false; + } + + /** + * Update tous les objets de la scène + */ + update(deltaTime: number): void { + // Update tous les GameObjects actifs (seulement ceux sans parent - les enfants sont mis à jour par le parent) + for (const obj of this._gameObjects) { + if (!obj.parent && obj.active && !obj.isDestroyed) { + obj.update(deltaTime); + } + } + + // Nettoie les objets détruits + this._gameObjects = this._gameObjects.filter(obj => !obj.isDestroyed); + + // Update le UICanvas si présent + if (this._uiCanvas) { + this._uiCanvas.update(deltaTime); + } + } + + /** + * Récupère tous les SpriteRenderer de la scène (trie par layer) + */ + getAllRenderables(): SpriteRenderer[] { + const renderables: SpriteRenderer[] = []; + + // Parcourt tous les GameObjects (récursif pour inclure les enfants) + this._collectRenderablesRecursive(this._gameObjects, renderables); + + // Trie par layer (z-index) + renderables.sort((a, b) => { + // Si même layer, on peut trier par nom pour stabilité + if (a.layer !== b.layer) { + return a.layer - b.layer; + } + return a.gameObject.name.localeCompare(b.gameObject.name); + }); + + return renderables; + } + + /** + * Collecte récursivement tous les SpriteRenderer d'une liste de GameObjects + */ + private _collectRenderablesRecursive(gameObjects: GameObject[], renderables: SpriteRenderer[]): void { + for (const obj of gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + // Ajoute les SpriteRenderer de cet objet + const spriteRenderer = obj.getComponent(SpriteRenderer); + if (spriteRenderer && spriteRenderer.enabled) { + renderables.push(spriteRenderer); + } + + // Parcourt les enfants récursivement + if (obj.children.length > 0) { + this._collectRenderablesRecursive(Array.from(obj.children), renderables); + } + } + } + + /** + * Rend la scène (déprécié - maintenant géré par WebGLRenderer.render()) + * @deprecated Cette méthode est appelée par WebGLRenderer qui gère maintenant le rendu complet + */ + render(_renderer: WebGLRenderer): void { + // Cette méthode est maintenant vide car le rendu est géré directement + // par WebGLRenderer.render() qui appelle getAllRenderables() + } + + /** + * Ajoute un GameObject à la scène + */ + addGameObject(gameObject: GameObject): void { + if (this._gameObjects.indexOf(gameObject) !== -1) { + console.warn(`GameObject "${gameObject.name}" est déjà dans la scène`); + return; + } + this._gameObjects.push(gameObject); + gameObject._setScene(this); + } + + /** + * Retire un GameObject de la scène + */ + removeGameObject(gameObject: GameObject): void { + const index = this._gameObjects.indexOf(gameObject); + if (index !== -1) { + this._gameObjects.splice(index, 1); + gameObject._setScene(null); + } + } + + /** + * Recherche un GameObject par nom + */ + findGameObjectByName(name: string): GameObject | null { + for (const obj of this._gameObjects) { + if (obj.name === name) { + return obj; + } + // Recherche récursive dans les enfants + const found = obj.findChild(name); + if (found) { + return found; + } + } + return null; + } + + /** + * Récupère tous les GameObjects avec un tag spécifique + */ + findGameObjectsByTag(tag: string): GameObject[] { + const result: GameObject[] = []; + for (const obj of this._gameObjects) { + if (obj.tag === tag) { + result.push(obj); + } + } + return result; + } + + /** + * Liste de tous les GameObjects de la scène + */ + /** + * Trouve un GameObject par son nom + */ + findGameObject(name: string): GameObject | null { + return this._gameObjects.find(go => go.name === name) || null; + } + + get gameObjects(): readonly GameObject[] { + return this._gameObjects; + } +} + diff --git a/src/engine/core/SceneManager.ts b/src/engine/core/SceneManager.ts new file mode 100644 index 0000000..4dce4ce --- /dev/null +++ b/src/engine/core/SceneManager.ts @@ -0,0 +1,137 @@ +/** + * SceneManager - Gestion des scènes + * Gère les différents "écrans" du jeu (menu, niveaux, game over...) + */ + +import { Scene } from './Scene'; +import { WebGLRenderer } from '../rendering/WebGLRenderer'; + +export class SceneManager { + private _scenes: Map = new Map(); + private _activeScene: Scene | null = null; + private _isTransitioning: boolean = false; + + /** + * Enregistre une scène + */ + registerScene(name: string, scene: Scene): void { + if (this._scenes.has(name)) { + console.warn(`Scène "${name}" déjà enregistrée, remplacement`); + } + this._scenes.set(name, scene); + } + + /** + * Charge une scène (décharge l'ancienne et charge la nouvelle) + */ + loadScene(name: string): boolean { + if (this._isTransitioning) { + console.warn('Transition en cours, impossible de charger une nouvelle scène'); + return false; + } + + const scene = this._scenes.get(name); + if (!scene) { + console.error(`Scène "${name}" introuvable`); + return false; + } + + // Décharge la scène active + if (this._activeScene && this._activeScene.isLoaded) { + this._activeScene.onUnload(); + } + + // Charge la nouvelle scène + this._activeScene = scene; + this._activeScene.onLoad(); + + console.log(`Scène "${name}" chargée`); + return true; + } + + /** + * Décharge une scène + */ + unloadScene(name: string): boolean { + const scene = this._scenes.get(name); + if (!scene) { + console.error(`Scène "${name}" introuvable`); + return false; + } + + if (scene.isLoaded) { + scene.onUnload(); + } + + // Si c'est la scène active, désactive-la + if (this._activeScene === scene) { + this._activeScene = null; + } + + return true; + } + + /** + * Récupère la scène active + */ + getActiveScene(): Scene | null { + return this._activeScene; + } + + /** + * Récupère une scène par son nom + */ + getScene(name: string): Scene | null { + return this._scenes.get(name) || null; + } + + /** + * Update la scène active + */ + update(deltaTime: number): void { + if (this._activeScene && this._activeScene.isLoaded) { + this._activeScene.update(deltaTime); + } + } + + /** + * Rend la scène active + */ + render(renderer: WebGLRenderer): void { + if (this._activeScene && this._activeScene.isLoaded) { + // Appelle directement WebGLRenderer.render() avec la scène et sa caméra + renderer.render(this._activeScene, this._activeScene.camera); + } + } + + /** + * État de transition + */ + get isTransitioning(): boolean { + return this._isTransitioning; + } + + /** + * Définit l'état de transition + */ + set isTransitioning(value: boolean) { + this._isTransitioning = value; + } + + /** + * Nettoie toutes les scènes + */ + dispose(): void { + // Décharge toutes les scènes + for (const scene of this._scenes.values()) { + if (scene.isLoaded) { + scene.onUnload(); + } + } + + this._scenes.clear(); + this._activeScene = null; + this._isTransitioning = false; + } +} + diff --git a/src/engine/core/Time.ts b/src/engine/core/Time.ts new file mode 100644 index 0000000..9e2464c --- /dev/null +++ b/src/engine/core/Time.ts @@ -0,0 +1,103 @@ +/** + * Time - Gestion du temps + * Centralise toutes les informations temporelles du jeu + */ + +export class Time { + private static _deltaTime: number = 0; + private static _unscaledDeltaTime: number = 0; + private static _timeScale: number = 1.0; + private static _time: number = 0; + private static _frameCount: number = 0; + private static _fps: number = 0; + + // Pour calculer les FPS + private static _fpsUpdateInterval: number = 0.5; // Mise à jour FPS toutes les 0.5 secondes + private static _fpsAccumulator: number = 0; + private static _fpsFrameCount: number = 0; + + /** + * Temps écoulé depuis la dernière frame (en secondes) + * Affecté par timeScale + */ + static get deltaTime(): number { + return this._deltaTime; + } + + /** + * Temps écoulé depuis la dernière frame (en secondes) + * NON affecté par timeScale + */ + static get unscaledDeltaTime(): number { + return this._unscaledDeltaTime; + } + + /** + * Multiplicateur de temps + * 1 = normal, 0.5 = ralenti (2× plus lent), 2 = rapide (2× plus rapide) + */ + static get timeScale(): number { + return this._timeScale; + } + + static set timeScale(value: number) { + this._timeScale = Math.max(0, value); // Pas de valeur négative + } + + /** + * Temps total depuis le démarrage (en secondes) + */ + static get time(): number { + return this._time; + } + + /** + * Nombre de frames rendues depuis le démarrage + */ + static get frameCount(): number { + return this._frameCount; + } + + /** + * FPS moyen calculé + */ + static get fps(): number { + return this._fps; + } + + /** + * Met à jour Time (appelé par GameLoop chaque frame) + * @param deltaTimeSecondes Temps écoulé en secondes depuis la dernière frame + */ + static update(deltaTimeSecondes: number): void { + this._unscaledDeltaTime = deltaTimeSecondes; + this._deltaTime = deltaTimeSecondes * this._timeScale; + + this._time += this._deltaTime; + this._frameCount++; + + // Calcul des FPS + this._fpsAccumulator += deltaTimeSecondes; + this._fpsFrameCount++; + + if (this._fpsAccumulator >= this._fpsUpdateInterval) { + this._fps = this._fpsFrameCount / this._fpsAccumulator; + this._fpsAccumulator = 0; + this._fpsFrameCount = 0; + } + } + + /** + * Réinitialise Time (utile pour les restart de jeu) + */ + static reset(): void { + this._deltaTime = 0; + this._unscaledDeltaTime = 0; + this._time = 0; + this._frameCount = 0; + this._fps = 0; + this._fpsAccumulator = 0; + this._fpsFrameCount = 0; + } +} + diff --git a/src/engine/debug/DebugPanel.ts b/src/engine/debug/DebugPanel.ts new file mode 100644 index 0000000..be4bfc3 --- /dev/null +++ b/src/engine/debug/DebugPanel.ts @@ -0,0 +1,191 @@ +/** + * DebugPanel - Panel HTML pour afficher les informations de debug + */ + +export interface DebugInfo { + fps?: number; + deltaTime?: number; + playerPosition?: { x: number; y: number }; + playerVelocity?: { x: number; y: number }; + mouseScreenPos?: { x: number; y: number }; + mouseWorldPos?: { x: number; y: number }; + playerDirection?: string; + playerState?: string; + entityCount?: number; + activeColliders?: number; + cameraPosition?: { x: number; y: number }; + [key: string]: any; // Permet d'ajouter des propriétés dynamiques +} + +export class DebugPanel { + private panel: HTMLDivElement; + private isVisible: boolean = true; + private refreshRate: number = 100; // ms entre chaque update + private lastUpdate: number = 0; + private debugInfo: DebugInfo = {}; + + constructor() { + this.panel = this.createPanel(); + document.body.appendChild(this.panel); + + // Toggle avec F3 + window.addEventListener('keydown', (e) => { + if (e.key === 'F3') { + e.preventDefault(); + this.toggle(); + } + }); + } + + private createPanel(): HTMLDivElement { + const panel = document.createElement('div'); + panel.id = 'debug-panel'; + panel.style.cssText = ` + position: fixed; + bottom: 10px; + left: 10px; + background: rgba(0, 0, 0, 0.85); + color: #00ff00; + font-family: 'Courier New', monospace; + font-size: 12px; + padding: 15px; + border-radius: 8px; + border: 2px solid #00ff00; + z-index: 10000; + min-width: 300px; + max-width: 400px; + box-shadow: 0 4px 20px rgba(0, 255, 0, 0.3); + line-height: 1.6; + user-select: none; + `; + + panel.innerHTML = ` +
+ 🎮 DEBUG PANEL [F3] +
+
+ `; + + return panel; + } + + public update(currentTime: number): void { + if (!this.isVisible) return; + + // Limite le taux de rafraîchissement + if (currentTime - this.lastUpdate < this.refreshRate) { + return; + } + + this.lastUpdate = currentTime; + this.render(); + } + + private render(): void { + const content = this.panel.querySelector('#debug-content'); + if (!content) return; + + let html = ''; + + // Performance + if (this.debugInfo.fps !== undefined || this.debugInfo.deltaTime !== undefined) { + html += this.section('Performance', [ + this.debugInfo.fps !== undefined ? `FPS: ${this.debugInfo.fps.toFixed(0)}` : null, + this.debugInfo.deltaTime !== undefined ? `Delta: ${(this.debugInfo.deltaTime * 1000).toFixed(2)}ms` : null, + ]); + } + + // Joueur + if (this.debugInfo.playerPosition || this.debugInfo.playerVelocity || this.debugInfo.playerDirection) { + html += this.section('Player', [ + this.debugInfo.playerPosition ? `Pos: (${this.debugInfo.playerPosition.x.toFixed(1)}, ${this.debugInfo.playerPosition.y.toFixed(1)})` : null, + this.debugInfo.playerVelocity ? `Vel: (${this.debugInfo.playerVelocity.x.toFixed(1)}, ${this.debugInfo.playerVelocity.y.toFixed(1)})` : null, + this.debugInfo.playerDirection ? `Dir: ${this.debugInfo.playerDirection}` : null, + this.debugInfo.playerState ? `State: ${this.debugInfo.playerState}` : null, + ]); + } + + // Input + if (this.debugInfo.mouseScreenPos || this.debugInfo.mouseWorldPos) { + html += this.section('Input', [ + this.debugInfo.mouseScreenPos ? `Mouse Screen: (${this.debugInfo.mouseScreenPos.x.toFixed(0)}, ${this.debugInfo.mouseScreenPos.y.toFixed(0)})` : null, + this.debugInfo.mouseWorldPos ? `Mouse World: (${this.debugInfo.mouseWorldPos.x.toFixed(1)}, ${this.debugInfo.mouseWorldPos.y.toFixed(1)})` : null, + ]); + } + + // Caméra + if (this.debugInfo.cameraPosition) { + html += this.section('Camera', [ + `Pos: (${this.debugInfo.cameraPosition.x.toFixed(1)}, ${this.debugInfo.cameraPosition.y.toFixed(1)})`, + ]); + } + + // Scène + if (this.debugInfo.entityCount !== undefined || this.debugInfo.activeColliders !== undefined) { + html += this.section('Scene', [ + this.debugInfo.entityCount !== undefined ? `Entities: ${this.debugInfo.entityCount}` : null, + this.debugInfo.activeColliders !== undefined ? `Colliders: ${this.debugInfo.activeColliders}` : null, + ]); + } + + // Données custom + const customKeys = Object.keys(this.debugInfo).filter(key => + !['fps', 'deltaTime', 'playerPosition', 'playerVelocity', 'mouseScreenPos', + 'mouseWorldPos', 'playerDirection', 'playerState', 'entityCount', + 'activeColliders', 'cameraPosition'].includes(key) + ); + + if (customKeys.length > 0) { + const customData = customKeys.map(key => { + const value = this.debugInfo[key]; + if (typeof value === 'object') { + return `${key}: ${JSON.stringify(value)}`; + } + return `${key}: ${value}`; + }); + html += this.section('Custom', customData); + } + + content.innerHTML = html; + } + + private section(title: string, items: (string | null)[]): string { + const validItems = items.filter(item => item !== null); + if (validItems.length === 0) return ''; + + return ` +
+
+ ${title}: +
+
+ ${validItems.map(item => `
${item}
`).join('')} +
+
+ `; + } + + public setInfo(info: Partial): void { + this.debugInfo = { ...this.debugInfo, ...info }; + } + + public toggle(): void { + this.isVisible = !this.isVisible; + this.panel.style.display = this.isVisible ? 'block' : 'none'; + } + + public show(): void { + this.isVisible = true; + this.panel.style.display = 'block'; + } + + public hide(): void { + this.isVisible = false; + this.panel.style.display = 'none'; + } + + public dispose(): void { + this.panel.remove(); + } +} + diff --git a/src/engine/entities/Component.ts b/src/engine/entities/Component.ts new file mode 100644 index 0000000..f7ad1f6 --- /dev/null +++ b/src/engine/entities/Component.ts @@ -0,0 +1,120 @@ +/** + * Component - Classe abstraite de base pour tous les composants + * Philosophie ECS : Composition > Héritage + */ + +import { GameObject } from './GameObject'; +import { Transform } from './Transform'; + +export abstract class Component { + private _gameObject: GameObject | null = null; + private _enabled: boolean = true; + private _hasStarted: boolean = false; + + /** + * GameObject parent (assigné automatiquement lors de l'ajout) + */ + get gameObject(): GameObject { + if (!this._gameObject) { + throw new Error('Component n\'est pas attaché à un GameObject'); + } + return this._gameObject; + } + + /** + * Transform du GameObject parent (raccourci pratique) + */ + get transform(): Transform { + return this.gameObject.transform; + } + + /** + * État actif/inactif du composant + */ + get enabled(): boolean { + return this._enabled; + } + + set enabled(value: boolean) { + this._enabled = value; + } + + /** + * Indique si start() a été appelé + */ + get hasStarted(): boolean { + return this._hasStarted; + } + + /** + * Assigne ce composant à un GameObject (appelé par GameObject.addComponent) + * @internal + */ + _setGameObject(gameObject: GameObject): void { + if (this._gameObject && this._gameObject !== gameObject) { + throw new Error('Component déjà attaché à un autre GameObject'); + } + this._gameObject = gameObject; + } + + /** + * Marque que start() a été appelé (appelé par GameObject) + * @internal + */ + _markStarted(): void { + this._hasStarted = true; + } + + /** + * Récupère un composant du même GameObject par type + * Raccourci pratique pour gameObject.getComponent() + */ + getComponent(type: new (...args: any[]) => T): T | null { + return this.gameObject.getComponent(type); + } + + /** + * Cycle de vie : Appelé à l'ajout du composant (avant start) + * Override pour initialiser le composant + */ + awake(): void { + // À override par les composants enfants + } + + /** + * Cycle de vie : Appelé avant la première update + * Tous les composants sont créés, safe pour référencer d'autres composants + * Override pour initialiser après que tous les composants soient créés + */ + start(): void { + // À override par les composants enfants + } + + /** + * Cycle de vie : Appelé chaque frame + * @param deltaTime Temps écoulé depuis la dernière frame (en secondes) + */ + update(_deltaTime: number): void { + // À override par les composants enfants + } + + /** + * Cycle de vie : Appelé à la destruction du composant + * Override pour nettoyer les ressources + */ + onDestroy(): void { + // À override par les composants enfants + } + + /** + * Nettoie le composant (appelé par GameObject) + * @internal + */ + _destroy(): void { + this.onDestroy(); + this._gameObject = null; + this._enabled = false; + this._hasStarted = false; + } +} + diff --git a/src/engine/entities/GameObject.ts b/src/engine/entities/GameObject.ts new file mode 100644 index 0000000..7e1da2a --- /dev/null +++ b/src/engine/entities/GameObject.ts @@ -0,0 +1,406 @@ +/** + * GameObject - Entité de base du jeu + * Tout ce qui existe dans le jeu est un GameObject + * Contient un Transform et des Components + */ + +import { Transform } from './Transform'; +import { Component } from './Component'; +import { Scene } from '../core/Scene'; + +export class GameObject { + private _name: string; + private _active: boolean = true; + private _tag: string = 'Untagged'; + private _layer: number = 0; + + private _transform: Transform; + private _components: Map = new Map(); + + private _parent: GameObject | null = null; + private _children: GameObject[] = []; + + private _scene: Scene | null = null; + + // Cycle de vie + private _hasAwakened: boolean = false; + private _hasStarted: boolean = false; + private _isDestroyed: boolean = false; + private _destroyNextFrame: boolean = false; + + constructor(name: string = 'GameObject') { + this._name = name; + this._transform = new Transform(); + } + + /** + * Nom de l'objet (pour debug/recherche) + */ + get name(): string { + return this._name; + } + + set name(value: string) { + this._name = value; + } + + /** + * État actif/inactif + * Si false, update() n'est pas appelé + */ + get active(): boolean { + return this._active; + } + + set active(value: boolean) { + if (this._active === value) { + return; + } + + this._active = value; + + // Propage aux enfants + for (const child of this._children) { + child.active = value; + } + } + + /** + * Tag pour catégorisation (ex: "Player", "Enemy", "Coin") + */ + get tag(): string { + return this._tag; + } + + set tag(value: string) { + this._tag = value; + } + + /** + * Layer de rendu (z-index pour le tri) + */ + get layer(): number { + return this._layer; + } + + set layer(value: number) { + this._layer = value; + } + + /** + * Transform de l'objet (toujours présent) + */ + get transform(): Transform { + return this._transform; + } + + /** + * Scène à laquelle appartient cet objet + */ + get scene(): Scene | null { + return this._scene; + } + + /** + * Parent dans la hiérarchie + */ + get parent(): GameObject | null { + return this._parent; + } + + /** + * Liste des enfants + */ + get children(): readonly GameObject[] { + return this._children; + } + + /** + * Indique si l'objet a été détruit + */ + get isDestroyed(): boolean { + return this._isDestroyed; + } + + /** + * Assigne la scène (appelé par Scene.addGameObject) + * @internal + */ + _setScene(scene: Scene | null): void { + this._scene = scene; + } + + /** + * Déclenche awake() si ce n'est pas déjà fait + */ + private _awake(): void { + if (this._hasAwakened || !this._active) { + return; + } + + this._hasAwakened = true; + + // Appelle awake() sur tous les composants + for (const component of this._components.values()) { + if (component.enabled) { + component.awake(); + } + } + + // Appelle awake() sur tous les enfants + for (const child of this._children) { + child._awake(); + } + } + + /** + * Déclenche start() si ce n'est pas déjà fait + */ + private _start(): void { + if (this._hasStarted || !this._active || !this._hasAwakened) { + return; + } + + this._hasStarted = true; + + // Appelle start() sur tous les composants + for (const component of this._components.values()) { + if (component.enabled && !component.hasStarted) { + component.start(); + component._markStarted(); + } + } + + // Appelle start() sur tous les enfants + for (const child of this._children) { + child._start(); + } + } + + /** + * Update tous les composants actifs + */ + update(deltaTime: number): void { + // S'assure que awake() et start() ont été appelés + if (!this._hasAwakened) { + this._awake(); + } + if (!this._hasStarted && this._hasAwakened) { + this._start(); + } + + if (!this._active || this._isDestroyed) { + return; + } + + // Update tous les composants actifs + for (const component of this._components.values()) { + if (component.enabled) { + component.update(deltaTime); + } + } + + // Update tous les enfants + for (const child of this._children) { + child.update(deltaTime); + } + + // Gère la destruction différée + if (this._destroyNextFrame) { + this._destroy(); + } + } + + /** + * Ajoute un composant à l'objet + * @returns Le composant ajouté (pour chaînage) + */ + addComponent(component: T): T { + const componentName = component.constructor.name; + + if (this._components.has(componentName)) { + console.warn(`GameObject "${this._name}" a déjà un composant de type "${componentName}"`); + } + + component._setGameObject(this); + this._components.set(componentName, component); + + // Si l'objet est déjà initialisé, appelle awake() immédiatement + if (this._hasAwakened) { + if (component.enabled) { + component.awake(); + } + // start() sera appelé au prochain update + } + + return component; + } + + /** + * Récupère un composant par type + * @returns Le composant trouvé ou null + */ + getComponent(type: new (...args: any[]) => T): T | null { + const componentName = type.name; + const component = this._components.get(componentName); + return component instanceof type ? component : null; + } + + /** + * Récupère tous les composants + */ + getComponents(): Component[] { + return Array.from(this._components.values()); + } + + /** + * Retire un composant + */ + removeComponent(type: new (...args: any[]) => T): void { + const componentName = type.name; + const component = this._components.get(componentName); + + if (component) { + component._destroy(); + this._components.delete(componentName); + } + } + + /** + * Définit le parent dans la hiérarchie + */ + setParent(parent: GameObject | null): void { + if (this._parent === parent) { + return; + } + + // Retire de l'ancien parent + if (this._parent) { + const index = this._parent._children.indexOf(this); + if (index !== -1) { + this._parent._children.splice(index, 1); + } + } + + // Ajoute au nouveau parent + this._parent = parent; + if (parent) { + parent._children.push(this); + // Met à jour le transform parent + this._transform.setParent(parent.transform); + // Hérite de la scène + this._setScene(parent.scene); + } else { + this._transform.setParent(null); + } + } + + /** + * Ajoute un enfant + */ + addChild(child: GameObject): void { + child.setParent(this); + } + + /** + * Retire un enfant + */ + removeChild(child: GameObject): void { + if (child.parent === this) { + child.setParent(null); + } + } + + /** + * Recherche un enfant par nom + */ + findChild(name: string): GameObject | null { + for (const child of this._children) { + if (child.name === name) { + return child; + } + // Recherche récursive + const found = child.findChild(name); + if (found) { + return found; + } + } + return null; + } + + /** + * Recherche un composant dans cet objet ou ses enfants + */ + getComponentInChildren(type: new (...args: any[]) => T): T | null { + // Cherche dans cet objet + const component = this.getComponent(type); + if (component) { + return component; + } + + // Cherche dans les enfants + for (const child of this._children) { + const childComponent = child.getComponentInChildren(type); + if (childComponent) { + return childComponent; + } + } + + return null; + } + + /** + * Marque l'objet pour destruction au prochain frame + * (destruction différée pour éviter de détruire pendant update) + */ + destroy(): void { + if (this._isDestroyed || this._destroyNextFrame) { + return; + } + this._destroyNextFrame = true; + } + + /** + * Détruit l'objet immédiatement (appelé à la fin du frame) + * @internal + */ + private _destroy(): void { + if (this._isDestroyed) { + return; + } + + this._isDestroyed = true; + this._active = false; + + // Détruit tous les composants + for (const component of this._components.values()) { + component._destroy(); + } + this._components.clear(); + + // Détruit tous les enfants + for (const child of this._children) { + child._destroy(); + } + this._children.length = 0; + + // Retire du parent + this.setParent(null); + + // Retire de la scène + if (this._scene) { + this._scene.removeGameObject(this); + } + + // Nettoie le transform + this._transform.destroy(); + } + + /** + * Crée un nouveau GameObject avec un nom + */ + static create(name: string = 'GameObject'): GameObject { + return new GameObject(name); + } +} + diff --git a/src/engine/entities/Transform.ts b/src/engine/entities/Transform.ts new file mode 100644 index 0000000..287670f --- /dev/null +++ b/src/engine/entities/Transform.ts @@ -0,0 +1,340 @@ +/** + * Transform - Position, rotation, scale avec hiérarchie parent-enfant + * Gère les transformations locales vs monde avec dirty flags pour optimisation + */ + +import { Vector2 } from '../math/Vector2'; +import { Matrix3 } from '../math/Matrix3'; + +export class Transform { + private _localPosition: Vector2 = new Vector2(0, 0); + private _localRotation: number = 0; // en degrés + private _localScale: Vector2 = new Vector2(1, 1); + + private _parent: Transform | null = null; + private _children: Transform[] = []; + + // Dirty flags pour optimisation + private _dirty: boolean = true; + private _dirtyWorld: boolean = true; + private _cachedWorldMatrix: Matrix3 = Matrix3.identity(); + private _cachedWorldPosition: Vector2 = new Vector2(0, 0); + private _cachedWorldRotation: number = 0; + private _cachedWorldScale: Vector2 = new Vector2(1, 1); + + /** + * Position locale (relative au parent) + */ + get localPosition(): Vector2 { + return this._localPosition; + } + + set localPosition(value: Vector2) { + this._localPosition = value.clone(); + this._markDirty(); + } + + /** + * Rotation locale (relative au parent, en degrés) + */ + get localRotation(): number { + return this._localRotation; + } + + set localRotation(value: number) { + this._localRotation = value; + this._markDirty(); + } + + /** + * Scale local (relative au parent) + */ + get localScale(): Vector2 { + return this._localScale; + } + + set localScale(value: Vector2) { + this._localScale = value.clone(); + this._markDirty(); + } + + /** + * Position dans l'espace monde (calculée à partir de la hiérarchie) + */ + get position(): Vector2 { + this._updateWorldTransform(); + return this._cachedWorldPosition; + } + + set position(value: Vector2) { + if (this._parent) { + // Convertit la position monde en position locale + const parentInverse = this._parent.getWorldMatrix().inverse(); + const localPos = parentInverse.transformPoint(value); + this.localPosition = localPos; + } else { + this.localPosition = value; + } + } + + /** + * Rotation dans l'espace monde (en degrés) + */ + get rotation(): number { + this._updateWorldTransform(); + return this._cachedWorldRotation; + } + + set rotation(value: number) { + if (this._parent) { + // Rotation monde = rotation parent + rotation locale + const parentRotation = this._parent.rotation; + this.localRotation = value - parentRotation; + } else { + this.localRotation = value; + } + } + + /** + * Scale dans l'espace monde + */ + get scale(): Vector2 { + this._updateWorldTransform(); + return this._cachedWorldScale; + } + + set scale(value: Vector2) { + if (this._parent) { + const parentScale = this._parent.scale; + this.localScale = new Vector2( + value.x / parentScale.x, + value.y / parentScale.y + ); + } else { + this.localScale = value; + } + } + + /** + * Transform parent dans la hiérarchie + */ + get parent(): Transform | null { + return this._parent; + } + + /** + * Liste des transforms enfants + */ + get children(): readonly Transform[] { + return this._children; + } + + /** + * Définit le parent (retire automatiquement de l'ancien parent si nécessaire) + */ + setParent(parent: Transform | null): void { + if (this._parent === parent) { + return; + } + + // Retire de l'ancien parent + if (this._parent) { + const index = this._parent._children.indexOf(this); + if (index !== -1) { + this._parent._children.splice(index, 1); + } + } + + // Ajoute au nouveau parent + this._parent = parent; + if (parent) { + parent._children.push(this); + } + + this._markDirty(); + } + + /** + * Matrice de transformation monde (avec cache et dirty flags) + */ + getWorldMatrix(): Matrix3 { + this._updateWorldTransform(); + return this._cachedWorldMatrix; + } + + /** + * Met à jour les transformations monde si nécessaire + */ + private _updateWorldTransform(): void { + if (!this._dirty && !this._dirtyWorld) { + return; // Déjà à jour + } + + // Si le parent est aussi dirty, on doit attendre qu'il se mette à jour d'abord + if (this._parent && this._parent._dirtyWorld) { + this._parent._updateWorldTransform(); + } + + // Calcule la matrice monde + const localMatrix = this._calculateLocalMatrix(); + + if (this._parent) { + // Combine avec la matrice parent + const parentMatrix = this._parent.getWorldMatrix(); + this._cachedWorldMatrix = parentMatrix.clone().multiply(localMatrix); + + // Calcule position/rotation/scale monde à partir de la matrice + this._extractWorldFromMatrix(); + } else { + // Pas de parent = local = monde + this._cachedWorldMatrix = localMatrix; + this._cachedWorldPosition = this._localPosition.clone(); + this._cachedWorldRotation = this._localRotation; + this._cachedWorldScale = this._localScale.clone(); + } + + this._dirty = false; + this._dirtyWorld = false; + } + + /** + * Calcule la matrice locale (translation, rotation, scale) + */ + private _calculateLocalMatrix(): Matrix3 { + const matrix = Matrix3.identity(); + matrix.translate(this._localPosition.x, this._localPosition.y); + matrix.rotate(this._localRotation * Math.PI / 180); // Convertit degrés → radians + matrix.scale(this._localScale.x, this._localScale.y); + return matrix; + } + + /** + * Extrait position/rotation/scale monde à partir de la matrice monde + * (approximation pour rotation et scale - pour une extraction exacte, il faudrait décomposer SVD) + */ + private _extractWorldFromMatrix(): void { + const m = this._cachedWorldMatrix.toArray(); + + // Position (colonne de translation) + this._cachedWorldPosition = new Vector2(m[6]!, m[7]!); + + // Rotation et scale (décomposition approximative) + const a = m[0]!, b = m[1]!; + const c = m[3]!, d = m[4]!; + + // Scale (magnitude des vecteurs de base) + const scaleX = Math.sqrt(a * a + b * b); + const scaleY = Math.sqrt(c * c + d * d); + this._cachedWorldScale = new Vector2(scaleX, scaleY); + + // Rotation (angle du premier vecteur de base) + this._cachedWorldRotation = Math.atan2(b, a) * 180 / Math.PI; // Convertit radians → degrés + } + + /** + * Marque ce transform et ses enfants comme dirty + */ + private _markDirty(): void { + this._dirty = true; + this._dirtyWorld = true; + + // Propage aux enfants + for (const child of this._children) { + child._markDirtyWorld(); + } + } + + /** + * Marque comme dirty monde uniquement (sans recalculer local) + * @internal + */ + _markDirtyWorld(): void { + this._dirtyWorld = true; + for (const child of this._children) { + child._markDirtyWorld(); + } + } + + /** + * Déplace le transform (additionne à la position) + */ + translate(delta: Vector2): void { + this.localPosition = this._localPosition.add(delta); + } + + /** + * Tourne le transform (ajoute à la rotation) + * @param angle Angle en degrés + */ + rotate(angle: number): void { + this.localRotation = this._localRotation + angle; + } + + /** + * Fait regarder le transform vers une position + * @param target Position cible dans l'espace monde + */ + lookAt(target: Vector2): void { + const currentPos = this.position; + const direction = target.subtract(currentPos); + const angle = Math.atan2(direction.y, direction.x) * 180 / Math.PI; + this.rotation = angle; + } + + /** + * Transforme un point de l'espace local vers l'espace monde + */ + transformPoint(localPoint: Vector2): Vector2 { + return this.getWorldMatrix().transformPoint(localPoint); + } + + /** + * Transforme un point de l'espace monde vers l'espace local + */ + inverseTransformPoint(worldPoint: Vector2): Vector2 { + const inverseMatrix = this.getWorldMatrix().inverse(); + return inverseMatrix.transformPoint(worldPoint); + } + + /** + * Vecteur droit (direction +X après rotation) + */ + get right(): Vector2 { + const angle = this.rotation * Math.PI / 180; + return new Vector2(Math.cos(angle), Math.sin(angle)); + } + + /** + * Vecteur haut (direction +Y après rotation, WebGL: Y vers le haut = négatif) + */ + get up(): Vector2 { + const angle = (this.rotation + 90) * Math.PI / 180; + return new Vector2(Math.cos(angle), Math.sin(angle)); + } + + /** + * Direction avant (même que right pour 2D) + */ + get forward(): Vector2 { + return this.right; + } + + /** + * Réinitialise le transform (position 0, rotation 0, scale 1) + */ + reset(): void { + this._localPosition = new Vector2(0, 0); + this._localRotation = 0; + this._localScale = new Vector2(1, 1); + this._markDirty(); + } + + /** + * Nettoie le transform (retire de la hiérarchie) + */ + destroy(): void { + this.setParent(null); + // Les enfants sont détruits avec le GameObject parent + } +} + diff --git a/src/engine/input/InputManager.ts b/src/engine/input/InputManager.ts new file mode 100644 index 0000000..55d919e --- /dev/null +++ b/src/engine/input/InputManager.ts @@ -0,0 +1,255 @@ +/** + * InputManager - Gestion des entrées utilisateur + * Centralise toutes les entrées (clavier, souris, tactile) + */ + +import { Vector2 } from '../math/Vector2'; +import { Camera } from '../rendering/Camera'; + +/** + * États possibles pour une touche/bouton + */ +export enum KeyState { + Up, // Pas pressé + Down, // Pressé cette frame + Held, // Maintenu + Released // Relâché cette frame +} + +export enum MouseButton { + Left = 0, + Middle = 1, + Right = 2 +} + +export class InputManager { + private _keys: Map = new Map(); + private _mousePosition: Vector2 = new Vector2(0, 0); + private _mouseButtons: Map = new Map(); + private _wheelDelta: number = 0; + private _canvas: HTMLCanvasElement | null = null; + + /** + * Initialise l'InputManager avec un canvas + */ + initialize(canvas: HTMLCanvasElement): void { + this._canvas = canvas; + + // Écouteurs clavier + window.addEventListener('keydown', this._onKeyDown.bind(this)); + window.addEventListener('keyup', this._onKeyUp.bind(this)); + + // Écouteurs souris + canvas.addEventListener('mousedown', this._onMouseDown.bind(this)); + canvas.addEventListener('mouseup', this._onMouseUp.bind(this)); + canvas.addEventListener('mousemove', this._onMouseMove.bind(this)); + canvas.addEventListener('wheel', this._onWheel.bind(this)); + + // Empêche le menu contextuel sur clic droit + canvas.addEventListener('contextmenu', (e) => e.preventDefault()); + + // Empêche les raccourcis clavier (ex: F5, Ctrl+R) + window.addEventListener('keydown', (e) => { + // Autorise quelques touches utiles + if (e.key === 'F12' || (e.key === 'F5' && e.ctrlKey)) { + return; // Autorise F12 (dev tools) et Ctrl+F5 + } + // Empêche les autres raccourcis pendant le jeu + if (e.key.startsWith('F') || (e.ctrlKey && ['r', 'R'].includes(e.key))) { + e.preventDefault(); + } + }); + } + + /** + * Met à jour l'InputManager (doit être appelé chaque frame) + * Réinitialise les états Down et Released + */ + update(): void { + // Passe Down -> Held + for (const [key, state] of this._keys.entries()) { + if (state === KeyState.Down) { + this._keys.set(key, KeyState.Held); + } else if (state === KeyState.Released) { + this._keys.set(key, KeyState.Up); + } + } + + // Passe Down -> Held pour les boutons souris + for (const [button, state] of this._mouseButtons.entries()) { + if (state === KeyState.Down) { + this._mouseButtons.set(button, KeyState.Held); + } else if (state === KeyState.Released) { + this._mouseButtons.set(button, KeyState.Up); + } + } + + // Reset wheel delta chaque frame + this._wheelDelta = 0; + } + + /** + * Touche pressée cette frame (une seule fois par pression) + */ + isKeyDown(key: string): boolean { + return this._keys.get(key) === KeyState.Down; + } + + /** + * Touche maintenue (down ou held) + */ + isKeyHeld(key: string): boolean { + const state = this._keys.get(key); + return state === KeyState.Down || state === KeyState.Held; + } + + /** + * Touche relâchée cette frame + */ + isKeyUp(key: string): boolean { + return this._keys.get(key) === KeyState.Released; + } + + /** + * Bouton souris pressé cette frame + */ + isMouseButtonDown(button: number = MouseButton.Left): boolean { + return this._mouseButtons.get(button) === KeyState.Down; + } + + /** + * Bouton souris maintenu + */ + isMouseButtonHeld(button: number = MouseButton.Left): boolean { + const state = this._mouseButtons.get(button); + return state === KeyState.Down || state === KeyState.Held; + } + + /** + * Bouton souris relâché cette frame + */ + isMouseButtonUp(button: number = MouseButton.Left): boolean { + return this._mouseButtons.get(button) === KeyState.Released; + } + + /** + * Position de la souris en pixels (coordonnées écran) + */ + getMousePosition(): Vector2 { + return this._mousePosition.clone(); + } + + /** + * Position de la souris dans l'espace monde (convertit via caméra) + */ + getMouseWorldPosition(camera: Camera): Vector2 { + return camera.screenToWorld(this._mousePosition); + } + + /** + * Delta de la molette cette frame + * Positif = vers le haut, Négatif = vers le bas + */ + getWheelDelta(): number { + return this._wheelDelta; + } + + /** + * Normalise le nom de touche (standardise les noms) + */ + private _normalizeKey(key: string): string { + // Mappe certaines touches spéciales + const keyMap: Record = { + ' ': 'Space', + 'ArrowUp': 'ArrowUp', + 'ArrowDown': 'ArrowDown', + 'ArrowLeft': 'ArrowLeft', + 'ArrowRight': 'ArrowRight', + 'Escape': 'Escape', + 'Enter': 'Enter', + 'Tab': 'Tab', + 'Shift': 'Shift', + 'Control': 'Control', + 'Alt': 'Alt', + 'Meta': 'Meta' + }; + + return keyMap[key] || key; + } + + /** + * Handler pour keydown + */ + private _onKeyDown(event: KeyboardEvent): void { + const key = this._normalizeKey(event.key); + const currentState = this._keys.get(key); + + // Évite les répétitions de touches + if (currentState !== KeyState.Held && currentState !== KeyState.Down) { + this._keys.set(key, KeyState.Down); + } + } + + /** + * Handler pour keyup + */ + private _onKeyUp(event: KeyboardEvent): void { + const key = this._normalizeKey(event.key); + this._keys.set(key, KeyState.Released); + } + + /** + * Handler pour mousedown + */ + private _onMouseDown(event: MouseEvent): void { + const button = event.button; + const currentState = this._mouseButtons.get(button); + + if (currentState !== KeyState.Held && currentState !== KeyState.Down) { + this._mouseButtons.set(button, KeyState.Down); + } + + event.preventDefault(); + } + + /** + * Handler pour mouseup + */ + private _onMouseUp(event: MouseEvent): void { + const button = event.button; + this._mouseButtons.set(button, KeyState.Released); + event.preventDefault(); + } + + /** + * Handler pour mousemove + */ + private _onMouseMove(event: MouseEvent): void { + if (!this._canvas) { + return; + } + + const rect = this._canvas.getBoundingClientRect(); + this._mousePosition.x = event.clientX - rect.left; + this._mousePosition.y = event.clientY - rect.top; + } + + /** + * Handler pour wheel (molette) + */ + private _onWheel(event: WheelEvent): void { + this._wheelDelta = event.deltaY > 0 ? 1 : -1; + event.preventDefault(); + } + + /** + * Nettoie les ressources + */ + dispose(): void { + // Les event listeners seront automatiquement nettoyés quand la page se ferme + // Mais on peut les retirer manuellement si nécessaire + this._keys.clear(); + this._mouseButtons.clear(); + } +} + diff --git a/src/engine/math/Color.ts b/src/engine/math/Color.ts new file mode 100644 index 0000000..d73bbe8 --- /dev/null +++ b/src/engine/math/Color.ts @@ -0,0 +1,139 @@ +/** + * Color - Couleur RGBA + * Représente une couleur avec alpha (transparence) + */ + +export class Color { + public r: number; // Rouge (0-1) + public g: number; // Vert (0-1) + public b: number; // Bleu (0-1) + public a: number; // Alpha (0-1, 0 = transparent, 1 = opaque) + + constructor(r: number = 1, g: number = 1, b: number = 1, a: number = 1) { + this.r = Math.max(0, Math.min(1, r)); + this.g = Math.max(0, Math.min(1, g)); + this.b = Math.max(0, Math.min(1, b)); + this.a = Math.max(0, Math.min(1, a)); + } + + /** + * Crée une couleur depuis des valeurs 0-255 + */ + static fromRGB(r: number, g: number, b: number, a: number = 255): Color { + return new Color(r / 255, g / 255, b / 255, a / 255); + } + + /** + * Crée une couleur depuis une valeur hexadécimale (#RRGGBB ou #RRGGBBAA) + */ + static fromHex(hex: string): Color { + hex = hex.replace('#', ''); + + if (hex.length === 6) { + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + return new Color(r, g, b, 1); + } else if (hex.length === 8) { + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + const a = parseInt(hex.substring(6, 8), 16) / 255; + return new Color(r, g, b, a); + } + + throw new Error(`Format hex invalide: ${hex}`); + } + + /** + * Interpolation linéaire entre deux couleurs + */ + lerp(other: Color, t: number): Color { + const clampedT = Math.max(0, Math.min(1, t)); + return new Color( + this.r + (other.r - this.r) * clampedT, + this.g + (other.g - this.g) * clampedT, + this.b + (other.b - this.b) * clampedT, + this.a + (other.a - this.a) * clampedT + ); + } + + /** + * Multiplication de couleurs (utile pour les teintes) + */ + multiply(other: Color): Color { + return new Color( + this.r * other.r, + this.g * other.g, + this.b * other.b, + this.a * other.a + ); + } + + /** + * Convertit en format hexadécimal (#RRGGBBAA) + */ + toHex(): string { + const r = Math.round(this.r * 255).toString(16).padStart(2, '0'); + const g = Math.round(this.g * 255).toString(16).padStart(2, '0'); + const b = Math.round(this.b * 255).toString(16).padStart(2, '0'); + const a = Math.round(this.a * 255).toString(16).padStart(2, '0'); + return `#${r}${g}${b}${a}`; + } + + /** + * Convertit en format rgba(r, g, b, a) + */ + toRGBA(): string { + const r = Math.round(this.r * 255); + const g = Math.round(this.g * 255); + const b = Math.round(this.b * 255); + return `rgba(${r}, ${g}, ${b}, ${this.a})`; + } + + /** + * Retourne un tableau [r, g, b, a] + */ + toArray(): [number, number, number, number] { + return [this.r, this.g, this.b, this.a]; + } + + /** + * Copie cette couleur + */ + clone(): Color { + return new Color(this.r, this.g, this.b, this.a); + } + + /** + * Copie les valeurs d'une autre couleur + */ + copyFrom(other: Color): Color { + this.r = other.r; + this.g = other.g; + this.b = other.b; + this.a = other.a; + return this; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Color(${this.r}, ${this.g}, ${this.b}, ${this.a})`; + } + + /** + * Constantes prédéfinies + */ + static readonly white = new Color(1, 1, 1, 1); + static readonly black = new Color(0, 0, 0, 1); + static readonly red = new Color(1, 0, 0, 1); + static readonly green = new Color(0, 1, 0, 1); + static readonly blue = new Color(0, 0, 1, 1); + static readonly yellow = new Color(1, 1, 0, 1); + static readonly cyan = new Color(0, 1, 1, 1); + static readonly magenta = new Color(1, 0, 1, 1); + static readonly transparent = new Color(0, 0, 0, 0); +} + diff --git a/src/engine/math/Matrix3.ts b/src/engine/math/Matrix3.ts new file mode 100644 index 0000000..029ebcd --- /dev/null +++ b/src/engine/math/Matrix3.ts @@ -0,0 +1,223 @@ +/** + * Matrix3 - Matrice 3×3 + * Représente une transformation 2D (translation, rotation, scale) + * + * Structure (colonne-major pour WebGL) : + * [a, c, tx] [m[0], m[3], m[6]] + * [b, d, ty] = [m[1], m[4], m[7]] + * [0, 0, 1 ] [m[2], m[5], m[8]] + * + * Stockage en ligne-major pour facilité : + * [a, b, 0, c, d, 0, tx, ty, 1] + */ + +import { Vector2 } from './Vector2'; + +export class Matrix3 { + private _m: Float32Array; + + constructor( + a: number = 1, c: number = 0, tx: number = 0, + b: number = 0, d: number = 1, ty: number = 0 + ) { + // Stockage ligne-major: [a, b, 0, c, d, 0, tx, ty, 1] + this._m = new Float32Array(9); + this.set(a, c, tx, b, d, ty); + } + + /** + * Définit les valeurs de la matrice + */ + set( + a: number, c: number, tx: number, + b: number, d: number, ty: number + ): Matrix3 { + this._m[0] = a; this._m[1] = b; this._m[2] = 0; + this._m[3] = c; this._m[4] = d; this._m[5] = 0; + this._m[6] = tx; this._m[7] = ty; this._m[8] = 1; + return this; + } + + /** + * Retourne la matrice identité + */ + identity(): Matrix3 { + return this.set(1, 0, 0, 0, 1, 0); + } + + /** + * Applique une translation + */ + translate(x: number, y: number): Matrix3 { + this._m[6]! += this._m[0]! * x + this._m[3]! * y; + this._m[7]! += this._m[1]! * x + this._m[4]! * y; + return this; + } + + /** + * Applique une rotation (angle en radians) + */ + rotate(angle: number): Matrix3 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + const a = this._m[0]!; + const b = this._m[1]!; + const c = this._m[3]!; + const d = this._m[4]!; + + this._m[0] = a * cos + c * sin; + this._m[1] = b * cos + d * sin; + this._m[3] = c * cos - a * sin; + this._m[4] = d * cos - b * sin; + + return this; + } + + /** + * Applique un scale + */ + scale(x: number, y: number): Matrix3 { + this._m[0]! *= x; + this._m[1]! *= y; + this._m[3]! *= x; + this._m[4]! *= y; + return this; + } + + /** + * Multiplie par une autre matrice (this * other) + */ + multiply(other: Matrix3): Matrix3 { + const a1 = this._m[0]!, b1 = this._m[1]!, c1 = this._m[3]!, d1 = this._m[4]!, tx1 = this._m[6]!, ty1 = this._m[7]!; + const a2 = other._m[0]!, b2 = other._m[1]!, c2 = other._m[3]!, d2 = other._m[4]!, tx2 = other._m[6]!, ty2 = other._m[7]!; + + this._m[0] = a1 * a2 + c1 * b2; + this._m[1] = b1 * a2 + d1 * b2; + this._m[3] = a1 * c2 + c1 * d2; + this._m[4] = b1 * c2 + d1 * d2; + this._m[6] = a1 * tx2 + c1 * ty2 + tx1; + this._m[7] = b1 * tx2 + d1 * ty2 + ty1; + + return this; + } + + /** + * Applique la transformation à un point + */ + transformPoint(point: Vector2): Vector2 { + const x = point.x; + const y = point.y; + return new Vector2( + this._m[0]! * x + this._m[3]! * y + this._m[6]!, + this._m[1]! * x + this._m[4]! * y + this._m[7]! + ); + } + + /** + * Calcule la matrice inverse + */ + inverse(): Matrix3 { + const a = this._m[0]!; + const b = this._m[1]!; + const c = this._m[3]!; + const d = this._m[4]!; + const tx = this._m[6]!; + const ty = this._m[7]!; + + const det = a * d - b * c; + if (Math.abs(det) < 1e-10) { + console.warn('Matrice non inversible'); + return this.identity(); + } + + const invDet = 1 / det; + + return new Matrix3( + d * invDet, -c * invDet, (c * ty - d * tx) * invDet, + -b * invDet, a * invDet, (b * tx - a * ty) * invDet + ); + } + + /** + * Retourne un Float32Array pour WebGL (colonne-major) + */ + toArray(): Float32Array { + // Convertit ligne-major vers colonne-major pour WebGL + return new Float32Array([ + this._m[0]!, this._m[1]!, this._m[2]!, + this._m[3]!, this._m[4]!, this._m[5]!, + this._m[6]!, this._m[7]!, this._m[8]! + ]); + } + + /** + * Copie cette matrice + */ + clone(): Matrix3 { + return new Matrix3( + this._m[0], this._m[3], this._m[6], + this._m[1], this._m[4], this._m[7] + ); + } + + /** + * Copie les valeurs d'une autre matrice + */ + copyFrom(other: Matrix3): Matrix3 { + this._m.set(other._m); + return this; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Matrix3(${this._m[0]}, ${this._m[3]}, ${this._m[6]}, ${this._m[1]}, ${this._m[4]}, ${this._m[7]})`; + } + + /** + * Matrice identité + */ + static identity(): Matrix3 { + return new Matrix3(1, 0, 0, 0, 1, 0); + } + + /** + * Matrice de translation + */ + static translation(x: number, y: number): Matrix3 { + return new Matrix3(1, 0, x, 0, 1, y); + } + + /** + * Matrice de rotation (angle en radians) + */ + static rotation(angle: number): Matrix3 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return new Matrix3(cos, -sin, 0, sin, cos, 0); + } + + /** + * Matrice de scale + */ + static scaling(x: number, y: number): Matrix3 { + return new Matrix3(x, 0, 0, 0, y, 0); + } + + /** + * Matrice de projection orthographique + * Convertit coordonnées pixel en coordonnées NDC (-1 à 1) + */ + static orthographic(left: number, right: number, bottom: number, top: number): Matrix3 { + const width = right - left; + const height = top - bottom; + + return new Matrix3( + 2 / width, 0, -(right + left) / width, + 0, 2 / height, -(top + bottom) / height + ); + } +} + diff --git a/src/engine/math/Rect.ts b/src/engine/math/Rect.ts new file mode 100644 index 0000000..6b92fe3 --- /dev/null +++ b/src/engine/math/Rect.ts @@ -0,0 +1,178 @@ +/** + * Rect - Rectangle + * Représente un rectangle (pour bounds, collisions, UVs...) + */ + +import { Vector2 } from './Vector2'; + +export class Rect { + public x: number; + public y: number; + public width: number; + public height: number; + + constructor(x: number = 0, y: number = 0, width: number = 0, height: number = 0) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** + * Bord gauche + */ + get left(): number { + return this.x; + } + + /** + * Bord droit + */ + get right(): number { + return this.x + this.width; + } + + /** + * Bord haut (WebGL: Y vers le haut = plus petit) + */ + get top(): number { + return this.y; + } + + /** + * Bord bas + */ + get bottom(): number { + return this.y + this.height; + } + + /** + * Centre du rectangle + */ + get center(): Vector2 { + return new Vector2(this.x + this.width / 2, this.y + this.height / 2); + } + + /** + * Taille du rectangle (width, height) + */ + get size(): Vector2 { + return new Vector2(this.width, this.height); + } + + /** + * Teste si un point est à l'intérieur du rectangle + */ + contains(point: Vector2): boolean { + return point.x >= this.left && + point.x <= this.right && + point.y >= this.top && + point.y <= this.bottom; + } + + /** + * Teste si deux rectangles se chevauchent + */ + overlaps(other: Rect): boolean { + return this.left < other.right && + this.right > other.left && + this.top < other.bottom && + this.bottom > other.top; + } + + /** + * Calcule l'intersection de deux rectangles + * Retourne null si pas d'intersection + */ + intersection(other: Rect): Rect | null { + const left = Math.max(this.left, other.left); + const right = Math.min(this.right, other.right); + const top = Math.max(this.top, other.top); + const bottom = Math.min(this.bottom, other.bottom); + + if (left >= right || top >= bottom) { + return null; // Pas d'intersection + } + + return new Rect(left, top, right - left, bottom - top); + } + + /** + * Calcule l'union (plus petit rectangle contenant les deux) + */ + union(other: Rect): Rect { + const left = Math.min(this.left, other.left); + const right = Math.max(this.right, other.right); + const top = Math.min(this.top, other.top); + const bottom = Math.max(this.bottom, other.bottom); + + return new Rect(left, top, right - left, bottom - top); + } + + /** + * Agrandit le rectangle de chaque côté par `amount` + */ + expand(amount: number): Rect { + return new Rect( + this.x - amount, + this.y - amount, + this.width + amount * 2, + this.height + amount * 2 + ); + } + + /** + * Réduit le rectangle de chaque côté par `amount` + */ + shrink(amount: number): Rect { + const doubleAmount = amount * 2; + return new Rect( + this.x + amount, + this.y + amount, + Math.max(0, this.width - doubleAmount), + Math.max(0, this.height - doubleAmount) + ); + } + + /** + * Copie ce rectangle + */ + clone(): Rect { + return new Rect(this.x, this.y, this.width, this.height); + } + + /** + * Copie les valeurs d'un autre rectangle + */ + copyFrom(other: Rect): Rect { + this.x = other.x; + this.y = other.y; + this.width = other.width; + this.height = other.height; + return this; + } + + /** + * Définit les valeurs x, y, width, height + */ + set(x: number, y: number, width: number, height: number): Rect { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + return this; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Rect(${this.x}, ${this.y}, ${this.width}, ${this.height})`; + } + + /** + * Rectangle vide (width et height à 0) + */ + static readonly empty = new Rect(0, 0, 0, 0); +} + diff --git a/src/engine/math/Vector2.ts b/src/engine/math/Vector2.ts new file mode 100644 index 0000000..d541d53 --- /dev/null +++ b/src/engine/math/Vector2.ts @@ -0,0 +1,246 @@ +/** + * Vector2 - Vecteur 2D + * Représente un point ou une direction en 2D + */ + +export class Vector2 { + public x: number; + public y: number; + + constructor(x: number = 0, y: number = 0) { + this.x = x; + this.y = y; + } + + /** + * Addition de deux vecteurs (retourne un nouveau vecteur) + */ + add(other: Vector2): Vector2 { + return new Vector2(this.x + other.x, this.y + other.y); + } + + /** + * Addition mutative (modifie ce vecteur) + */ + addMut(other: Vector2): Vector2 { + this.x += other.x; + this.y += other.y; + return this; + } + + /** + * Soustraction de deux vecteurs (retourne un nouveau vecteur) + */ + subtract(other: Vector2): Vector2 { + return new Vector2(this.x - other.x, this.y - other.y); + } + + /** + * Soustraction mutative (modifie ce vecteur) + */ + subtractMut(other: Vector2): Vector2 { + this.x -= other.x; + this.y -= other.y; + return this; + } + + /** + * Multiplication par un scalaire (retourne un nouveau vecteur) + */ + multiply(scalar: number): Vector2 { + return new Vector2(this.x * scalar, this.y * scalar); + } + + /** + * Multiplication mutative (modifie ce vecteur) + */ + multiplyMut(scalar: number): Vector2 { + this.x *= scalar; + this.y *= scalar; + return this; + } + + /** + * Division par un scalaire (retourne un nouveau vecteur) + */ + divide(scalar: number): Vector2 { + if (scalar === 0) { + throw new Error('Division par zéro'); + } + return new Vector2(this.x / scalar, this.y / scalar); + } + + /** + * Division mutative (modifie ce vecteur) + */ + divideMut(scalar: number): Vector2 { + if (scalar === 0) { + throw new Error('Division par zéro'); + } + this.x /= scalar; + this.y /= scalar; + return this; + } + + /** + * Longueur (magnitude) du vecteur + */ + length(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + /** + * Longueur au carré (plus rapide, évite la racine carrée) + */ + lengthSquared(): number { + return this.x * this.x + this.y * this.y; + } + + /** + * Normalise le vecteur (retourne un nouveau vecteur de longueur 1) + */ + normalize(): Vector2 { + const len = this.length(); + if (len === 0) { + return new Vector2(0, 0); + } + return new Vector2(this.x / len, this.y / len); + } + + /** + * Normalise mutatif (modifie ce vecteur pour qu'il ait une longueur de 1) + */ + normalizeMut(): Vector2 { + const len = this.length(); + if (len === 0) { + this.x = 0; + this.y = 0; + return this; + } + this.x /= len; + this.y /= len; + return this; + } + + /** + * Produit scalaire avec un autre vecteur + */ + dot(other: Vector2): number { + return this.x * other.x + this.y * other.y; + } + + /** + * Distance entre ce point et un autre + */ + distance(other: Vector2): number { + const dx = this.x - other.x; + const dy = this.y - other.y; + return Math.sqrt(dx * dx + dy * dy); + } + + /** + * Distance au carré (plus rapide) + */ + distanceSquared(other: Vector2): number { + const dx = this.x - other.x; + const dy = this.y - other.y; + return dx * dx + dy * dy; + } + + /** + * Interpolation linéaire vers un autre vecteur + * @param other Vecteur de destination + * @param t Facteur d'interpolation (0 = ce vecteur, 1 = other) + */ + lerp(other: Vector2, t: number): Vector2 { + const clampedT = Math.max(0, Math.min(1, t)); + return new Vector2( + this.x + (other.x - this.x) * clampedT, + this.y + (other.y - this.y) * clampedT + ); + } + + /** + * Rotation du vecteur (retourne un nouveau vecteur) + * @param angle Angle en radians + */ + rotate(angle: number): Vector2 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + return new Vector2( + this.x * cos - this.y * sin, + this.x * sin + this.y * cos + ); + } + + /** + * Rotation mutative (modifie ce vecteur) + * @param angle Angle en radians + */ + rotateMut(angle: number): Vector2 { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + const newX = this.x * cos - this.y * sin; + const newY = this.x * sin + this.y * cos; + this.x = newX; + this.y = newY; + return this; + } + + /** + * Angle du vecteur en radians (de 0 à 2π) + */ + angle(): number { + return Math.atan2(this.y, this.x); + } + + /** + * Copie ce vecteur + */ + clone(): Vector2 { + return new Vector2(this.x, this.y); + } + + /** + * Copie les valeurs d'un autre vecteur + */ + copyFrom(other: Vector2): Vector2 { + this.x = other.x; + this.y = other.y; + return this; + } + + /** + * Définit les valeurs x et y + */ + set(x: number, y: number): Vector2 { + this.x = x; + this.y = y; + return this; + } + + /** + * Retourne un tableau [x, y] + */ + toArray(): [number, number] { + return [this.x, this.y]; + } + + /** + * Retourne une représentation string + */ + toString(): string { + return `Vector2(${this.x}, ${this.y})`; + } + + /** + * Constantes statiques + */ + static readonly zero = new Vector2(0, 0); + static readonly one = new Vector2(1, 1); + static readonly up = new Vector2(0, -1); // WebGL: Y vers le haut = négatif + static readonly down = new Vector2(0, 1); + static readonly left = new Vector2(-1, 0); + static readonly right = new Vector2(1, 0); +} + diff --git a/src/engine/physics/BoxCollider2D.ts b/src/engine/physics/BoxCollider2D.ts new file mode 100644 index 0000000..9b91120 --- /dev/null +++ b/src/engine/physics/BoxCollider2D.ts @@ -0,0 +1,115 @@ +/** + * BoxCollider2D - Collider rectangulaire (AABB - Axis-Aligned Bounding Box) + */ + +import { Collider2D } from './Collider2D'; +import { Vector2 } from '../math/Vector2'; +import { Rect } from '../math/Rect'; +import { CircleCollider2D } from './CircleCollider2D'; + +export class BoxCollider2D extends Collider2D { + private _size: Vector2 = new Vector2(1, 1); + + /** + * Taille du collider (largeur, hauteur) + */ + get size(): Vector2 { + return this._size; + } + + set size(value: Vector2) { + this._size = value.clone(); + } + + /** + * Largeur du collider + */ + get width(): number { + return this._size.x; + } + + set width(value: number) { + this._size.x = value; + } + + /** + * Hauteur du collider + */ + get height(): number { + return this._size.y; + } + + set height(value: number) { + this._size.y = value; + } + + /** + * Retourne le rectangle de collision dans l'espace monde + */ + getBounds(): { x: number; y: number; width: number; height: number } { + const pos = this.worldPosition; + // Centre le rectangle sur la position (comme Unity) + return { + x: pos.x - this._size.x / 2, + y: pos.y - this._size.y / 2, + width: this._size.x, + height: this._size.y + }; + } + + /** + * Retourne le rectangle de collision + */ + getRect(): Rect { + const bounds = this.getBounds(); + return new Rect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + /** + * Teste la collision avec un autre collider + */ + intersects(other: Collider2D): boolean { + if (other instanceof BoxCollider2D) { + return this._intersectsBox(other); + } + if (other instanceof CircleCollider2D) { + return this._intersectsCircle(other); + } + return false; + } + + /** + * Teste la collision avec un autre BoxCollider2D (AABB) + */ + private _intersectsBox(other: BoxCollider2D): boolean { + const thisBounds = this.getBounds(); + const otherBounds = other.getBounds(); + + return thisBounds.x < otherBounds.x + otherBounds.width && + thisBounds.x + thisBounds.width > otherBounds.x && + thisBounds.y < otherBounds.y + otherBounds.height && + thisBounds.y + thisBounds.height > otherBounds.y; + } + + /** + * Teste la collision avec un CircleCollider2D + */ + private _intersectsCircle(other: CircleCollider2D): boolean { + const thisBounds = this.getBounds(); + const thisRect = new Rect(thisBounds.x, thisBounds.y, thisBounds.width, thisBounds.height); + const circleCenter = other.worldPosition; + const circleRadius = other.radius; + + // Trouve le point le plus proche du cercle dans le rectangle + const closestX = Math.max(thisRect.left, Math.min(circleCenter.x, thisRect.right)); + const closestY = Math.max(thisRect.top, Math.min(circleCenter.y, thisRect.bottom)); + + // Calcule la distance du point le plus proche au centre du cercle + const distanceX = circleCenter.x - closestX; + const distanceY = circleCenter.y - closestY; + const distanceSquared = distanceX * distanceX + distanceY * distanceY; + + return distanceSquared < circleRadius * circleRadius; + } +} + diff --git a/src/engine/physics/CircleCollider2D.ts b/src/engine/physics/CircleCollider2D.ts new file mode 100644 index 0000000..f991968 --- /dev/null +++ b/src/engine/physics/CircleCollider2D.ts @@ -0,0 +1,70 @@ +/** + * CircleCollider2D - Collider circulaire + */ + +import { Collider2D } from './Collider2D'; +import { BoxCollider2D } from './BoxCollider2D'; + +export class CircleCollider2D extends Collider2D { + private _radius: number = 0.5; + + /** + * Rayon du collider + */ + get radius(): number { + return this._radius; + } + + set radius(value: number) { + this._radius = Math.max(0, value); + } + + /** + * Diamètre du collider + */ + get diameter(): number { + return this._radius * 2; + } + + set diameter(value: number) { + this._radius = Math.max(0, value / 2); + } + + /** + * Retourne les bounds (rectangle englobant) du collider + */ + getBounds(): { x: number; y: number; width: number; height: number } { + const pos = this.worldPosition; + return { + x: pos.x - this._radius, + y: pos.y - this._radius, + width: this._radius * 2, + height: this._radius * 2 + }; + } + + /** + * Teste la collision avec un autre collider + */ + intersects(other: Collider2D): boolean { + if (other instanceof CircleCollider2D) { + return this._intersectsCircle(other); + } + // BoxCollider2D gère déjà Box-Circle dans son intersects() + if (other instanceof BoxCollider2D) { + return other.intersects(this); // Délègue à BoxCollider2D (qui gère Box-Circle) + } + return false; + } + + /** + * Teste la collision avec un autre CircleCollider2D + */ + private _intersectsCircle(other: CircleCollider2D): boolean { + const thisPos = this.worldPosition; + const otherPos = other.worldPosition; + const distance = thisPos.distance(otherPos); + return distance < (this._radius + other.radius); + } +} + diff --git a/src/engine/physics/Collider2D.ts b/src/engine/physics/Collider2D.ts new file mode 100644 index 0000000..9accf0b --- /dev/null +++ b/src/engine/physics/Collider2D.ts @@ -0,0 +1,207 @@ +/** + * Collider2D - Classe de base abstraite pour les colliders 2D + * Gère les collisions et les événements de trigger + */ + +import { Component } from '../entities/Component'; +import { Vector2 } from '../math/Vector2'; + +export type CollisionCallback = (other: Collider2D) => void; + +export abstract class Collider2D extends Component { + private _isTrigger: boolean = false; + private _offset: Vector2 = new Vector2(0, 0); + + // Événements de collision + private _onTriggerEnterCallbacks: CollisionCallback[] = []; + private _onTriggerExitCallbacks: CollisionCallback[] = []; + private _onCollisionEnterCallbacks: CollisionCallback[] = []; + private _onCollisionExitCallbacks: CollisionCallback[] = []; + + // Suivi des collisions actuelles (pour détecter exit) + private _currentTriggers: Set = new Set(); + private _currentCollisions: Set = new Set(); + + /** + * Si true, le collider est un trigger (pas de résolution physique) + */ + get isTrigger(): boolean { + return this._isTrigger; + } + + set isTrigger(value: boolean) { + this._isTrigger = value; + } + + /** + * Offset du collider par rapport au Transform + */ + get offset(): Vector2 { + return this._offset; + } + + set offset(value: Vector2) { + this._offset = value.clone(); + } + + /** + * Position du collider dans l'espace monde (position + offset) + */ + get worldPosition(): Vector2 { + const pos = this.transform.position; + if (this._offset.x === 0 && this._offset.y === 0) { + return pos; + } + return pos.add(this._offset); + } + + /** + * Ajoute un callback pour OnTriggerEnter + */ + onTriggerEnter(callback: CollisionCallback): void { + this._onTriggerEnterCallbacks.push(callback); + } + + /** + * Retire un callback pour OnTriggerEnter + */ + removeTriggerEnter(callback: CollisionCallback): void { + const index = this._onTriggerEnterCallbacks.indexOf(callback); + if (index !== -1) { + this._onTriggerEnterCallbacks.splice(index, 1); + } + } + + /** + * Ajoute un callback pour OnTriggerExit + */ + onTriggerExit(callback: CollisionCallback): void { + this._onTriggerExitCallbacks.push(callback); + } + + /** + * Retire un callback pour OnTriggerExit + */ + removeTriggerExit(callback: CollisionCallback): void { + const index = this._onTriggerExitCallbacks.indexOf(callback); + if (index !== -1) { + this._onTriggerExitCallbacks.splice(index, 1); + } + } + + /** + * Ajoute un callback pour OnCollisionEnter + */ + onCollisionEnter(callback: CollisionCallback): void { + this._onCollisionEnterCallbacks.push(callback); + } + + /** + * Retire un callback pour OnCollisionEnter + */ + removeCollisionEnter(callback: CollisionCallback): void { + const index = this._onCollisionEnterCallbacks.indexOf(callback); + if (index !== -1) { + this._onCollisionEnterCallbacks.splice(index, 1); + } + } + + /** + * Ajoute un callback pour OnCollisionExit + */ + onCollisionExit(callback: CollisionCallback): void { + this._onCollisionExitCallbacks.push(callback); + } + + /** + * Retire un callback pour OnCollisionExit + */ + removeCollisionExit(callback: CollisionCallback): void { + const index = this._onCollisionExitCallbacks.indexOf(callback); + if (index !== -1) { + this._onCollisionExitCallbacks.splice(index, 1); + } + } + + /** + * Méthode abstraite : Teste la collision avec un autre collider + */ + abstract intersects(other: Collider2D): boolean; + + /** + * Méthode abstraite : Calcule le bounds (Rectangle englobant) du collider + */ + abstract getBounds(): { x: number; y: number; width: number; height: number }; + + /** + * Appelé par PhysicsSystem quand une collision est détectée + * @internal + */ + _notifyCollision(other: Collider2D): void { + if (this._isTrigger || other._isTrigger) { + // Trigger event + if (!this._currentTriggers.has(other)) { + this._currentTriggers.add(other); + for (const callback of this._onTriggerEnterCallbacks) { + callback(other); + } + } + } else { + // Collision event + if (!this._currentCollisions.has(other)) { + this._currentCollisions.add(other); + for (const callback of this._onCollisionEnterCallbacks) { + callback(other); + } + } + } + } + + /** + * Appelé par PhysicsSystem après la détection de collisions + * Nettoie les collisions qui n'existent plus + * @internal + */ + _cleanupCollisions(currentCollisions: Set): void { + // Triggers + const triggersToRemove: Collider2D[] = []; + for (const trigger of this._currentTriggers) { + if (!currentCollisions.has(trigger)) { + triggersToRemove.push(trigger); + } + } + for (const trigger of triggersToRemove) { + this._currentTriggers.delete(trigger); + for (const callback of this._onTriggerExitCallbacks) { + callback(trigger); + } + } + + // Collisions + const collisionsToRemove: Collider2D[] = []; + for (const collision of this._currentCollisions) { + if (!currentCollisions.has(collision)) { + collisionsToRemove.push(collision); + } + } + for (const collision of collisionsToRemove) { + this._currentCollisions.delete(collision); + for (const callback of this._onCollisionExitCallbacks) { + callback(collision); + } + } + } + + /** + * Nettoie les ressources + */ + onDestroy(): void { + this._onTriggerEnterCallbacks.length = 0; + this._onTriggerExitCallbacks.length = 0; + this._onCollisionEnterCallbacks.length = 0; + this._onCollisionExitCallbacks.length = 0; + this._currentTriggers.clear(); + this._currentCollisions.clear(); + } +} + diff --git a/src/engine/physics/PhysicsSystem.ts b/src/engine/physics/PhysicsSystem.ts new file mode 100644 index 0000000..9b1c679 --- /dev/null +++ b/src/engine/physics/PhysicsSystem.ts @@ -0,0 +1,323 @@ +/** + * PhysicsSystem - Système de détection et résolution de collisions + * Gère la détection de collisions et l'intégration de la physique + */ + +import { Scene } from '../core/Scene'; +import { GameObject } from '../entities/GameObject'; +import { Collider2D } from './Collider2D'; +import { BoxCollider2D } from './BoxCollider2D'; +import { CircleCollider2D } from './CircleCollider2D'; +import { Rigidbody2D } from './Rigidbody2D'; +import { Vector2 } from '../math/Vector2'; +import { Time } from '../core/Time'; + +export class PhysicsSystem { + private _fixedTimeStep: number = 1 / 60; // 60 FPS fixe pour la physique + private _accumulator: number = 0; + + /** + * Timestep fixe pour la physique (en secondes) + */ + get fixedTimeStep(): number { + return this._fixedTimeStep; + } + + set fixedTimeStep(value: number) { + this._fixedTimeStep = Math.max(0.001, value); + } + + /** + * Update du système de physique (appelé chaque frame) + * Utilise un fixed timestep pour la stabilité + */ + update(scene: Scene): void { + // Fixed timestep accumulation + this._accumulator += Time.deltaTime; + + // Exécute plusieurs fixed updates si nécessaire + const maxIterations = 5; // Limite pour éviter le spiral of death + let iterations = 0; + + while (this._accumulator >= this._fixedTimeStep && iterations < maxIterations) { + this._fixedUpdate(scene, this._fixedTimeStep); + this._accumulator -= this._fixedTimeStep; + iterations++; + } + + // Si l'accumulateur devient trop grand, on le limite + if (this._accumulator > this._fixedTimeStep * 2) { + this._accumulator = this._fixedTimeStep; + } + } + + /** + * Fixed update (physique stable) + */ + private _fixedUpdate(scene: Scene, fixedDeltaTime: number): void { + // 1. Update tous les Rigidbody2D + this._updateRigidbodies(scene, fixedDeltaTime); + + // 2. Détecte les collisions + this._detectCollisions(scene); + } + + /** + * Update tous les Rigidbody2D de la scène + */ + private _updateRigidbodies(scene: Scene, fixedDeltaTime: number): void { + const gameObjects = scene.gameObjects; + + for (const obj of gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + // Update récursif des enfants + this._updateRigidbodyRecursive(obj, fixedDeltaTime); + } + } + + /** + * Update récursif des Rigidbody2D + */ + private _updateRigidbodyRecursive(obj: GameObject, fixedDeltaTime: number): void { + if (!obj.active || obj.isDestroyed) { + return; + } + + const rigidbody = obj.getComponent(Rigidbody2D); + if (rigidbody && rigidbody.enabled) { + rigidbody._fixedUpdate(fixedDeltaTime); + } + + // Parcourt les enfants + for (const child of obj.children) { + this._updateRigidbodyRecursive(child, fixedDeltaTime); + } + } + + /** + * Détecte toutes les collisions dans la scène + */ + private _detectCollisions(scene: Scene): void { + const colliders: Collider2D[] = []; + + // Collecte tous les colliders actifs + this._collectColliders(scene, colliders); + + // Suivi des collisions actuelles (pour cleanup des événements Exit) + const collisionMap = new Map>(); + + // Teste chaque paire de colliders + for (let i = 0; i < colliders.length; i++) { + const colliderA = colliders[i]!; + + if (!colliderA.enabled || !colliderA.gameObject.active) { + continue; + } + + const collisionsA = new Set(); + + for (let j = i + 1; j < colliders.length; j++) { + const colliderB = colliders[j]!; + + if (!colliderB.enabled || !colliderB.gameObject.active) { + continue; + } + + // Ignore les colliders du même GameObject + if (colliderA.gameObject === colliderB.gameObject) { + continue; + } + + // Teste la collision + if (colliderA.intersects(colliderB)) { + collisionsA.add(colliderB); + + // Notifie les deux colliders + colliderA._notifyCollision(colliderB); + colliderB._notifyCollision(colliderA); + + // Résolution physique (si pas de trigger) + if (!colliderA.isTrigger && !colliderB.isTrigger) { + this._resolveCollision(colliderA, colliderB); + } + } + } + + collisionMap.set(colliderA, collisionsA); + } + + // Cleanup des événements Exit + for (const [collider, currentCollisions] of collisionMap) { + collider._cleanupCollisions(currentCollisions); + } + } + + /** + * Collecte récursivement tous les colliders de la scène + */ + private _collectColliders(scene: Scene, colliders: Collider2D[]): void { + const gameObjects = scene.gameObjects; + + for (const obj of gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + this._collectCollidersRecursive(obj, colliders); + } + } + + /** + * Collecte récursivement les colliders d'un GameObject + */ + private _collectCollidersRecursive(obj: GameObject, colliders: Collider2D[]): void { + if (!obj.active || obj.isDestroyed) { + return; + } + + // Cherche BoxCollider2D + const boxCollider = obj.getComponent(BoxCollider2D); + if (boxCollider && boxCollider.enabled) { + colliders.push(boxCollider); + } + + // Cherche CircleCollider2D + const circleCollider = obj.getComponent(CircleCollider2D); + if (circleCollider && circleCollider.enabled) { + colliders.push(circleCollider); + } + + // Parcourt les enfants + for (const child of obj.children) { + this._collectCollidersRecursive(child, colliders); + } + } + + /** + * Résout une collision physique (sépare les objets) + */ + private _resolveCollision(colliderA: Collider2D, colliderB: Collider2D): void { + const rigidbodyA = colliderA.getComponent(Rigidbody2D); + const rigidbodyB = colliderB.getComponent(Rigidbody2D); + + // Si aucun des deux n'a de rigidbody, pas de résolution + if (!rigidbodyA && !rigidbodyB) { + return; + } + + // Calcul de la séparation (basique - pour BoxCollider2D) + if (colliderA instanceof BoxCollider2D && colliderB instanceof BoxCollider2D) { + this._resolveBoxBoxCollision(colliderA, colliderB, rigidbodyA, rigidbodyB); + } else { + // Pour CircleCollider2D ou Box-Circle, résolution simple + this._resolveSimpleCollision(colliderA, colliderB, rigidbodyA, rigidbodyB); + } + } + + /** + * Résout une collision Box-Box (AABB) + */ + private _resolveBoxBoxCollision( + colliderA: BoxCollider2D, + colliderB: BoxCollider2D, + rigidbodyA: Rigidbody2D | null, + rigidbodyB: Rigidbody2D | null + ): void { + const boundsA = colliderA.getBounds(); + const boundsB = colliderB.getBounds(); + + // Calcule la pénétration + const overlapX = Math.min( + boundsA.x + boundsA.width - boundsB.x, + boundsB.x + boundsB.width - boundsA.x + ); + const overlapY = Math.min( + boundsA.y + boundsA.height - boundsB.y, + boundsB.y + boundsB.height - boundsA.y + ); + + // Séparation dans la direction de la plus petite pénétration + if (overlapX < overlapY) { + const direction = boundsA.x < boundsB.x ? -1 : 1; + const separation = overlapX * direction; + + if (rigidbodyA && !rigidbodyB) { + colliderA.transform.position = colliderA.transform.position.add(new Vector2(separation, 0)); + } else if (rigidbodyB && !rigidbodyA) { + colliderB.transform.position = colliderB.transform.position.add(new Vector2(-separation, 0)); + } else if (rigidbodyA && rigidbodyB) { + // Les deux bougent : séparation proportionnelle à la masse + const totalMass = rigidbodyA.mass + rigidbodyB.mass; + const ratioA = rigidbodyB.mass / totalMass; + const ratioB = rigidbodyA.mass / totalMass; + colliderA.transform.position = colliderA.transform.position.add(new Vector2(separation * ratioA, 0)); + colliderB.transform.position = colliderB.transform.position.add(new Vector2(-separation * ratioB, 0)); + } + } else { + const direction = boundsA.y < boundsB.y ? -1 : 1; + const separation = overlapY * direction; + + if (rigidbodyA && !rigidbodyB) { + colliderA.transform.position = colliderA.transform.position.add(new Vector2(0, separation)); + } else if (rigidbodyB && !rigidbodyA) { + colliderB.transform.position = colliderB.transform.position.add(new Vector2(0, -separation)); + } else if (rigidbodyA && rigidbodyB) { + const totalMass = rigidbodyA.mass + rigidbodyB.mass; + const ratioA = rigidbodyB.mass / totalMass; + const ratioB = rigidbodyA.mass / totalMass; + colliderA.transform.position = colliderA.transform.position.add(new Vector2(0, separation * ratioA)); + colliderB.transform.position = colliderB.transform.position.add(new Vector2(0, -separation * ratioB)); + } + } + } + + /** + * Résout une collision simple (pour Circle ou Box-Circle) + */ + private _resolveSimpleCollision( + colliderA: Collider2D, + colliderB: Collider2D, + rigidbodyA: Rigidbody2D | null, + rigidbodyB: Rigidbody2D | null + ): void { + const posA = colliderA.worldPosition; + const posB = colliderB.worldPosition; + const direction = posB.subtract(posA); + const distance = direction.length(); + + if (distance === 0) { + return; // Évite la division par zéro + } + + const normalized = direction.multiply(1 / distance); + + // Séparation minimale (pour BoxCollider2D, utilise la moitié de la taille) + let separation = 0; + if (colliderA instanceof BoxCollider2D && colliderB instanceof BoxCollider2D) { + const sizeA = colliderA.size; + const sizeB = colliderB.size; + separation = (sizeA.x + sizeA.y + sizeB.x + sizeB.y) / 4; // Approximation + } else { + // Pour les cercles ou autres + separation = distance; + } + + const correction = normalized.multiply((separation - distance) / 2); + + if (rigidbodyA && !rigidbodyB) { + colliderA.transform.position = colliderA.transform.position.add(correction); + } else if (rigidbodyB && !rigidbodyA) { + colliderB.transform.position = colliderB.transform.position.add(correction.multiply(-1)); + } else if (rigidbodyA && rigidbodyB) { + const totalMass = rigidbodyA.mass + rigidbodyB.mass; + const ratioA = rigidbodyB.mass / totalMass; + const ratioB = rigidbodyA.mass / totalMass; + colliderA.transform.position = colliderA.transform.position.add(correction.multiply(ratioA)); + colliderB.transform.position = colliderB.transform.position.add(correction.multiply(-ratioB)); + } + } +} + diff --git a/src/engine/physics/Rigidbody2D.ts b/src/engine/physics/Rigidbody2D.ts new file mode 100644 index 0000000..0c03f2d --- /dev/null +++ b/src/engine/physics/Rigidbody2D.ts @@ -0,0 +1,184 @@ +/** + * Rigidbody2D - Physique de mouvement 2D + * Gère la vélocité, les forces et l'intégration du mouvement + */ + +import { Component } from '../entities/Component'; +import { Vector2 } from '../math/Vector2'; +import { Time } from '../core/Time'; + +export class Rigidbody2D extends Component { + private _velocity: Vector2 = new Vector2(0, 0); + private _angularVelocity: number = 0; // degrés par seconde + + private _mass: number = 1; + private _drag: number = 0; // Friction linéaire (0 = pas de friction) + private _angularDrag: number = 0; // Friction angulaire + + private _useGravity: boolean = false; + private _gravityScale: number = 1; + + // Constante de gravité (pixels par seconde²) + private static readonly GRAVITY: number = 980; // ~9.8 m/s² * 100 pixels/mètre + + /** + * Vélocité linéaire (pixels par seconde) + */ + get velocity(): Vector2 { + return this._velocity; + } + + set velocity(value: Vector2) { + this._velocity = value.clone(); + } + + /** + * Vélocité angulaire (degrés par seconde) + */ + get angularVelocity(): number { + return this._angularVelocity; + } + + set angularVelocity(value: number) { + this._angularVelocity = value; + } + + /** + * Masse de l'objet + */ + get mass(): number { + return this._mass; + } + + set mass(value: number) { + this._mass = Math.max(0.001, value); // Évite la division par zéro + } + + /** + * Friction linéaire (drag) + */ + get drag(): number { + return this._drag; + } + + set drag(value: number) { + this._drag = Math.max(0, value); + } + + /** + * Friction angulaire + */ + get angularDrag(): number { + return this._angularDrag; + } + + set angularDrag(value: number) { + this._angularDrag = Math.max(0, value); + } + + /** + * Si true, l'objet est affecté par la gravité + */ + get useGravity(): boolean { + return this._useGravity; + } + + set useGravity(value: boolean) { + this._useGravity = value; + } + + /** + * Multiplicateur de gravité + */ + get gravityScale(): number { + return this._gravityScale; + } + + set gravityScale(value: number) { + this._gravityScale = value; + } + + /** + * Ajoute une force à l'objet (modifie la vélocité) + * @param force Force en newtons (sera divisée par la masse) + */ + addForce(force: Vector2): void { + // F = ma => a = F/m + const acceleration = force.multiply(1 / this._mass); + this._velocity.addMut(acceleration.multiply(Time.deltaTime)); + } + + /** + * Ajoute une impulsion (modifie la vélocité instantanément) + * @param impulse Impulsion (sera divisée par la masse) + */ + addImpulse(impulse: Vector2): void { + const velocityChange = impulse.multiply(1 / this._mass); + this._velocity.addMut(velocityChange); + } + + /** + * Ajoute un couple (rotation) + * @param torque Couple en newtons-mètres + */ + addTorque(torque: number): void { + // Moment d'inertie approximatif pour un rectangle + const momentOfInertia = this._mass * 0.0833; // I = m * (w² + h²) / 12, approximé + const angularAcceleration = torque / momentOfInertia; + this._angularVelocity += angularAcceleration * Time.deltaTime; + } + + /** + * Définit la vélocité directement (ignorant les forces) + */ + setVelocity(velocity: Vector2): void { + this._velocity = velocity.clone(); + } + + /** + * Met la vélocité à zéro + */ + stop(): void { + this._velocity = new Vector2(0, 0); + this._angularVelocity = 0; + } + + /** + * Update du mouvement (appelé par PhysicsSystem) + * @internal + */ + _fixedUpdate(fixedDeltaTime: number): void { + // Gravité + if (this._useGravity) { + this._velocity.y -= Rigidbody2D.GRAVITY * this._gravityScale * fixedDeltaTime; + } + + // Drag (friction) + if (this._drag > 0) { + const dragForce = this._velocity.multiply(-this._drag * fixedDeltaTime); + this._velocity.addMut(dragForce); + // Arrête la vélocité si elle devient très petite + if (this._velocity.lengthSquared() < 0.01) { + this._velocity = new Vector2(0, 0); + } + } + + // Rotation avec drag + if (this._angularDrag > 0) { + this._angularVelocity *= (1 - this._angularDrag * fixedDeltaTime); + if (Math.abs(this._angularVelocity) < 0.01) { + this._angularVelocity = 0; + } + } + + // Intégration du mouvement + const deltaMovement = this._velocity.multiply(fixedDeltaTime); + this.transform.translate(deltaMovement); + + // Rotation + if (this._angularVelocity !== 0) { + this.transform.rotate(this._angularVelocity * fixedDeltaTime); + } + } +} + diff --git a/src/engine/physics/index.ts b/src/engine/physics/index.ts new file mode 100644 index 0000000..b3c47e3 --- /dev/null +++ b/src/engine/physics/index.ts @@ -0,0 +1,10 @@ +/** + * Exports du système de physique + */ + +export { Collider2D, type CollisionCallback } from './Collider2D'; +export { BoxCollider2D } from './BoxCollider2D'; +export { CircleCollider2D } from './CircleCollider2D'; +export { Rigidbody2D } from './Rigidbody2D'; +export { PhysicsSystem } from './PhysicsSystem'; + diff --git a/src/engine/rendering/Camera.ts b/src/engine/rendering/Camera.ts new file mode 100644 index 0000000..ef053af --- /dev/null +++ b/src/engine/rendering/Camera.ts @@ -0,0 +1,253 @@ +/** + * Camera - Viewport et transformations + * Définit quelle partie du monde est visible + */ + +import { Vector2 } from '../math/Vector2'; +import { Matrix3 } from '../math/Matrix3'; +import { Rect } from '../math/Rect'; + +export class Camera { + private _position: Vector2 = new Vector2(0, 0); + private _zoom: number = 1.0; + private _rotation: number = 0; // En radians + + private _viewportWidth: number = 800; + private _viewportHeight: number = 600; + + private _bounds: Rect | null = null; + + // Cache des matrices (dirty flags) + private _projectionMatrix: Matrix3 | null = null; + private _viewMatrix: Matrix3 | null = null; + private _viewProjMatrix: Matrix3 | null = null; + private _dirty: boolean = true; + + constructor(viewportWidth: number = 800, viewportHeight: number = 600) { + this._viewportWidth = viewportWidth; + this._viewportHeight = viewportHeight; + this._dirty = true; + } + + /** + * Position de la caméra dans le monde + */ + get position(): Vector2 { + return this._position; + } + + set position(value: Vector2) { + this._position = value; + this._markDirty(); + this._clampToBounds(); + } + + /** + * Zoom (1 = normal, 2 = 2× plus proche, 0.5 = 2× plus loin) + */ + get zoom(): number { + return this._zoom; + } + + set zoom(value: number) { + this._zoom = Math.max(0.01, value); // Évite le zoom négatif ou zéro + this._markDirty(); + } + + /** + * Rotation en radians + */ + get rotation(): number { + return this._rotation; + } + + set rotation(value: number) { + this._rotation = value; + this._markDirty(); + } + + /** + * Largeur du viewport + */ + get viewportWidth(): number { + return this._viewportWidth; + } + + set viewportWidth(value: number) { + this._viewportWidth = value; + this._markDirty(); + } + + /** + * Hauteur du viewport + */ + get viewportHeight(): number { + return this._viewportHeight; + } + + set viewportHeight(value: number) { + this._viewportHeight = value; + this._markDirty(); + } + + /** + * Limites de déplacement (optionnel) + */ + get bounds(): Rect | null { + return this._bounds; + } + + set bounds(value: Rect | null) { + this._bounds = value; + this._clampToBounds(); + } + + /** + * Marque les matrices comme "sales" (besoin de recalculer) + */ + private _markDirty(): void { + this._dirty = true; + } + + /** + * Clamp la position aux bounds si définis + */ + private _clampToBounds(): void { + if (!this._bounds) { + return; + } + + const halfWidth = (this._viewportWidth / this._zoom) / 2; + const halfHeight = (this._viewportHeight / this._zoom) / 2; + + this._position.x = Math.max( + this._bounds.left + halfWidth, + Math.min(this._bounds.right - halfWidth, this._position.x) + ); + this._position.y = Math.max( + this._bounds.top + halfHeight, + Math.min(this._bounds.bottom - halfHeight, this._position.y) + ); + } + + /** + * Calcule la matrice de projection orthographique + * Convertit coordonnées pixel en coordonnées NDC WebGL (-1 à 1) + * Centre l'écran sur (0, 0) pour que la caméra fonctionne correctement + */ + getProjectionMatrix(): Matrix3 { + if (this._projectionMatrix && !this._dirty) { + return this._projectionMatrix; + } + + // Projection orthographique centrée sur le viewport + // Centre de l'écran = (viewportWidth/2, viewportHeight/2) + const centerX = this._viewportWidth / 2; + const centerY = this._viewportHeight / 2; + + this._projectionMatrix = Matrix3.orthographic( + -centerX, // left + centerX, // right + centerY, // bottom (WebGL: Y positif = bas) + -centerY // top (WebGL: Y négatif = haut) + ); + + return this._projectionMatrix; + } + + /** + * Calcule la matrice de vue + * Applique position, rotation, zoom de la caméra + */ + getViewMatrix(): Matrix3 { + if (this._viewMatrix && !this._dirty) { + return this._viewMatrix; + } + + // Ordre : Translate → Rotate → Scale (zoom) + this._viewMatrix = Matrix3.identity() + .translate(-this._position.x, -this._position.y) + .rotate(-this._rotation) + .scale(this._zoom, this._zoom); + + return this._viewMatrix; + } + + /** + * Calcule la matrice View × Projection (combinaison) + */ + getViewProjectionMatrix(): Matrix3 { + if (this._viewProjMatrix && !this._dirty) { + return this._viewProjMatrix; + } + + const proj = this.getProjectionMatrix(); + const view = this.getViewMatrix(); + + this._viewProjMatrix = proj.clone().multiply(view); + this._dirty = false; // Marque comme propre maintenant + + return this._viewProjMatrix; + } + + /** + * Convertit coordonnées écran (pixels) → coordonnées monde + */ + screenToWorld(screenPos: Vector2): Vector2 { + // Normalise les coordonnées écran (0-1) + const normalizedX = (screenPos.x / this._viewportWidth) * 2 - 1; + const normalizedY = (screenPos.y / this._viewportHeight) * 2 - 1; + + // Inverse la projection + const worldX = normalizedX * (this._viewportWidth / this._zoom) + this._position.x; + const worldY = normalizedY * (this._viewportHeight / this._zoom) + this._position.y; + + return new Vector2(worldX, worldY); + } + + /** + * Convertit coordonnées monde → coordonnées écran (pixels) + */ + worldToScreen(worldPos: Vector2): Vector2 { + const viewProj = this.getViewProjectionMatrix(); + const transformed = viewProj.transformPoint(worldPos); + + // Convertit NDC (-1 à 1) en pixels + const screenX = ((transformed.x + 1) / 2) * this._viewportWidth; + const screenY = ((transformed.y + 1) / 2) * this._viewportHeight; + + return new Vector2(screenX, screenY); + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this._viewportWidth = width; + this._viewportHeight = height; + this._markDirty(); + } + + /** + * Centre la caméra sur une position + */ + lookAt(target: Vector2): void { + this.position = target; + } + + /** + * Récupère le rectangle visible dans le monde (pour culling) + */ + getVisibleWorldBounds(): Rect { + const halfWidth = (this._viewportWidth / this._zoom) / 2; + const halfHeight = (this._viewportHeight / this._zoom) / 2; + + return new Rect( + this._position.x - halfWidth, + this._position.y - halfHeight, + this._viewportWidth / this._zoom, + this._viewportHeight / this._zoom + ); + } +} + diff --git a/src/engine/rendering/Shader.ts b/src/engine/rendering/Shader.ts new file mode 100644 index 0000000..88af074 --- /dev/null +++ b/src/engine/rendering/Shader.ts @@ -0,0 +1,121 @@ +/** + * Shader - Gestion des programmes GPU + * Compile et gère les shaders vertex et fragment + */ + +export class Shader { + private _program: WebGLProgram | null = null; + private _vertexShader: WebGLShader | null = null; + private _fragmentShader: WebGLShader | null = null; + + /** + * Compile un shader depuis le code source + */ + compile(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): boolean { + // Compile vertex shader + this._vertexShader = this.compileShader(gl, gl.VERTEX_SHADER, vertexSource); + if (!this._vertexShader) { + return false; + } + + // Compile fragment shader + this._fragmentShader = this.compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource); + if (!this._fragmentShader) { + return false; + } + + // Crée et lie le programme + this._program = gl.createProgram(); + if (!this._program) { + console.error('Impossible de créer le programme WebGL'); + return false; + } + + gl.attachShader(this._program, this._vertexShader); + gl.attachShader(this._program, this._fragmentShader); + gl.linkProgram(this._program); + + // Vérifie les erreurs de linking + if (!gl.getProgramParameter(this._program, gl.LINK_STATUS)) { + const info = gl.getProgramInfoLog(this._program); + console.error('Erreur de linking du shader:', info); + gl.deleteProgram(this._program); + this._program = null; + return false; + } + + return true; + } + + /** + * Compile un shader individuel + */ + private compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null { + const shader = gl.createShader(type); + if (!shader) { + console.error('Impossible de créer le shader'); + return null; + } + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const info = gl.getShaderInfoLog(shader); + console.error(`Erreur de compilation du shader (${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'}):`, info); + gl.deleteShader(shader); + return null; + } + + return shader; + } + + /** + * Active ce shader pour le rendu + */ + use(gl: WebGLRenderingContext): void { + if (!this._program) { + throw new Error('Shader non compilé'); + } + gl.useProgram(this._program); + } + + /** + * Récupère l'emplacement d'un attribut + */ + getAttributeLocation(gl: WebGLRenderingContext, name: string): number { + if (!this._program) { + throw new Error('Shader non compilé'); + } + return gl.getAttribLocation(this._program, name); + } + + /** + * Récupère l'emplacement d'un uniform + */ + getUniformLocation(gl: WebGLRenderingContext, name: string): WebGLUniformLocation | null { + if (!this._program) { + throw new Error('Shader non compilé'); + } + return gl.getUniformLocation(this._program, name); + } + + /** + * Nettoie les ressources + */ + dispose(gl: WebGLRenderingContext): void { + if (this._program) { + gl.deleteProgram(this._program); + this._program = null; + } + if (this._vertexShader) { + gl.deleteShader(this._vertexShader); + this._vertexShader = null; + } + if (this._fragmentShader) { + gl.deleteShader(this._fragmentShader); + this._fragmentShader = null; + } + } +} + diff --git a/src/engine/rendering/SpriteBatch.ts b/src/engine/rendering/SpriteBatch.ts new file mode 100644 index 0000000..17c2d1c --- /dev/null +++ b/src/engine/rendering/SpriteBatch.ts @@ -0,0 +1,316 @@ +/** + * SpriteBatch - Optimisation batch rendering + * Accumule plusieurs sprites et les dessine en un seul draw call + */ + +import { GLContext } from '../webgl/GLContext'; +import { Buffer, BufferType, BufferUsage } from '../webgl/Buffer'; +import { Texture } from './Texture'; +import { Matrix3 } from '../math/Matrix3'; +import { Vector2 } from '../math/Vector2'; +import { Rect } from '../math/Rect'; +import { Color } from '../math/Color'; +import { Shader } from './Shader'; + +// Structure d'un vertex : [x, y, u, v, r, g, b, a] = 8 floats +const FLOATS_PER_VERTEX = 8; +const VERTICES_PER_SPRITE = 4; // Un quad = 4 vertices +const INDICES_PER_SPRITE = 6; // 2 triangles = 6 indices + +export class SpriteBatch { + private _glContext: GLContext; + private _maxSprites: number = 10000; + + // Buffers CPU + private _vertices: Float32Array; + private _indices: Uint16Array; + + // Buffers GPU + private _vertexBuffer: Buffer | null = null; + private _indexBuffer: Buffer | null = null; + + // État du batch + private _spriteCount: number = 0; + private _currentTexture: Texture | null = null; + private _isBatching: boolean = false; + + // Shader actif (pour configurer les attributs) + private _shader: Shader | null = null; + + // Matrices pour les uniforms (stockées depuis begin()) + private _projectionMatrix: Matrix3 | null = null; + private _viewMatrix: Matrix3 | null = null; + + constructor(glContext: GLContext, maxSprites: number = 10000) { + this._glContext = glContext; + this._maxSprites = maxSprites; + + // Alloue les buffers CPU + this._vertices = new Float32Array(maxSprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX); + this._indices = new Uint16Array(maxSprites * INDICES_PER_SPRITE); + + // Crée les buffers GPU + this._vertexBuffer = new Buffer( + glContext.gl, + BufferType.ARRAY_BUFFER, + BufferUsage.DYNAMIC_DRAW + ); + + this._indexBuffer = new Buffer( + glContext.gl, + BufferType.ELEMENT_ARRAY_BUFFER, + BufferUsage.STATIC_DRAW + ); + + // Prépare les indices (fixe pour tous les sprites) + this._generateIndices(); + } + + /** + * Génère les indices pour tous les sprites (une seule fois) + */ + private _generateIndices(): void { + for (let i = 0; i < this._maxSprites; i++) { + const baseIndex = i * VERTICES_PER_SPRITE; + const indexOffset = i * INDICES_PER_SPRITE; + + // Premier triangle: 0, 1, 2 + this._indices[indexOffset + 0] = baseIndex + 0; + this._indices[indexOffset + 1] = baseIndex + 1; + this._indices[indexOffset + 2] = baseIndex + 2; + + // Second triangle: 2, 1, 3 + this._indices[indexOffset + 3] = baseIndex + 2; + this._indices[indexOffset + 4] = baseIndex + 1; + this._indices[indexOffset + 5] = baseIndex + 3; + } + + // Upload les indices (statique) + this._indexBuffer!.uploadData(this._indices); + } + + /** + * Commence un batch de rendu + * @param projection Matrice de projection (utilisée par le shader via uniform) + * @param view Matrice de vue (utilisée par le shader via uniform) + * @param shader Shader à utiliser pour configurer les attributs + */ + begin(projection: Matrix3, view: Matrix3, shader?: Shader): void { + if (this._isBatching) { + console.warn('SpriteBatch déjà en cours, appel end() d\'abord'); + return; + } + + this._shader = shader || null; + this._projectionMatrix = projection; + this._viewMatrix = view; + this._spriteCount = 0; + this._currentTexture = null; + this._isBatching = true; + } + + /** + * Ajoute un sprite au batch + */ + draw( + texture: Texture, + position: Vector2, + sourceRect: Rect | null = null, + tint: Color = Color.white, + rotation: number = 0, + scale: Vector2 = Vector2.one, + pivot: Vector2 = new Vector2(0.5, 0.5) + ): void { + if (!this._isBatching) { + console.warn('SpriteBatch non démarré, appel begin() d\'abord'); + return; + } + + // Flush si le batch est plein ou si la texture change + if (this._spriteCount >= this._maxSprites || + (this._currentTexture && texture !== this._currentTexture)) { + this.flush(); + } + + // Si flush a été appelé, réinitialise pour la nouvelle texture + if (this._spriteCount === 0) { + this._currentTexture = texture; + } + + // Calcule les UVs + const srcRect = sourceRect || new Rect(0, 0, texture.width, texture.height); + const u1 = srcRect.left / texture.width; + const v1 = srcRect.top / texture.height; + const u2 = srcRect.right / texture.width; + const v2 = srcRect.bottom / texture.height; + + // Calcule les 4 coins du sprite avec rotation et scale + const halfWidth = (srcRect.width * scale.x) / 2; + const halfHeight = (srcRect.height * scale.y) / 2; + + // Offset du pivot + const pivotOffsetX = (pivot.x - 0.5) * srcRect.width * scale.x; + const pivotOffsetY = (pivot.y - 0.5) * srcRect.height * scale.y; + + // Les 4 coins locaux (avant rotation) + const corners = [ + new Vector2(-halfWidth - pivotOffsetX, -halfHeight - pivotOffsetY), // Top-left + new Vector2( halfWidth - pivotOffsetX, -halfHeight - pivotOffsetY), // Top-right + new Vector2(-halfWidth - pivotOffsetX, halfHeight - pivotOffsetY), // Bottom-left + new Vector2( halfWidth - pivotOffsetX, halfHeight - pivotOffsetY) // Bottom-right + ]; + + // Applique la rotation si nécessaire + if (rotation !== 0) { + for (let i = 0; i < corners.length; i++) { + const corner = corners[i]; + if (corner) { + corners[i] = corner.rotate(rotation); + } + } + } + + // Ajoute la position et transforme les vertices + const vertexOffset = this._spriteCount * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX; + const r = tint.r; + const g = tint.g; + const b = tint.b; + const a = tint.a; + + // Calcule les positions monde de chaque coin (le shader fera la transformation view/projection) + for (let i = 0; i < corners.length; i++) { + const corner = corners[i]; + if (!corner) continue; + + // Position monde = coin local + position du transform + const worldPos = corner.add(position); + + const vOffset = vertexOffset + i * FLOATS_PER_VERTEX; + const u = i < 2 ? u1 : u2; + const v = (i === 0 || i === 2) ? v1 : v2; + + // [x, y, u, v, r, g, b, a] + // x, y = position monde (le shader appliquera view/projection) + this._vertices[vOffset + 0] = worldPos.x; + this._vertices[vOffset + 1] = worldPos.y; + this._vertices[vOffset + 2] = u; + this._vertices[vOffset + 3] = v; + this._vertices[vOffset + 4] = r; + this._vertices[vOffset + 5] = g; + this._vertices[vOffset + 6] = b; + this._vertices[vOffset + 7] = a; + } + + this._spriteCount++; + } + + /** + * Envoie tout au GPU et dessine + */ + flush(): void { + if (this._spriteCount === 0 || !this._currentTexture) { + return; + } + + if (!this._shader) { + console.warn('SpriteBatch.flush() appelé sans shader actif'); + return; + } + + const gl = this._glContext.gl; + + // Active le shader AVANT de configurer les attributs et de dessiner + this._shader.use(gl); + + // Définit les uniforms des matrices (doit être fait après l'activation du shader) + if (this._projectionMatrix && this._viewMatrix) { + const projLoc = this._shader.getUniformLocation(gl, 'uProjection'); + const viewLoc = this._shader.getUniformLocation(gl, 'uView'); + + if (projLoc) { + gl.uniformMatrix3fv(projLoc, false, this._projectionMatrix.toArray()); + } + if (viewLoc) { + gl.uniformMatrix3fv(viewLoc, false, this._viewMatrix.toArray()); + } + } + + // Upload les vertices vers le GPU + const verticesUsed = this._spriteCount * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX; + const verticesSubset = this._vertices.subarray(0, verticesUsed); + this._vertexBuffer!.uploadData(verticesSubset); + + // Active et bind les buffers + this._vertexBuffer!.bind(); + this._indexBuffer!.bind(); + + // Configure les attributs du shader + const positionLoc = this._shader.getAttributeLocation(gl, 'aPosition'); + const texCoordLoc = this._shader.getAttributeLocation(gl, 'aTexCoord'); + const colorLoc = this._shader.getAttributeLocation(gl, 'aColor'); + + const stride = FLOATS_PER_VERTEX * 4; // 32 bytes (8 floats * 4 bytes) + const BYTES_PER_FLOAT = 4; + + // aPosition: [x, y] - offset 0, 2 floats + if (positionLoc >= 0) { + gl.enableVertexAttribArray(positionLoc); + gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, stride, 0); + } + + // aTexCoord: [u, v] - offset 8 bytes, 2 floats + if (texCoordLoc >= 0) { + gl.enableVertexAttribArray(texCoordLoc); + gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, stride, 2 * BYTES_PER_FLOAT); + } + + // aColor: [r, g, b, a] - offset 16 bytes, 4 floats + if (colorLoc >= 0) { + gl.enableVertexAttribArray(colorLoc); + gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, stride, 4 * BYTES_PER_FLOAT); + } + + // Bind la texture + this._currentTexture.bind(0); + + // Dessine + const indicesUsed = this._spriteCount * INDICES_PER_SPRITE; + gl.drawElements( + gl.TRIANGLES, + indicesUsed, + gl.UNSIGNED_SHORT, + 0 + ); + + // Reset pour le prochain batch + this._spriteCount = 0; + this._currentTexture = null; + } + + /** + * Termine le batch (flush automatique) + */ + end(): void { + if (!this._isBatching) { + return; + } + + this.flush(); + this._isBatching = false; + } + + /** + * Nettoie les ressources + */ + dispose(): void { + if (this._vertexBuffer) { + this._vertexBuffer.dispose(); + this._vertexBuffer = null; + } + if (this._indexBuffer) { + this._indexBuffer.dispose(); + this._indexBuffer = null; + } + } +} + diff --git a/src/engine/rendering/Texture.ts b/src/engine/rendering/Texture.ts new file mode 100644 index 0000000..ed39f7c --- /dev/null +++ b/src/engine/rendering/Texture.ts @@ -0,0 +1,220 @@ +/** + * Texture - Gestion des images WebGL + * Encapsule une texture WebGL (image sur le GPU) + */ + +export enum FilterMode { + NEAREST = WebGLRenderingContext.NEAREST, + LINEAR = WebGLRenderingContext.LINEAR +} + +export enum WrapMode { + CLAMP_TO_EDGE = WebGLRenderingContext.CLAMP_TO_EDGE, + REPEAT = WebGLRenderingContext.REPEAT, + MIRRORED_REPEAT = WebGLRenderingContext.MIRRORED_REPEAT +} + +export class Texture { + private _gl: WebGLRenderingContext; + private _texture: WebGLTexture | null = null; + private _width: number = 0; + private _height: number = 0; + private _id: number; + + private _filterMode: FilterMode = FilterMode.LINEAR; + private _wrapMode: WrapMode = WrapMode.CLAMP_TO_EDGE; + + private static _nextId = 1; + + constructor(gl: WebGLRenderingContext) { + this._gl = gl; + this._id = Texture._nextId++; + + // Crée la texture WebGL + const texture = gl.createTexture(); + if (!texture) { + throw new Error('Impossible de créer la texture WebGL'); + } + this._texture = texture; + } + + /** + * Charge une texture depuis une image HTML + */ + loadFromImage(image: HTMLImageElement): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._width = image.width; + this._height = image.height; + + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + + // Upload l'image vers le GPU + this._gl.texImage2D( + this._gl.TEXTURE_2D, + 0, + this._gl.RGBA, + this._gl.RGBA, + this._gl.UNSIGNED_BYTE, + image + ); + + // Configure le filtrage + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._filterMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._filterMode); + + // Configure le wrapping + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._wrapMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._wrapMode); + + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Charge une texture depuis une URL (asynchrone) + */ + static async loadFromURL(gl: WebGLRenderingContext, url: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + image.crossOrigin = 'anonymous'; + + image.onload = () => { + const texture = new Texture(gl); + texture.loadFromImage(image); + resolve(texture); + }; + + image.onerror = () => { + reject(new Error(`Impossible de charger l'image: ${url}`)); + }; + + image.src = url; + }); + } + + /** + * Crée une texture vide (utile pour rendu vers texture) + */ + createEmpty(width: number, height: number): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._width = width; + this._height = height; + + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + + this._gl.texImage2D( + this._gl.TEXTURE_2D, + 0, + this._gl.RGBA, + width, + height, + 0, + this._gl.RGBA, + this._gl.UNSIGNED_BYTE, + null + ); + + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._filterMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._filterMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._wrapMode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._wrapMode); + + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Active la texture sur un slot (0-31) + */ + bind(slot: number = 0): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + const gl = this._gl; + gl.activeTexture(gl.TEXTURE0 + slot); + gl.bindTexture(gl.TEXTURE_2D, this._texture); + } + + /** + * Désactive la texture + */ + unbind(): void { + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Définit le mode de filtrage + */ + setFilterMode(mode: FilterMode): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._filterMode = mode; + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, mode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, mode); + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Définit le mode de wrapping + */ + setWrapMode(mode: WrapMode): void { + if (!this._texture) { + throw new Error('Texture détruite'); + } + + this._wrapMode = mode; + this._gl.bindTexture(this._gl.TEXTURE_2D, this._texture); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, mode); + this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, mode); + this._gl.bindTexture(this._gl.TEXTURE_2D, null); + } + + /** + * Largeur de la texture + */ + get width(): number { + return this._width; + } + + /** + * Hauteur de la texture + */ + get height(): number { + return this._height; + } + + /** + * Identifiant unique de la texture + */ + get id(): number { + return this._id; + } + + /** + * Texture WebGL native + */ + get glTexture(): WebGLTexture | null { + return this._texture; + } + + /** + * Libère la mémoire GPU + */ + dispose(): void { + if (this._texture) { + this._gl.deleteTexture(this._texture); + this._texture = null; + this._width = 0; + this._height = 0; + } + } +} + diff --git a/src/engine/rendering/WebGLRenderer.ts b/src/engine/rendering/WebGLRenderer.ts new file mode 100644 index 0000000..2727e3e --- /dev/null +++ b/src/engine/rendering/WebGLRenderer.ts @@ -0,0 +1,313 @@ +/** + * WebGLRenderer - Coordonne le rendu + * Chef d'orchestre du système de rendu + */ + +import { GLContext } from '../webgl/GLContext'; +import { Shader } from './Shader'; +import { Texture } from './Texture'; +import { Camera } from './Camera'; +import { Color } from '../math/Color'; +import { Scene } from '../core/Scene'; +import { SpriteBatch } from './SpriteBatch'; + +export class WebGLRenderer { + private _glContext: GLContext; + private _spriteBatch: SpriteBatch; + private _shaderLibrary: Map = new Map(); + private _textureCache: Map = new Map(); + private _defaultShader: Shader | null = null; + private _currentShader: Shader | null = null; + private _currentTexture: Texture | null = null; + private _viewportWidth: number = 800; + private _viewportHeight: number = 600; + + constructor(glContext: GLContext) { + this._glContext = glContext; + this._viewportWidth = glContext.canvas.width; + this._viewportHeight = glContext.canvas.height; + this._spriteBatch = new SpriteBatch(glContext); + } + + /** + * Initialise le renderer et crée le shader par défaut + */ + initialize(): boolean { + // Crée le shader sprite par défaut + const vertexShader = ` + attribute vec2 aPosition; + attribute vec2 aTexCoord; + attribute vec4 aColor; + + uniform mat3 uProjection; + uniform mat3 uView; + + varying vec2 vTexCoord; + varying vec4 vColor; + + void main() { + vec3 pos = uProjection * uView * vec3(aPosition, 1.0); + gl_Position = vec4(pos.xy, 0.0, 1.0); + vTexCoord = aTexCoord; + vColor = aColor; + } + `; + + const fragmentShader = ` + precision mediump float; + + uniform sampler2D uTexture; + + varying vec2 vTexCoord; + varying vec4 vColor; + + void main() { + vec4 texColor = texture2D(uTexture, vTexCoord); + gl_FragColor = texColor * vColor; + } + `; + + const shader = new Shader(); + if (!shader.compile(this._glContext.gl, vertexShader, fragmentShader)) { + console.error('Échec de la compilation du shader par défaut'); + return false; + } + + this._defaultShader = shader; + this._currentShader = shader; + this.loadShader('default', vertexShader, fragmentShader); + + return true; + } + + /** + * Charge et enregistre un shader + */ + loadShader(name: string, vertexSource: string, fragmentSource: string): Shader | null { + // Vérifie si le shader existe déjà + if (this._shaderLibrary.has(name)) { + console.warn(`Shader "${name}" déjà chargé`); + return this._shaderLibrary.get(name) || null; + } + + const shader = new Shader(); + if (!shader.compile(this._glContext.gl, vertexSource, fragmentSource)) { + console.error(`Échec de la compilation du shader "${name}"`); + return null; + } + + this._shaderLibrary.set(name, shader); + return shader; + } + + /** + * Récupère un shader par son nom + */ + getShader(name: string): Shader | null { + return this._shaderLibrary.get(name) || null; + } + + /** + * Charge une texture depuis une URL et la met en cache + */ + async loadTexture(name: string, url: string): Promise { + // Vérifie si la texture est déjà en cache + if (this._textureCache.has(name)) { + console.warn(`Texture "${name}" déjà chargée`); + return this._textureCache.get(name) || null; + } + + try { + const texture = await Texture.loadFromURL(this._glContext.gl, url); + this._textureCache.set(name, texture); + return texture; + } catch (error) { + console.error(`Échec du chargement de la texture "${name}":`, error); + return null; + } + } + + /** + * Récupère une texture par son nom + */ + getTexture(name: string): Texture | null { + return this._textureCache.get(name) || null; + } + + /** + * Efface le canvas avec une couleur + */ + clear(color?: Color): void { + if (color) { + this._glContext.gl.clearColor(color.r, color.g, color.b, color.a); + } + this._glContext.clear(); + } + + /** + * Active un shader + */ + useShader(shader: Shader | string | null): void { + let targetShader: Shader | null = null; + + if (shader === null) { + targetShader = this._defaultShader; + } else if (typeof shader === 'string') { + targetShader = this.getShader(shader); + if (!targetShader) { + console.warn(`Shader "${shader}" introuvable, utilisation du shader par défaut`); + targetShader = this._defaultShader; + } + } else { + targetShader = shader; + } + + if (targetShader && targetShader !== this._currentShader) { + targetShader.use(this._glContext.gl); + this._currentShader = targetShader; + } + } + + /** + * Active une texture sur le slot 0 + */ + useTexture(texture: Texture | null): void { + if (texture && texture !== this._currentTexture) { + texture.bind(0); + this._currentTexture = texture; + } else if (!texture) { + this._glContext.gl.bindTexture(this._glContext.gl.TEXTURE_2D, null); + this._currentTexture = null; + } + } + + /** + * Rend une scène avec une caméra + */ + render(scene: Scene, camera: Camera): void { + // Met à jour la taille du viewport si nécessaire + if (camera.viewportWidth !== this._viewportWidth || + camera.viewportHeight !== this._viewportHeight) { + this.resize(camera.viewportWidth, camera.viewportHeight); + } + + // Clear avec fond blanc + this.clear(Color.white); + + // Récupère les matrices de la caméra + const proj = camera.getProjectionMatrix(); + const view = camera.getViewMatrix(); + + // S'assure que le shader par défaut existe + if (!this._defaultShader) { + console.error('Aucun shader par défaut disponible'); + return; + } + + // Commence le batch avec les matrices de caméra et le shader actif + // Les uniforms seront définis dans SpriteBatch.flush() après l'activation du shader + this._spriteBatch.begin(proj, view, this._defaultShader); + + // Collecte et rend tous les SpriteRenderer de la scène + const renderables = scene.getAllRenderables(); + for (const renderable of renderables) { + renderable.render(this._spriteBatch); + } + + // Termine le batch (flush automatique) + this._spriteBatch.end(); + + // Rend l'UI en overlay (par-dessus la scène) + const uiCanvas = scene.uiCanvas; + if (uiCanvas) { + const uiCamera = uiCanvas.camera; + const uiProj = uiCamera.getProjectionMatrix(); + const uiView = uiCamera.getViewMatrix(); + + // Commence un nouveau batch pour l'UI + this._spriteBatch.begin(uiProj, uiView, this._defaultShader); + + // Rend tous les éléments UI + uiCanvas.render(this._spriteBatch); + + // Termine le batch UI + this._spriteBatch.end(); + } + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this._viewportWidth = width; + this._viewportHeight = height; + this._glContext.resize(width, height); + } + + /** + * Récupère le contexte WebGL + */ + get gl(): WebGLRenderingContext { + return this._glContext.gl; + } + + /** + * Récupère le GLContext + */ + get glContext(): GLContext { + return this._glContext; + } + + /** + * Largeur du viewport + */ + get viewportWidth(): number { + return this._viewportWidth; + } + + /** + * Hauteur du viewport + */ + get viewportHeight(): number { + return this._viewportHeight; + } + + /** + * Shader par défaut + */ + get defaultShader(): Shader | null { + return this._defaultShader; + } + + /** + * SpriteBatch (pour accès externe si nécessaire) + */ + get spriteBatch(): SpriteBatch { + return this._spriteBatch; + } + + /** + * Nettoie les ressources + */ + dispose(): void { + // Nettoie tous les shaders + for (const shader of this._shaderLibrary.values()) { + shader.dispose(this._glContext.gl); + } + this._shaderLibrary.clear(); + + // Nettoie toutes les textures + for (const texture of this._textureCache.values()) { + texture.dispose(); + } + this._textureCache.clear(); + + this._defaultShader = null; + this._currentShader = null; + this._currentTexture = null; + + // Nettoie le SpriteBatch + this._spriteBatch.dispose(); + } +} + diff --git a/src/engine/ui/RectTransform.ts b/src/engine/ui/RectTransform.ts new file mode 100644 index 0000000..216cfc9 --- /dev/null +++ b/src/engine/ui/RectTransform.ts @@ -0,0 +1,250 @@ +/** + * RectTransform - Gestion du positionnement UI avec anchors + * Similaire à Unity RectTransform mais simplifié + */ + +import { Vector2 } from '../math/Vector2'; +import { Rect } from '../math/Rect'; + +export enum AnchorPreset { + TopLeft, + TopCenter, + TopRight, + MiddleLeft, + MiddleCenter, + MiddleRight, + BottomLeft, + BottomCenter, + BottomRight, + StretchHorizontal, + StretchVertical, + StretchAll +} + +export class RectTransform { + private _anchoredPosition: Vector2 = new Vector2(0, 0); + private _size: Vector2 = new Vector2(100, 100); + private _anchorMin: Vector2 = new Vector2(0, 0); // 0-1 (coin bas-gauche de l'anchor) + private _anchorMax: Vector2 = new Vector2(0, 0); // 0-1 (coin haut-droite de l'anchor) + private _pivot: Vector2 = new Vector2(0.5, 0.5); // 0-1 (point de référence pour rotation/scale) + + // Référence au parent (null = écran) + private _parent: RectTransform | null = null; + private _canvasSize: Vector2 = new Vector2(800, 600); + + /** + * Position ancrée (offset depuis l'anchor) + */ + get anchoredPosition(): Vector2 { + return this._anchoredPosition; + } + + set anchoredPosition(value: Vector2) { + this._anchoredPosition = value.clone(); + } + + /** + * Taille du rectangle (width, height) + */ + get size(): Vector2 { + return this._size; + } + + set size(value: Vector2) { + this._size = value.clone(); + } + + /** + * Largeur + */ + get width(): number { + return this._size.x; + } + + set width(value: number) { + this._size.x = value; + } + + /** + * Hauteur + */ + get height(): number { + return this._size.y; + } + + set height(value: number) { + this._size.y = value; + } + + /** + * Anchor minimum (coin bas-gauche, 0-1) + */ + get anchorMin(): Vector2 { + return this._anchorMin; + } + + set anchorMin(value: Vector2) { + this._anchorMin = value.clone(); + } + + /** + * Anchor maximum (coin haut-droite, 0-1) + */ + get anchorMax(): Vector2 { + return this._anchorMax; + } + + set anchorMax(value: Vector2) { + this._anchorMax = value.clone(); + } + + /** + * Pivot (point de référence, 0-1) + */ + get pivot(): Vector2 { + return this._pivot; + } + + set pivot(value: Vector2) { + this._pivot = value.clone(); + } + + /** + * Parent RectTransform (null = écran/canvas) + */ + get parent(): RectTransform | null { + return this._parent; + } + + set parent(value: RectTransform | null) { + this._parent = value; + } + + /** + * Taille du canvas (pour calculer les positions) + */ + get canvasSize(): Vector2 { + return this._canvasSize; + } + + set canvasSize(value: Vector2) { + this._canvasSize = value.clone(); + } + + /** + * Retourne le rectangle écran final (position absolue à l'écran) + */ + getRect(): Rect { + // Calcule la position de l'anchor dans l'espace écran + let anchorX: number; + let anchorY: number; + + if (this._parent) { + const parentRect = this._parent.getRect(); + anchorX = parentRect.left + (this._anchorMin.x * parentRect.width); + anchorY = parentRect.top + (this._anchorMin.y * parentRect.height); + } else { + // Relatif au canvas + anchorX = this._anchorMin.x * this._canvasSize.x; + anchorY = this._anchorMin.y * this._canvasSize.y; + } + + // Position finale = anchor + offset + const x = anchorX + this._anchoredPosition.x; + const y = anchorY + this._anchoredPosition.y; + + // Pour WebGL: Y vers le haut = négatif, donc on inverse + // Si anchorMin.y = 1 (haut), on veut y proche de canvasSize.y + // Si anchorMin.y = 0 (bas), on veut y proche de 0 + // Mais dans notre système, Y+ = bas, donc on inverse + const screenY = this._canvasSize.y - y - this._size.y; + + return new Rect(x, screenY, this._size.x, this._size.y); + } + + /** + * Position absolue dans l'espace écran (coin haut-gauche pour WebGL) + */ + get screenPosition(): Vector2 { + const rect = this.getRect(); + return new Vector2(rect.left, rect.top); + } + + /** + * Centre du rectangle dans l'espace écran + */ + get center(): Vector2 { + const rect = this.getRect(); + return rect.center; + } + + /** + * Configure l'anchor avec un preset + * Note: Dans WebGL, Y=0 est en bas, donc on inverse par rapport à l'écran + */ + setAnchorPreset(preset: AnchorPreset): void { + switch (preset) { + case AnchorPreset.TopLeft: + this._anchorMin = new Vector2(0, 1); // 1 = haut (WebGL) + this._anchorMax = new Vector2(0, 1); + this._pivot = new Vector2(0, 1); + break; + case AnchorPreset.TopCenter: + this._anchorMin = new Vector2(0.5, 1); + this._anchorMax = new Vector2(0.5, 1); + this._pivot = new Vector2(0.5, 1); + break; + case AnchorPreset.TopRight: + this._anchorMin = new Vector2(1, 1); + this._anchorMax = new Vector2(1, 1); + this._pivot = new Vector2(1, 1); + break; + case AnchorPreset.MiddleLeft: + this._anchorMin = new Vector2(0, 0.5); + this._anchorMax = new Vector2(0, 0.5); + this._pivot = new Vector2(0, 0.5); + break; + case AnchorPreset.MiddleCenter: + this._anchorMin = new Vector2(0.5, 0.5); + this._anchorMax = new Vector2(0.5, 0.5); + this._pivot = new Vector2(0.5, 0.5); + break; + case AnchorPreset.MiddleRight: + this._anchorMin = new Vector2(1, 0.5); + this._anchorMax = new Vector2(1, 0.5); + this._pivot = new Vector2(1, 0.5); + break; + case AnchorPreset.BottomLeft: + this._anchorMin = new Vector2(0, 0); // 0 = bas (WebGL) + this._anchorMax = new Vector2(0, 0); + this._pivot = new Vector2(0, 0); + break; + case AnchorPreset.BottomCenter: + this._anchorMin = new Vector2(0.5, 0); + this._anchorMax = new Vector2(0.5, 0); + this._pivot = new Vector2(0.5, 0); + break; + case AnchorPreset.BottomRight: + this._anchorMin = new Vector2(1, 0); + this._anchorMax = new Vector2(1, 0); + this._pivot = new Vector2(1, 0); + break; + case AnchorPreset.StretchHorizontal: + this._anchorMin = new Vector2(0, 0.5); + this._anchorMax = new Vector2(1, 0.5); + this._pivot = new Vector2(0.5, 0.5); + break; + case AnchorPreset.StretchVertical: + this._anchorMin = new Vector2(0.5, 0); + this._anchorMax = new Vector2(0.5, 1); + this._pivot = new Vector2(0.5, 0.5); + break; + case AnchorPreset.StretchAll: + this._anchorMin = new Vector2(0, 0); + this._anchorMax = new Vector2(1, 1); + this._pivot = new Vector2(0.5, 0.5); + break; + } + } +} + diff --git a/src/engine/ui/UIButton.ts b/src/engine/ui/UIButton.ts new file mode 100644 index 0000000..f270df5 --- /dev/null +++ b/src/engine/ui/UIButton.ts @@ -0,0 +1,264 @@ +/** + * UIButton - Bouton cliquable avec états + */ + +import { UIComponent } from './UIComponent'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Texture } from '../rendering/Texture'; +import { Color } from '../math/Color'; +import { Vector2 } from '../math/Vector2'; +import { InputManager } from '../input/InputManager'; +// WebGLRenderingContext est un type global + +export enum ButtonState { + Normal, + Hover, + Pressed, + Disabled +} + +export type ButtonCallback = () => void; + +export class UIButton extends UIComponent { + private _backgroundTexture: Texture | null = null; + private _normalColor: Color = new Color(0.2, 0.4, 0.8, 1.0); + private _hoverColor: Color = new Color(0.3, 0.5, 0.9, 1.0); + private _pressedColor: Color = new Color(0.1, 0.3, 0.7, 1.0); + private _disabledColor: Color = new Color(0.5, 0.5, 0.5, 0.5); + + private _state: ButtonState = ButtonState.Normal; + private _onClick: ButtonCallback | null = null; + private _inputManager: InputManager | null = null; + + private _wasPressed: boolean = false; + private _isHovering: boolean = false; + + private _gl: WebGLRenderingContext | WebGL2RenderingContext | null = null; + private _solidColorTexture: Texture | null = null; + + /** + * Texture de fond (optionnel, sinon utilise une couleur unie) + */ + get backgroundTexture(): Texture | null { + return this._backgroundTexture; + } + + set backgroundTexture(value: Texture | null) { + this._backgroundTexture = value; + } + + /** + * Couleur normale + */ + get normalColor(): Color { + return this._normalColor; + } + + set normalColor(value: Color) { + this._normalColor = value; + } + + /** + * Couleur au survol + */ + get hoverColor(): Color { + return this._hoverColor; + } + + set hoverColor(value: Color) { + this._hoverColor = value; + } + + /** + * Couleur quand pressé + */ + get pressedColor(): Color { + return this._pressedColor; + } + + set pressedColor(value: Color) { + this._pressedColor = value; + } + + /** + * État actuel du bouton + */ + get state(): ButtonState { + return this._state; + } + + /** + * Callback appelé quand le bouton est cliqué + */ + set onClick(callback: ButtonCallback | null) { + this._onClick = callback; + } + + /** + * Configure l'InputManager (nécessaire pour détecter les clics) + */ + setInputManager(inputManager: InputManager): void { + this._inputManager = inputManager; + } + + /** + * Initialise avec le contexte WebGL (pour créer les textures de couleur unie) + */ + initialize(gl: WebGLRenderingContext | WebGL2RenderingContext): void { + this._gl = gl; + } + + /** + * Crée une texture de couleur unie pour le fond + */ + private _createSolidColorTexture(color: Color): Texture | null { + if (!this._gl) { + return null; + } + + // Crée un canvas temporaire + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return null; + } + + ctx.fillStyle = color.toRGBA(); + ctx.fillRect(0, 0, 1, 1); + + // Crée ou met à jour la texture + if (!this._solidColorTexture) { + this._solidColorTexture = new Texture(this._gl); + } + + // Upload directement depuis le canvas + if (!this._solidColorTexture || !this._gl) { + return null; + } + const gl = this._gl; + const texture = (this._solidColorTexture as any)._texture; + if (!texture) { + return null; + } + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (this._solidColorTexture as any)._width = 1; + (this._solidColorTexture as any)._height = 1; + return this._solidColorTexture; + } + + /** + * Update pour détecter les interactions + */ + update(_deltaTime: number): void { + if (!this._inputManager || !this.gameObject.active || !this.enabled) { + return; + } + + const mousePos = this._inputManager.getMousePosition(); + const isMouseDown = this._inputManager.isMouseButtonDown(0); // Bouton gauche + + // Teste si la souris est sur le bouton + this._isHovering = this.containsPoint(mousePos); + + // Gère les états + if (this._state === ButtonState.Disabled) { + return; + } + + if (this._isHovering) { + if (isMouseDown) { + this._state = ButtonState.Pressed; + this._wasPressed = true; + } else { + if (this._wasPressed && this._state === ButtonState.Pressed) { + // Relâchement après avoir été pressé = clic + if (this._onClick) { + this._onClick(); + } + this._wasPressed = false; + } + this._state = ButtonState.Hover; + } + } else { + this._state = ButtonState.Normal; + this._wasPressed = false; + } + } + + /** + * Rend le bouton UI + */ + renderUI(spriteBatch: SpriteBatch): void { + if (!this.gameObject.active || !this.enabled) { + return; + } + + const rect = this.rectTransform.getRect(); + const position = new Vector2(rect.center.x, rect.center.y); + + // Choisit la couleur selon l'état + let tint: Color; + switch (this._state) { + case ButtonState.Hover: + tint = this._hoverColor; + break; + case ButtonState.Pressed: + tint = this._pressedColor; + break; + case ButtonState.Disabled: + tint = this._disabledColor; + break; + default: + tint = this._normalColor; + } + + // Si une texture est définie, l'utilise + if (this._backgroundTexture) { + spriteBatch.draw( + this._backgroundTexture, + position, + null, + tint, + 0, + new Vector2(rect.width / this._backgroundTexture.width, rect.height / this._backgroundTexture.height), + new Vector2(0.5, 0.5) + ); + } else { + // Sinon, crée un rectangle de couleur unie avec une texture 1x1 + const colorTexture = this._createSolidColorTexture(tint); + if (colorTexture) { + spriteBatch.draw( + colorTexture, + position, + null, + Color.white, // La couleur est déjà dans la texture + 0, + new Vector2(rect.width, rect.height), + new Vector2(0.5, 0.5) + ); + } + } + } + + /** + * Active ou désactive le bouton + */ + setEnabled(enabled: boolean): void { + this.enabled = enabled; + if (!enabled) { + this._state = ButtonState.Disabled; + } else { + this._state = ButtonState.Normal; + } + } +} + diff --git a/src/engine/ui/UICanvas.ts b/src/engine/ui/UICanvas.ts new file mode 100644 index 0000000..5ec14a7 --- /dev/null +++ b/src/engine/ui/UICanvas.ts @@ -0,0 +1,168 @@ +/** + * UICanvas - Système de gestion de l'interface utilisateur + * Collecte et rend tous les éléments UI + */ + +import { GameObject } from '../entities/GameObject'; +import { UIComponent } from './UIComponent'; +import { UIImage } from './UIImage'; +import { UIText } from './UIText'; +import { UIButton } from './UIButton'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Vector2 } from '../math/Vector2'; +import { Camera } from '../rendering/Camera'; + +export class UICanvas { + private _gameObjects: GameObject[] = []; + private _canvasSize: Vector2 = new Vector2(800, 600); + private _camera: Camera; + + constructor(canvasWidth: number = 800, canvasHeight: number = 600) { + this._canvasSize = new Vector2(canvasWidth, canvasHeight); + // Crée une caméra spéciale pour l'UI (pas de transformation, rendu direct à l'écran) + this._camera = new Camera(canvasWidth, canvasHeight); + this._camera.position = new Vector2(canvasWidth / 2, canvasHeight / 2); + } + + /** + * Taille du canvas UI + */ + get canvasSize(): Vector2 { + return this._canvasSize; + } + + set canvasSize(value: Vector2) { + this._canvasSize = value.clone(); + this._camera.resize(value.x, value.y); + this._camera.position = new Vector2(value.x / 2, value.y / 2); + + // Met à jour tous les UIComponents + for (const obj of this._gameObjects) { + const uiComponent = this._findUIComponent(obj); + if (uiComponent) { + uiComponent.updateCanvasSize(value); + } + } + } + + /** + * Caméra UI (pour le rendu) + */ + get camera(): Camera { + return this._camera; + } + + /** + * Ajoute un GameObject avec UIComponent + */ + addGameObject(gameObject: GameObject): void { + if (this._gameObjects.indexOf(gameObject) !== -1) { + console.warn(`GameObject "${gameObject.name}" déjà dans le UICanvas`); + return; + } + this._gameObjects.push(gameObject); + + // Met à jour la taille du canvas pour le UIComponent + const uiComponent = this._findUIComponent(gameObject); + if (uiComponent) { + uiComponent.updateCanvasSize(this._canvasSize); + } + } + + /** + * Retire un GameObject + */ + removeGameObject(gameObject: GameObject): void { + const index = this._gameObjects.indexOf(gameObject); + if (index !== -1) { + this._gameObjects.splice(index, 1); + } + } + + /** + * Update tous les GameObjects UI (pour les boutons, etc.) + */ + update(deltaTime: number): void { + for (const obj of this._gameObjects) { + if (obj.active && !obj.isDestroyed) { + obj.update(deltaTime); + } + } + + // Nettoie les objets détruits + this._gameObjects = this._gameObjects.filter(obj => !obj.isDestroyed); + } + + /** + * Rend tous les éléments UI + */ + render(spriteBatch: SpriteBatch): void { + // Collecte tous les UIComponents actifs + const uiComponents: UIComponent[] = []; + + for (const obj of this._gameObjects) { + if (!obj.active || obj.isDestroyed) { + continue; + } + + // Cherche les UIComponents dans cet objet et ses enfants + this._collectUIComponentsRecursive(obj, uiComponents); + } + + // Trie par layer si nécessaire (pour l'instant on garde l'ordre d'ajout) + // Rend tous les éléments UI + for (const component of uiComponents) { + if (component.enabled) { + component.renderUI(spriteBatch); + } + } + } + + /** + * Trouve un UIComponent dans un GameObject + */ + private _findUIComponent(obj: GameObject): UIComponent | null { + const uiImage = obj.getComponent(UIImage); + if (uiImage) return uiImage; + + const uiText = obj.getComponent(UIText); + if (uiText) return uiText; + + const uiButton = obj.getComponent(UIButton); + if (uiButton) return uiButton; + + return null; + } + + /** + * Collecte récursivement tous les UIComponents + */ + private _collectUIComponentsRecursive(obj: GameObject, components: UIComponent[]): void { + if (!obj.active || obj.isDestroyed) { + return; + } + + // Cherche les UIComponents dans cet objet + const uiImage = obj.getComponent(UIImage); + if (uiImage && uiImage.enabled) components.push(uiImage); + + const uiText = obj.getComponent(UIText); + if (uiText && uiText.enabled) components.push(uiText); + + const uiButton = obj.getComponent(UIButton); + if (uiButton && uiButton.enabled) components.push(uiButton); + + // Parcourt les enfants + for (const child of obj.children) { + this._collectUIComponentsRecursive(child, components); + } + } + + /** + * Liste de tous les GameObjects UI + */ + get gameObjects(): readonly GameObject[] { + return this._gameObjects; + } +} + diff --git a/src/engine/ui/UIComponent.ts b/src/engine/ui/UIComponent.ts new file mode 100644 index 0000000..a86f53f --- /dev/null +++ b/src/engine/ui/UIComponent.ts @@ -0,0 +1,52 @@ +/** + * UIComponent - Classe de base pour tous les éléments UI + * Gère le RectTransform et le rendu UI + */ + +import { Component } from '../entities/Component'; +import { RectTransform } from './RectTransform'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Vector2 } from '../math/Vector2'; + +export abstract class UIComponent extends Component { + private _rectTransform: RectTransform; + + constructor() { + super(); + this._rectTransform = new RectTransform(); + } + + /** + * RectTransform pour le positionnement UI + */ + get rectTransform(): RectTransform { + return this._rectTransform; + } + + /** + * Méthode abstraite : Rend l'élément UI + */ + abstract renderUI(spriteBatch: SpriteBatch): void; + + /** + * Met à jour la taille du canvas + */ + updateCanvasSize(canvasSize: Vector2): void { + this._rectTransform.canvasSize = canvasSize; + } + + /** + * Teste si un point (en coordonnées écran) est dans cet élément UI + */ + containsPoint(screenPoint: Vector2): boolean { + const rect = this._rectTransform.getRect(); + // Convertit le point écran en coordonnées UI + // WebGL: Y vers le haut = négatif, donc on inverse + const uiY = this._rectTransform.canvasSize.y - screenPoint.y; + return screenPoint.x >= rect.left && + screenPoint.x <= rect.right && + uiY >= rect.top && + uiY <= rect.bottom; + } +} + diff --git a/src/engine/ui/UIImage.ts b/src/engine/ui/UIImage.ts new file mode 100644 index 0000000..f2bbf79 --- /dev/null +++ b/src/engine/ui/UIImage.ts @@ -0,0 +1,139 @@ +/** + * UIImage - Image d'interface utilisateur + */ + +import { UIComponent } from './UIComponent'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Texture } from '../rendering/Texture'; +import { Color } from '../math/Color'; +import { Rect } from '../math/Rect'; +import { Vector2 } from '../math/Vector2'; + +export class UIImage extends UIComponent { + private _texture: Texture | null = null; + private _tint: Color = Color.white; + private _sourceRect: Rect | null = null; + private _shadowEnabled: boolean = false; + private _shadowOffset: Vector2 = new Vector2(2, 2); + private _shadowColor: Color = new Color(0, 0, 0, 0.5); + + /** + * Texture à afficher + */ + get texture(): Texture | null { + return this._texture; + } + + set texture(value: Texture | null) { + this._texture = value; + if (value && !this._sourceRect) { + this._sourceRect = new Rect(0, 0, value.width, value.height); + } + } + + /** + * Couleur de teinte + */ + get tint(): Color { + return this._tint; + } + + set tint(value: Color) { + this._tint = value; + } + + /** + * Rectangle source (pour afficher une partie de la texture) + */ + get sourceRect(): Rect | null { + return this._sourceRect; + } + + set sourceRect(value: Rect | null) { + this._sourceRect = value; + } + + /** + * Active/désactive l'ombre + */ + get shadowEnabled(): boolean { + return this._shadowEnabled; + } + + set shadowEnabled(value: boolean) { + this._shadowEnabled = value; + } + + /** + * Décalage de l'ombre (en pixels) + */ + get shadowOffset(): Vector2 { + return this._shadowOffset; + } + + set shadowOffset(value: Vector2) { + this._shadowOffset = value; + } + + /** + * Couleur de l'ombre + */ + get shadowColor(): Color { + return this._shadowColor; + } + + set shadowColor(value: Color) { + this._shadowColor = value; + } + + /** + * Rend l'image UI + */ + renderUI(spriteBatch: SpriteBatch): void { + if (!this._texture || !this.gameObject.active || !this.enabled) { + return; + } + + const rect = this.rectTransform.getRect(); + const position = new Vector2(rect.center.x, rect.center.y); + const sourceRect = this._sourceRect || new Rect(0, 0, this._texture.width, this._texture.height); + + // Calcule le scale pour que l'image s'adapte à la taille du rectTransform + const scaleX = rect.width / sourceRect.width; + const scaleY = rect.height / sourceRect.height; + const scale = new Vector2(scaleX, scaleY); + + // Utilise la rotation du GameObject (converti en radians) + const rotation = (this.gameObject.transform.rotation * Math.PI) / 180; + + // Dessine l'ombre en premier si activée + if (this._shadowEnabled) { + const shadowPosition = new Vector2( + position.x + this._shadowOffset.x, + position.y + this._shadowOffset.y + ); + + spriteBatch.draw( + this._texture, + shadowPosition, + sourceRect, + this._shadowColor, + rotation, + scale, + new Vector2(0.5, 0.5) // Pivot au centre + ); + } + + // Dessine l'image normale par-dessus + spriteBatch.draw( + this._texture, + position, + sourceRect, + this._tint, + rotation, + scale, + new Vector2(0.5, 0.5) // Pivot au centre + ); + } +} + diff --git a/src/engine/ui/UIText.ts b/src/engine/ui/UIText.ts new file mode 100644 index 0000000..e8b8cd0 --- /dev/null +++ b/src/engine/ui/UIText.ts @@ -0,0 +1,224 @@ +/** + * UIText - Affichage de texte UI + * Génère une texture de texte à partir d'un canvas 2D + */ + +import { UIComponent } from './UIComponent'; +import { SpriteBatch } from '../rendering/SpriteBatch'; +import { Texture } from '../rendering/Texture'; +import { Color } from '../math/Color'; +import { Vector2 } from '../math/Vector2'; + +export class UIText extends UIComponent { + private _text: string = ''; + private _fontSize: number = 16; + private _fontFamily: string = 'Arial'; + private _color: Color = Color.white; + private _textAlign: CanvasTextAlign = 'left'; + private _textBaseline: CanvasTextBaseline = 'top'; + + private _texture: Texture | null = null; + private _gl: WebGLRenderingContext | WebGL2RenderingContext | null = null; + private _dirty: boolean = true; + + /** + * Texte à afficher + */ + get text(): string { + return this._text; + } + + set text(value: string) { + if (this._text !== value) { + this._text = value; + this._dirty = true; + } + } + + /** + * Taille de la police + */ + get fontSize(): number { + return this._fontSize; + } + + set fontSize(value: number) { + if (this._fontSize !== value) { + this._fontSize = value; + this._dirty = true; + } + } + + /** + * Famille de police + */ + get fontFamily(): string { + return this._fontFamily; + } + + set fontFamily(value: string) { + if (this._fontFamily !== value) { + this._fontFamily = value; + this._dirty = true; + } + } + + /** + * Couleur du texte + */ + get color(): Color { + return this._color; + } + + set color(value: Color) { + this._color = value; + this._dirty = true; + } + + /** + * Alignement du texte + */ + get textAlign(): CanvasTextAlign { + return this._textAlign; + } + + set textAlign(value: CanvasTextAlign) { + if (this._textAlign !== value) { + this._textAlign = value; + this._dirty = true; + } + } + + /** + * Initialise avec le contexte WebGL (nécessaire pour créer les textures) + */ + initialize(gl: WebGLRenderingContext | WebGL2RenderingContext): void { + this._gl = gl; + this._dirty = true; + } + + /** + * Génère la texture de texte depuis le canvas 2D + */ + private _generateTexture(): void { + if (!this._gl || this._text === '') { + return; + } + + // Crée un canvas temporaire pour mesurer et dessiner le texte + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + // Configure la police + ctx.font = `${this._fontSize}px ${this._fontFamily}`; + ctx.textAlign = this._textAlign; + ctx.textBaseline = this._textBaseline; + ctx.fillStyle = this._color.toRGBA(); + + // Mesure le texte + const metrics = ctx.measureText(this._text); + const textWidth = Math.ceil(metrics.width); + const textHeight = this._fontSize; + + // Ajuste la taille du canvas + canvas.width = Math.max(1, textWidth); + canvas.height = Math.max(1, textHeight); + + // Réapplique les paramètres (perdus après resize) + ctx.font = `${this._fontSize}px ${this._fontFamily}`; + ctx.textAlign = this._textAlign; + ctx.textBaseline = this._textBaseline; + ctx.fillStyle = this._color.toRGBA(); + + // Dessine le texte + const x = this._textAlign === 'center' ? canvas.width / 2 : + this._textAlign === 'right' ? canvas.width : 0; + const y = this._textBaseline === 'middle' ? canvas.height / 2 : + this._textBaseline === 'bottom' || this._textBaseline === 'alphabetic' ? canvas.height : 0; + + ctx.fillText(this._text, x, y); + + // Crée ou met à jour la texture + if (!this._texture) { + this._texture = new Texture(this._gl); + } + + // Upload directement depuis le canvas (comme dans createSolidColorTexture) + if (!this._texture) { + throw new Error('Texture non créée'); + } + const gl = this._gl; + const texture = (this._texture as any)._texture; + if (!texture) { + throw new Error('Texture WebGL non créée'); + } + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (this._texture as any)._width = canvas.width; + (this._texture as any)._height = canvas.height; + + // Met à jour la taille du rectTransform si nécessaire + if (this.rectTransform.width === 100 && this.rectTransform.height === 100) { + this.rectTransform.width = canvas.width; + this.rectTransform.height = canvas.height; + } + + this._dirty = false; + } + + /** + * Rend le texte UI + */ + renderUI(spriteBatch: SpriteBatch): void { + if (!this.gameObject.active || !this.enabled || this._text === '') { + return; + } + + // Régénère la texture si nécessaire + if (this._dirty || !this._texture) { + this._generateTexture(); + } + + if (!this._texture) { + return; + } + + const rect = this.rectTransform.getRect(); + const position = new Vector2(rect.center.x, rect.center.y); + + // Scale pour adapter au rectTransform si nécessaire + const scaleX = rect.width / this._texture.width; + const scaleY = rect.height / this._texture.height; + const scale = new Vector2(scaleX, scaleY); + + spriteBatch.draw( + this._texture, + position, + null, + Color.white, + 0, + scale, + new Vector2(0.5, 0.5) + ); + } + + /** + * Nettoie la texture + */ + onDestroy(): void { + if (this._texture) { + this._texture.dispose(); + this._texture = null; + } + } +} + diff --git a/src/engine/ui/index.ts b/src/engine/ui/index.ts new file mode 100644 index 0000000..87f82bb --- /dev/null +++ b/src/engine/ui/index.ts @@ -0,0 +1,11 @@ +/** + * Exports du système UI + */ + +export { UICanvas } from './UICanvas'; +export { UIComponent } from './UIComponent'; +export { RectTransform, AnchorPreset } from './RectTransform'; +export { UIImage } from './UIImage'; +export { UIText } from './UIText'; +export { UIButton, ButtonState, type ButtonCallback } from './UIButton'; + diff --git a/src/engine/webgl/Buffer.ts b/src/engine/webgl/Buffer.ts new file mode 100644 index 0000000..a6704dc --- /dev/null +++ b/src/engine/webgl/Buffer.ts @@ -0,0 +1,109 @@ +/** + * Buffer - Encapsule un buffer WebGL + * Gère les vertex buffers et index buffers + */ + +export enum BufferUsage { + STATIC_DRAW = WebGLRenderingContext.STATIC_DRAW, + DYNAMIC_DRAW = WebGLRenderingContext.DYNAMIC_DRAW, + STREAM_DRAW = WebGLRenderingContext.STREAM_DRAW +} + +export enum BufferType { + ARRAY_BUFFER = WebGLRenderingContext.ARRAY_BUFFER, + ELEMENT_ARRAY_BUFFER = WebGLRenderingContext.ELEMENT_ARRAY_BUFFER +} + +export class Buffer { + private _gl: WebGLRenderingContext; + private _buffer: WebGLBuffer | null = null; + private _type: number; + private _usage: number; + private _size: number = 0; + + constructor(gl: WebGLRenderingContext, type: BufferType = BufferType.ARRAY_BUFFER, usage: BufferUsage = BufferUsage.STATIC_DRAW) { + this._gl = gl; + this._type = type; + this._usage = usage; + this._buffer = gl.createBuffer(); + + if (!this._buffer) { + throw new Error('Impossible de créer le buffer WebGL'); + } + } + + /** + * Lie le buffer (active ce buffer) + */ + bind(): void { + if (!this._buffer) { + throw new Error('Buffer détruit'); + } + this._gl.bindBuffer(this._type, this._buffer); + } + + /** + * Délie le buffer + */ + unbind(): void { + this._gl.bindBuffer(this._type, null); + } + + /** + * Upload des données dans le buffer + */ + uploadData(data: Float32Array | Uint16Array | Uint8Array): void { + if (!this._buffer) { + throw new Error('Buffer détruit'); + } + + this.bind(); + this._gl.bufferData(this._type, data, this._usage); + this._size = data.length; + } + + /** + * Upload des données partielles (pour les buffers dynamiques) + */ + uploadSubData(data: Float32Array | Uint16Array | Uint8Array, offset: number = 0): void { + if (!this._buffer) { + throw new Error('Buffer détruit'); + } + + this.bind(); + this._gl.bufferSubData(this._type, offset, data); + } + + /** + * Récupère la taille du buffer (nombre d'éléments) + */ + get size(): number { + return this._size; + } + + /** + * Récupère le type du buffer + */ + get type(): number { + return this._type; + } + + /** + * Libère les ressources GPU + */ + dispose(): void { + if (this._buffer) { + this._gl.deleteBuffer(this._buffer); + this._buffer = null; + this._size = 0; + } + } + + /** + * Vérifie si le buffer est valide + */ + get isValid(): boolean { + return this._buffer !== null; + } +} + diff --git a/src/engine/webgl/GLContext.ts b/src/engine/webgl/GLContext.ts new file mode 100644 index 0000000..1953330 --- /dev/null +++ b/src/engine/webgl/GLContext.ts @@ -0,0 +1,80 @@ +/** + * GLContext - Abstraction WebGL + * Encapsule le contexte WebGL et simplifie l'API + */ + +export class GLContext { + private _gl: WebGLRenderingContext | null = null; + private _canvas: HTMLCanvasElement | null = null; + + /** + * Initialise le contexte WebGL depuis un canvas + */ + initialize(canvas: HTMLCanvasElement): boolean { + this._canvas = canvas; + + // Tentative de récupération du contexte WebGL + this._gl = canvas.getContext('webgl') || canvas.getContext('webgl2') as WebGLRenderingContext | null; + + if (!this._gl) { + console.error('Impossible d\'obtenir le contexte WebGL'); + return false; + } + + // Configuration WebGL de base + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + + // Fond noir par défaut + this.gl.clearColor(0.0, 0.0, 0.0, 1.0); + + return true; + } + + /** + * Récupère le contexte WebGL + */ + get gl(): WebGLRenderingContext { + if (!this._gl) { + throw new Error('GLContext non initialisé. Appelez initialize() d\'abord.'); + } + return this._gl; + } + + /** + * Récupère le canvas + */ + get canvas(): HTMLCanvasElement { + if (!this._canvas) { + throw new Error('Canvas non initialisé.'); + } + return this._canvas; + } + + /** + * Efface le buffer avec la couleur de fond + */ + clear(): void { + this.gl.clear(this.gl.COLOR_BUFFER_BIT); + } + + /** + * Redimensionne le viewport + */ + resize(width: number, height: number): void { + this.canvas.width = width; + this.canvas.height = height; + this.gl.viewport(0, 0, width, height); + } + + /** + * Vérifie les erreurs WebGL + */ + checkError(operation: string): void { + const error = this.gl.getError(); + if (error !== this.gl.NO_ERROR) { + console.error(`WebGL Error ${error} lors de: ${operation}`); + } + } +} + diff --git a/src/game/Game.ts b/src/game/Game.ts new file mode 100644 index 0000000..26cf501 --- /dev/null +++ b/src/game/Game.ts @@ -0,0 +1,129 @@ +/** + * Game + * Point d'entrée principal du jeu CastleStorm + * Sépare la logique du jeu du moteur + */ + +import { Engine } from '../engine/core/Engine'; +import { Scene } from '../engine/core/Scene'; +import { Time } from '../engine/core/Time'; +import { GameScene } from './scenes/GameScene'; + +export class Game { + private engine: Engine; + private canvas: HTMLCanvasElement; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + + // Récupère la taille de la fenêtre + const width = window.innerWidth; + const height = window.innerHeight; + + console.log(`Initialisation du jeu : ${width}x${height}`); + + // Crée l'Engine + this.engine = new Engine(canvas, width, height); + + // Configure le gestionnaire de redimensionnement + this.setupWindowResize(); + } + + /** + * Initialise le jeu + */ + async initialize(): Promise { + console.log('=== Initialisation de CastleStorm ==='); + + // Initialise le moteur + if (!(await this.engine.initialize())) { + console.error('❌ Échec de l\'initialisation du moteur'); + return false; + } + + // Crée la scène de jeu + const gameScene = new GameScene(this.engine, window.innerWidth, window.innerHeight); + + // Charge les assets de la scène AVANT de l'enregistrer + await gameScene.onLoad(); + + // Enregistre et charge la scène + this.engine.registerScene('game', gameScene as unknown as Scene); + this.engine.loadScene('game'); + + console.log('✅ Jeu initialisé avec succès'); + return true; + } + + /** + * Démarre le jeu + */ + start(): void { + console.log('=== Démarrage du jeu ==='); + + // Configure l'affichage du FPS + this.setupFPSDisplay(); + + // Démarre le moteur + this.engine.start(); + + // Affiche les informations de contrôles + this.displayControls(); + } + + /** + * Configure le gestionnaire de redimensionnement automatique + */ + private setupWindowResize(): void { + window.addEventListener('resize', () => { + const newWidth = window.innerWidth; + const newHeight = window.innerHeight; + console.log(`Redimensionnement : ${newWidth}x${newHeight}`); + + // Redimensionne le canvas et le renderer + this.canvas.width = newWidth; + this.canvas.height = newHeight; + this.engine.renderer.resize(newWidth, newHeight); + + // Met à jour la caméra de la scène active + const activeScene = this.engine.getActiveScene(); + if (activeScene) { + activeScene.camera.resize(newWidth, newHeight); + } + }); + } + + /** + * Configure l'affichage du FPS + */ + private setupFPSDisplay(): void { + const fpsElement = document.getElementById('fps'); + if (fpsElement) { + setInterval(() => { + fpsElement.textContent = Math.round(Time.fps).toString(); + }, 200); + } + } + + /** + * Affiche les informations de contrôles dans la console + */ + private displayControls(): void { + console.log('===================================='); + console.log('🎮 CastleStorm - TopDownCharacter'); + console.log('===================================='); + console.log('✅ Sprites : 16x16 pixels, 4 frames par animation'); + console.log('✅ Animations : 8 walk, 8 roll, 4 slash (joueur + épée)'); + console.log('✅ Contrôles :'); + console.log(' 📍 ZQSD ou Flèches : Déplacement 8 directions'); + console.log(' Z+Q : Haut-Gauche | Z+D : Haut-Droite'); + console.log(' S+Q : Bas-Gauche | S+D : Bas-Droite'); + console.log(' ⚔️ Touche A : Attaque épée (slash)'); + console.log(' 🎯 Clic gauche : Tir projectile'); + console.log('✅ Physique : BoxCollider2D (16x16) + Rigidbody2D'); + console.log('✅ Épée : GameObject enfant, collider trigger (dégâts)'); + console.log('✅ Collisions : Ennemis, murs, pièces (triggers)'); + console.log('===================================='); + } +} + diff --git a/src/game/components/HealthComponent.ts b/src/game/components/HealthComponent.ts new file mode 100644 index 0000000..3d7476f --- /dev/null +++ b/src/game/components/HealthComponent.ts @@ -0,0 +1,112 @@ +/** + * HealthComponent - Gère les points de vie d'une entité + */ + +import { Component } from '../../engine/entities/Component'; + +export class HealthComponent extends Component { + private _maxHealth: number; + private _currentHealth: number; + private _onHealthChanged: ((current: number, max: number) => void) | null = null; + private _onDeath: (() => void) | null = null; + + constructor(maxHealth: number = 100) { + super(); + this._maxHealth = maxHealth; + this._currentHealth = maxHealth; + } + + /** + * Points de vie actuels + */ + get health(): number { + return this._currentHealth; + } + + set health(value: number) { + const oldHealth = this._currentHealth; + this._currentHealth = Math.max(0, Math.min(value, this._maxHealth)); + + if (oldHealth !== this._currentHealth) { + if (this._onHealthChanged) { + this._onHealthChanged(this._currentHealth, this._maxHealth); + } + + if (this._currentHealth <= 0 && this._onDeath) { + this._onDeath(); + } + } + } + + /** + * Points de vie maximum + */ + get maxHealth(): number { + return this._maxHealth; + } + + set maxHealth(value: number) { + this._maxHealth = Math.max(1, value); + // Ajuste la vie actuelle si elle dépasse le nouveau max + if (this._currentHealth > this._maxHealth) { + this.health = this._maxHealth; + } + } + + /** + * Pourcentage de vie (0-1) + */ + get healthPercent(): number { + return this._maxHealth > 0 ? this._currentHealth / this._maxHealth : 0; + } + + /** + * Est-ce que l'entité est morte ? + */ + get isDead(): boolean { + return this._currentHealth <= 0; + } + + /** + * Est-ce que l'entité est à pleine vie ? + */ + get isFullHealth(): boolean { + return this._currentHealth >= this._maxHealth; + } + + /** + * Inflige des dégâts + */ + damage(amount: number): void { + this.health -= amount; + } + + /** + * Soigne + */ + heal(amount: number): void { + this.health += amount; + } + + /** + * Remet à pleine vie + */ + fullHeal(): void { + this.health = this._maxHealth; + } + + /** + * Callback appelé quand la vie change + */ + onHealthChanged(callback: (current: number, max: number) => void): void { + this._onHealthChanged = callback; + } + + /** + * Callback appelé quand l'entité meurt + */ + onDeath(callback: () => void): void { + this._onDeath = callback; + } +} + diff --git a/src/game/components/PlayerController.ts b/src/game/components/PlayerController.ts new file mode 100644 index 0000000..489656e --- /dev/null +++ b/src/game/components/PlayerController.ts @@ -0,0 +1,491 @@ +/** + * PlayerController + * Component de contrôle du joueur - 8 DIRECTIONS + ATTAQUE + */ + +import { Component } from '../../engine/entities/Component'; +import { GameObject } from '../../engine/entities/GameObject'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Color } from '../../engine/math/Color'; +import { Rect } from '../../engine/math/Rect'; +import { InputManager } from '../../engine/input/InputManager'; +import { Scene } from '../../engine/core/Scene'; +import { Camera } from '../../engine/rendering/Camera'; +import { Texture } from '../../engine/rendering/Texture'; +import { AssetLoader } from '../../engine/assets/AssetLoader'; +import { Rigidbody2D } from '../../engine/physics/Rigidbody2D'; +import { BoxCollider2D } from '../../engine/physics/BoxCollider2D'; +import { Animator } from '../../engine/components/Animator'; +import { SpriteRenderer } from '../../engine/components/SpriteRenderer'; +import { Projectile } from './Projectile'; +import { TextureGenerator } from '../utils/TextureGenerator'; + +export class PlayerController extends Component { + private speed: number = 200; // pixels par seconde + private inputManager: InputManager; + private assetLoader: AssetLoader | null = null; + private scene: Scene | null = null; + private lastShotTime: number = 0; + private shootCooldown: number = 0.3; // 0.3 secondes entre chaque tir + private projectileTexture: Texture | null = null; + private currentDirection: string = 'down'; // Direction actuelle pour les animations + + // === SYSTÈME D'ATTAQUE === + private isAttacking: boolean = false; + private attackDuration: number = 0.32; // Durée de l'animation slash (4 frames × 0.08s) + private attackTimer: number = 0; + private swordGameObject: GameObject | null = null; + + // === SYSTÈME DE DASH/ROLL === + private isDashing: boolean = false; + private dashDuration: number = 0.32; // Durée de l'animation roll (4 frames × 0.08s) + private dashTimer: number = 0; + private dashDistance: number = 150; // Distance du dash en pixels + private dashSpeed: number = 0; // Vitesse calculée du dash + private dashDirection: Vector2 = new Vector2(0, 0); // Direction du dash + + constructor(inputManager: InputManager, gl: WebGLRenderingContext | WebGL2RenderingContext, assetLoader: AssetLoader) { + super(); + this.inputManager = inputManager; + this.assetLoader = assetLoader; + // Crée la texture du projectile une seule fois + this.projectileTexture = TextureGenerator.createSolidColorTexture(gl, 8, 8, new Color(1.0, 1.0, 0.0, 1.0)); // Jaune + } + + // Getters publics pour le debug + get isPlayerAttacking(): boolean { return this.isAttacking; } + get isPlayerDashing(): boolean { return this.isDashing; } + get isPlayerMoving(): boolean { + const rb = this.getComponent(Rigidbody2D); + return rb ? rb.velocity.lengthSquared() > 0.1 : false; + } + get direction(): string { return this.currentDirection; } + + start(): void { + this.scene = this.gameObject.scene; + console.log('[PlayerController] start() appelé, scene:', this.scene ? 'ok' : 'null'); + console.log('[PlayerController] projectileTexture:', this.projectileTexture ? 'ok' : 'null'); + + // Récupère la référence à l'épée (enfant du joueur) + this.swordGameObject = this.gameObject.findChild('Sword'); + if (this.swordGameObject) { + console.log('✅ [PlayerController] Épée trouvée et liée'); + } else { + console.warn('⚠️ [PlayerController] Épée non trouvée !'); + } + } + + /** + * Calcule la direction en 8 points cardinaux selon l'angle vers le curseur + */ + private getDirectionFromMouse(camera: Camera): string { + const mouseWorldPos = this.inputManager.getMouseWorldPosition(camera); + const playerPos = this.transform.position; + + // Vecteur du joueur vers le curseur + const direction = mouseWorldPos.subtract(playerPos); + + // Si le curseur est trop proche, garde la direction actuelle + if (direction.lengthSquared() < 1) { + return this.currentDirection; + } + + // Calcule l'angle en radians (atan2 retourne [-π, π]) + const angle = Math.atan2(direction.y, direction.x); + + // Convertit l'angle en direction 8-way + return this.angleToDirection8Way(angle); + } + + /** + * Convertit un angle (radians) en direction 8-way + * Sans rotation du GameObject, le mapping est direct + */ + private angleToDirection8Way(angle: number): string { + // Normalise l'angle entre 0 et 2π + let normalizedAngle = angle; + if (normalizedAngle < 0) { + normalizedAngle += Math.PI * 2; + } + + // Convertit en degrés + const degrees = normalizedAngle * (180 / Math.PI); + + // En WebGL : Y+ = bas, Y- = haut, X+ = droite, X- = gauche + // atan2(y, x) retourne : 0° = droite, 90° = bas, 180° = gauche, 270° = haut + + // Divise le cercle en 8 secteurs de 45° chacun + // Inversion left/right pour correspondre aux sprites + if (degrees >= 337.5 || degrees < 22.5) { + return 'left'; // 0° = curseur à droite → sprite left + } else if (degrees >= 22.5 && degrees < 67.5) { + return 'downleft'; // 45° = curseur en bas-droite → sprite downleft + } else if (degrees >= 67.5 && degrees < 112.5) { + return 'down'; // 90° = curseur en bas + } else if (degrees >= 112.5 && degrees < 157.5) { + return 'downright'; // 135° = curseur en bas-gauche → sprite downright + } else if (degrees >= 157.5 && degrees < 202.5) { + return 'right'; // 180° = curseur à gauche → sprite right + } else if (degrees >= 202.5 && degrees < 247.5) { + return 'upright'; // 225° = curseur en haut-gauche → sprite upright + } else if (degrees >= 247.5 && degrees < 292.5) { + return 'up'; // 270° = curseur en haut + } else { + return 'upleft'; // 315° = curseur en haut-droite → sprite upleft + } + } + + /** + * Obtient la direction diagonale la plus proche pour l'attaque + * (Les attaques slash ne sont disponibles qu'en diagonale) + */ + private getAttackDirection(): string { + const dir = this.currentDirection; + + // Si déjà en diagonale, utilise directement + if (dir.includes('up') || dir.includes('down')) { + if (dir.includes('left') || dir.includes('right')) { + return dir; // Déjà diagonale (upleft, upright, downleft, downright) + } + } + + // Sinon, convertit les directions cardinales en diagonales par défaut + // (On choisit la diagonale droite par défaut, ajuste selon tes préférences) + if (dir === 'up') return 'upright'; + if (dir === 'down') return 'downright'; + if (dir === 'left') return 'downleft'; + if (dir === 'right') return 'downright'; + + return 'downright'; // Fallback + } + + update(deltaTime: number): void { + const rigidbody = this.getComponent(Rigidbody2D); + if (!rigidbody) { + return; // Pas de rigidbody, on ne peut pas bouger avec la physique + } + + // === GESTION DU TIMER D'ATTAQUE === + if (this.isAttacking) { + this.attackTimer -= deltaTime; + + // Fin de l'attaque + if (this.attackTimer <= 0) { + this.isAttacking = false; + // Désactive l'épée + if (this.swordGameObject) { + this.swordGameObject.active = false; + } + console.log('⚔️ Fin attaque'); + } + } + + // === GESTION DU TIMER DE DASH === + if (this.isDashing) { + this.dashTimer -= deltaTime; + + // Fin du dash + if (this.dashTimer <= 0) { + this.isDashing = false; + console.log('🏃 Fin dash'); + } + } + + // === DÉTECTION INPUT ATTAQUE (touche A uniquement) === + const attackInput = this.inputManager.isKeyDown('a') || this.inputManager.isKeyDown('A'); + + if (attackInput && !this.isAttacking && !this.isDashing) { + // Démarre l'attaque + this.startAttack(); + } + + // === DÉTECTION INPUT DASH (clic droit) === + const dashInput = this.inputManager.isMouseButtonDown(2); + + if (dashInput && !this.isDashing && !this.isAttacking) { + // Démarre le dash vers la direction du curseur + this.startDash(); + } + + // Calcule la direction du mouvement (BRUT, non normalisé pour détecter diagonales) + const movement = new Vector2(0, 0); + + // === BLOQUE LE MOUVEMENT PENDANT L'ATTAQUE OU LE DASH === + if (!this.isAttacking && !this.isDashing) { + // Déplacement horizontal + if (this.inputManager.isKeyHeld('ArrowLeft') || this.inputManager.isKeyHeld('q') || this.inputManager.isKeyHeld('Q')) { + movement.x -= 1; + } + if (this.inputManager.isKeyHeld('ArrowRight') || this.inputManager.isKeyHeld('d') || this.inputManager.isKeyHeld('D')) { + movement.x += 1; + } + + // Déplacement vertical (WebGL: Y positif = bas, Y négatif = haut) + if (this.inputManager.isKeyHeld('ArrowUp') || this.inputManager.isKeyHeld('z') || this.inputManager.isKeyHeld('Z')) { + movement.y -= 1; // Y- vers le haut (WebGL) + } + if (this.inputManager.isKeyHeld('ArrowDown') || this.inputManager.isKeyHeld('s') || this.inputManager.isKeyHeld('S')) { + movement.y += 1; // Y+ vers le bas (WebGL) + } + } + + const moved = movement.x !== 0 || movement.y !== 0; + + // Fait suivre la caméra au joueur (centrée) - on en a besoin plus tôt maintenant + const scene = this.gameObject.scene; + if (!scene) return; + + const camera = scene.camera; + camera.position = this.transform.position.clone(); + + // Détermine la direction selon la position du curseur (système twin-stick) + this.currentDirection = this.getDirectionFromMouse(camera); + + // === GESTION DE LA VÉLOCITÉ === + if (this.isDashing) { + // En dash : applique la vitesse de dash dans la direction stockée + rigidbody.velocity = this.dashDirection.multiply(this.dashSpeed); + } else if (moved && !this.isAttacking) { + // Mouvement normal + movement.normalizeMut(); + rigidbody.velocity = movement.multiply(this.speed); + } else { + // Immobile + rigidbody.velocity = new Vector2(0, 0); + } + + // Change l'animation selon le mouvement et la direction + const animator = this.getComponent(Animator); + const spriteRenderer = this.getComponent(SpriteRenderer); + + if (animator && spriteRenderer) { + if (this.isDashing) { + // === EN DASH : Animation roll (déjà jouée dans startDash) === + // Ne fait rien, laisse l'animation roll se terminer + } else if (this.isAttacking) { + // === EN ATTAQUE : Animation slash (déjà jouée dans startAttack) === + // Ne fait rien, laisse l'animation slash se terminer + } else { + // === PAS EN ATTAQUE NI DASH : Animation walk normale === + const animationName = `walk_${this.currentDirection}`; + + // Change la texture si nécessaire + if (this.assetLoader) { + const textureName = `walk_${this.currentDirection}`; + const newTexture = this.assetLoader.getTexture(textureName); + if (newTexture && spriteRenderer.texture !== newTexture) { + spriteRenderer.texture = newTexture; + } + } + + if (moved) { + // Si en mouvement, joue l'animation de marche + if (animator.currentAnimation?.name !== animationName || !animator.isPlaying) { + animator.play(animationName); + } + } else { + // Si immobile, arrête l'animation (reste sur première frame = idle) + if (animator.isPlaying) { + animator.stop(); + // Remet à la première frame de l'animation actuelle (pose idle) + spriteRenderer.sourceRect = animator.currentAnimation?.frames[0] || spriteRenderer.sourceRect; + } + // S'assure que l'animation courante est celle de la dernière direction + if (animator.currentAnimation?.name !== animationName) { + animator.play(animationName); + animator.stop(); // Joue puis arrête immédiatement pour avoir la première frame + spriteRenderer.sourceRect = animator.currentAnimation?.frames[0] || spriteRenderer.sourceRect; + } + } + } + } + + // Gestion du tir de projectile + this.lastShotTime += deltaTime; + + // Utilise Held pour détecter le clic (car Down est réinitialisé trop tôt) + const mouseHeld = this.inputManager.isMouseButtonHeld(0); + const canShoot = this.lastShotTime >= this.shootCooldown; + + if (mouseHeld && canShoot) { + console.log('[PlayerController] Tir de projectile déclenché !'); + this.lastShotTime = 0; + this.shootProjectile(scene, camera); + } + } + + private shootProjectile(scene: Scene, camera: Camera): void { + console.log('[PlayerController] shootProjectile() appelé'); + + if (!this.scene || !this.projectileTexture) { + console.error('[PlayerController] Impossible de tirer: scene ou texture manquante'); + return; + } + + // Récupère la position du curseur dans le monde + const mouseWorldPos = this.inputManager.getMouseWorldPosition(camera); + const playerPos = this.transform.position; + + console.log(`[PlayerController] Position joueur: (${playerPos.x.toFixed(1)}, ${playerPos.y.toFixed(1)})`); + console.log(`[PlayerController] Position curseur monde: (${mouseWorldPos.x.toFixed(1)}, ${mouseWorldPos.y.toFixed(1)})`); + + // Calcule la direction vers le curseur + const direction = mouseWorldPos.subtract(playerPos); + + // Si la direction est trop petite, ne tire pas + if (direction.lengthSquared() < 1) { + console.warn('[PlayerController] Direction trop petite, pas de tir'); + return; + } + + // Crée le projectile + const projectile = new GameObject('Projectile'); + projectile.transform.position = playerPos.clone(); + + // Sprite + const projectileSprite = projectile.addComponent(new SpriteRenderer()); + projectileSprite.texture = this.projectileTexture; + projectileSprite.sourceRect = new Rect(0, 0, 8, 8); + projectileSprite.tint = Color.white; + projectileSprite.layer = 1; + projectileSprite.pivot = new Vector2(0.5, 0.5); + + // Rigidbody pour le mouvement + const projectileRigidbody = projectile.addComponent(new Rigidbody2D()); + projectileRigidbody.mass = 0.1; // Masse très faible + projectileRigidbody.drag = 0; // Pas de friction + + // Collider + const projectileCollider = projectile.addComponent(new BoxCollider2D()); + projectileCollider.size = new Vector2(8, 8); + projectileCollider.isTrigger = true; // Trigger pour ne pas résoudre physiquement + + // Ajoute le composant Projectile + projectile.addComponent(new Projectile(direction)); + + scene.addGameObject(projectile); + console.log('[PlayerController] Projectile créé et ajouté à la scène !'); + } + + /** + * Démarre une attaque au corps-à-corps avec l'épée + */ + private startAttack(): void { + if (this.isAttacking) return; // Déjà en attaque + + console.log('⚔️ Début attaque !'); + + // Active l'état d'attaque + this.isAttacking = true; + this.attackTimer = this.attackDuration; + + // Obtient la direction d'attaque (diagonale) + const attackDir = this.getAttackDirection(); + console.log(`⚔️ Direction attaque: ${attackDir}`); + + // Joue l'animation slash du joueur + const animator = this.getComponent(Animator); + const spriteRenderer = this.getComponent(SpriteRenderer); + + if (animator && spriteRenderer && this.assetLoader) { + const slashAnimName = `slash_${attackDir}`; + const slashTextureName = `slash_${attackDir}`; + + // Change la texture du joueur + const slashTexture = this.assetLoader.getTexture(slashTextureName); + if (slashTexture) { + spriteRenderer.texture = slashTexture; + } + + // Joue l'animation slash + animator.play(slashAnimName); + console.log(`⚔️ Animation joueur: ${slashAnimName}`); + } + + // Active l'épée et joue son animation + if (this.swordGameObject) { + this.swordGameObject.active = true; + + const swordAnimator = this.swordGameObject.getComponent(Animator); + const swordSprite = this.swordGameObject.getComponent(SpriteRenderer); + + if (swordAnimator && swordSprite && this.assetLoader) { + const swordAnimName = `sword_${attackDir}`; + const swordTextureName = `sword_${attackDir}`; + + // Change la texture de l'épée + const swordTexture = this.assetLoader.getTexture(swordTextureName); + if (swordTexture) { + swordSprite.texture = swordTexture; + } + + // Joue l'animation sword + swordAnimator.play(swordAnimName); + console.log(`⚔️ Animation épée: ${swordAnimName}`); + } + } + } + + /** + * Démarre un dash/roll rapide pour esquiver + * Le dash se fait vers la direction du curseur + */ + private startDash(): void { + if (this.isDashing) return; // Déjà en dash + + console.log('🏃 Début dash !'); + + // Active l'état de dash + this.isDashing = true; + this.dashTimer = this.dashDuration; + + // Calcule la vitesse du dash (distance / durée) + this.dashSpeed = this.dashDistance / this.dashDuration; + + // Détermine la direction du dash vers le curseur (currentDirection est déjà mis à jour) + // Convertit la direction (string) en vecteur normalisé + this.dashDirection = this.getDirectionVector(this.currentDirection); + + console.log(`🏃 Direction dash: ${this.currentDirection} -> (${this.dashDirection.x.toFixed(2)}, ${this.dashDirection.y.toFixed(2)})`); + + // Joue l'animation roll du joueur + const animator = this.getComponent(Animator); + const spriteRenderer = this.getComponent(SpriteRenderer); + + if (animator && spriteRenderer && this.assetLoader) { + const rollAnimName = `roll_${this.currentDirection}`; + const rollTextureName = `roll_${this.currentDirection}`; + + // Change la texture du joueur + const rollTexture = this.assetLoader.getTexture(rollTextureName); + if (rollTexture) { + spriteRenderer.texture = rollTexture; + } + + // Joue l'animation roll + animator.play(rollAnimName); + console.log(`🏃 Animation: ${rollAnimName}`); + } + } + + /** + * Convertit une direction (string) en Vector2 normalisé + * Les sprites left/right sont inversés visuellement + */ + private getDirectionVector(direction: string): Vector2 { + // En WebGL : X+ = droite, X- = gauche, Y+ = bas, Y- = haut + // Avec inversion left/right pour correspondre aux sprites + switch (direction) { + case 'up': return new Vector2(0, -1); + case 'down': return new Vector2(0, 1); + case 'left': return new Vector2(1, 0); // sprite left va à droite visuellement + case 'right': return new Vector2(-1, 0); // sprite right va à gauche visuellement + case 'upleft': return new Vector2(1, -1).normalize(); // va haut-droite visuellement + case 'upright': return new Vector2(-1, -1).normalize(); // va haut-gauche visuellement + case 'downleft': return new Vector2(1, 1).normalize(); // va bas-droite visuellement + case 'downright': return new Vector2(-1, 1).normalize(); // va bas-gauche visuellement + default: return new Vector2(0, 1); // Par défaut: bas + } + } +} + diff --git a/src/game/components/Projectile.ts b/src/game/components/Projectile.ts new file mode 100644 index 0000000..be96ef0 --- /dev/null +++ b/src/game/components/Projectile.ts @@ -0,0 +1,73 @@ +/** + * Projectile + * Component pour gérer le comportement d'un projectile + */ + +import { Component } from '../../engine/entities/Component'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Rigidbody2D } from '../../engine/physics/Rigidbody2D'; +import { BoxCollider2D } from '../../engine/physics/BoxCollider2D'; + +export class Projectile extends Component { + private speed: number = 400; // pixels par seconde + private direction: Vector2; + private lifetime: number = 5; // Se détruit après 5 secondes + + constructor(direction: Vector2) { + super(); + this.direction = direction.normalize(); + } + + update(deltaTime: number): void { + // Déplace le projectile + const rigidbody = this.getComponent(Rigidbody2D); + if (rigidbody) { + rigidbody.velocity = this.direction.multiply(this.speed); + } else { + // Si pas de rigidbody, utilise transform directement + this.transform.translate(this.direction.multiply(this.speed * deltaTime)); + } + + // Log position chaque seconde (pour debug) + if (Math.floor(this.lifetime) !== Math.floor(this.lifetime + deltaTime)) { + console.log(`[Projectile] Position: (${this.transform.position.x.toFixed(1)}, ${this.transform.position.y.toFixed(1)}), Lifetime: ${this.lifetime.toFixed(2)}s`); + } + + // Réduit la durée de vie + this.lifetime -= deltaTime; + if (this.lifetime <= 0) { + console.log('[Projectile] Durée de vie expirée, destruction'); + this.gameObject.destroy(); + } + } + + start(): void { + console.log('[Projectile] start() appelé'); + // Configure le collider pour détecter les collisions + const collider = this.getComponent(BoxCollider2D); + if (collider) { + console.log('[Projectile] Collider trouvé, configuration trigger'); + collider.isTrigger = true; // Trigger pour ne pas résoudre physiquement + collider.onTriggerEnter((other) => { + console.log(`[Projectile] Trigger avec ${other.gameObject.name} (tag: ${other.gameObject.tag})`); + // Si collision avec un mur ou un ennemi, détruit le projectile + if (other.gameObject.tag === 'Wall' || other.gameObject.tag === 'Enemy') { + console.log(`[Projectile] Collision détectée avec ${other.gameObject.tag}, destruction`); + if (other.gameObject.tag === 'Enemy') { + // Détruit l'ennemi aussi + console.log(`[Projectile] Destruction de l'ennemi ${other.gameObject.name}`); + other.gameObject.destroy(); + } + this.gameObject.destroy(); + } + }); + } else { + console.warn('[Projectile] Aucun collider trouvé !'); + } + } + + awake(): void { + console.log('[Projectile] awake() appelé'); + } +} + diff --git a/src/game/scenes/GameScene.ts b/src/game/scenes/GameScene.ts new file mode 100644 index 0000000..ddca30d --- /dev/null +++ b/src/game/scenes/GameScene.ts @@ -0,0 +1,515 @@ +/** + * GameScene + * Scène principale du jeu avec joueur, ennemis, pièces, et environnement + */ + +import { Scene } from '../../engine/core/Scene'; +import { GameObject } from '../../engine/entities/GameObject'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Color } from '../../engine/math/Color'; +import { Rect } from '../../engine/math/Rect'; +import { Engine } from '../../engine/core/Engine'; +import { AssetLoader } from '../../engine/assets/AssetLoader'; +import { SpriteSheet } from '../../engine/assets/SpriteSheet'; +import { Animation } from '../../engine/animation/Animation'; +import { SpriteRenderer } from '../../engine/components/SpriteRenderer'; +import { Animator } from '../../engine/components/Animator'; +import { BoxCollider2D, CircleCollider2D } from '../../engine/physics'; +import { Rigidbody2D } from '../../engine/physics/Rigidbody2D'; +import { PlayerController } from '../components/PlayerController'; +import { HealthComponent } from '../components/HealthComponent'; +import { TextureGenerator } from '../utils/TextureGenerator'; +import { UIHealthBar } from '../ui/UIHealthBar'; + +export class GameScene extends Scene { + private engine: Engine; + private assetLoader: AssetLoader; + private gl: WebGLRenderingContext | WebGL2RenderingContext; + + // Configuration du monde de jeu + private readonly TILE_SIZE = 80; + private readonly GRID_WIDTH = 10 * this.TILE_SIZE; // 800px + private readonly GRID_HEIGHT = 10 * this.TILE_SIZE; // 800px + + constructor(engine: Engine, width: number, height: number) { + super('GameScene', width, height); + this.engine = engine; + this.assetLoader = engine.assets; + this.gl = engine.renderer.gl; + } + + /** + * Charge les assets nécessaires à la scène + */ + async loadAssets(): Promise { + console.log('=== Chargement des assets du jeu ==='); + + try { + // === WALK (8 directions) === + await this.assetLoader.loadTexture('walk_down', '/TopDownCharacter/Character/Character_Down.png'); + await this.assetLoader.loadTexture('walk_downleft', '/TopDownCharacter/Character/Character_DownLeft.png'); + await this.assetLoader.loadTexture('walk_downright', '/TopDownCharacter/Character/Character_DownRight.png'); + await this.assetLoader.loadTexture('walk_left', '/TopDownCharacter/Character/Character_Left.png'); + await this.assetLoader.loadTexture('walk_right', '/TopDownCharacter/Character/Character_Right.png'); + await this.assetLoader.loadTexture('walk_up', '/TopDownCharacter/Character/Character_Up.png'); + await this.assetLoader.loadTexture('walk_upleft', '/TopDownCharacter/Character/Character_UpLeft.png'); + await this.assetLoader.loadTexture('walk_upright', '/TopDownCharacter/Character/Character_UpRight.png'); + + // === ROLL (8 directions) === + await this.assetLoader.loadTexture('roll_down', '/TopDownCharacter/Character/Character_RollDown.png'); + await this.assetLoader.loadTexture('roll_downleft', '/TopDownCharacter/Character/Character_RollDownLeft.png'); + await this.assetLoader.loadTexture('roll_downright', '/TopDownCharacter/Character/Character_RollDownRight.png'); + await this.assetLoader.loadTexture('roll_left', '/TopDownCharacter/Character/Character_RollLeft.png'); + await this.assetLoader.loadTexture('roll_right', '/TopDownCharacter/Character/Character_RollRight.png'); + await this.assetLoader.loadTexture('roll_up', '/TopDownCharacter/Character/Character_RollUp.png'); + await this.assetLoader.loadTexture('roll_upleft', '/TopDownCharacter/Character/Character_RollUpLeft.png'); + await this.assetLoader.loadTexture('roll_upright', '/TopDownCharacter/Character/Character_RollUpRight.png'); + + // === SLASH (4 directions diagonales) === + await this.assetLoader.loadTexture('slash_downleft', '/TopDownCharacter/Character/Character_SlashDownLeft.png'); + await this.assetLoader.loadTexture('slash_downright', '/TopDownCharacter/Character/Character_SlashDownRight.png'); + await this.assetLoader.loadTexture('slash_upleft', '/TopDownCharacter/Character/Character_SlashUpLeft.png'); + await this.assetLoader.loadTexture('slash_upright', '/TopDownCharacter/Character/Character_SlashUpRight.png'); + + // === SWORD/WEAPON (4 directions diagonales) === + await this.assetLoader.loadTexture('sword_downleft', '/TopDownCharacter/Weapon/Sword_DownLeft.png'); + await this.assetLoader.loadTexture('sword_downright', '/TopDownCharacter/Weapon/Sword_DownRight.png'); + await this.assetLoader.loadTexture('sword_upleft', '/TopDownCharacter/Weapon/Sword_UpLeft.png'); + await this.assetLoader.loadTexture('sword_upright', '/TopDownCharacter/Weapon/Sword_UpRight.png'); + + // === HEARTS (UI de vie) === + // ATTENTION : Les fichiers sont dans l'ordre INVERSE ! + // Hearts_Red_1.png = PLEIN, Hearts_Red_5.png = VIDE + await this.assetLoader.loadTexture('heart_full', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_1.png'); + await this.assetLoader.loadTexture('heart_three_quarters', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_2.png'); + await this.assetLoader.loadTexture('heart_half', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_3.png'); + await this.assetLoader.loadTexture('heart_quarter', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_4.png'); + await this.assetLoader.loadTexture('heart_empty', '/Retro Inventory/Retro Inventory/Original/Hearts_Red_5.png'); + + console.log('✅ Tous les assets chargés'); + } catch (error) { + console.error('❌ Erreur lors du chargement des assets:', error); + } + } + + /** + * Initialise la scène (appelé par Engine après loadScene) + */ + async onLoad(): Promise { + // Évite de charger deux fois + if (this.isLoaded) { + console.log('⚠️ GameScene déjà chargée, skip'); + return; + } + + console.log('=== Initialisation de GameScene ==='); + + // Crée le UICanvas + this.createUICanvas(); + console.log('✅ UICanvas créé'); + + // Charge les assets + await this.loadAssets(); + + // Détermine la taille du sprite + const testTexture = this.assetLoader.getTexture('walk_down'); + let spriteWidth = 16; + let spriteHeight = 16; + + if (testTexture) { + spriteWidth = testTexture.width / 4; // 4 frames par animation + spriteHeight = testTexture.height; + console.log(`📐 Taille sprite détectée: ${spriteWidth}x${spriteHeight}`); + } + + // Crée les éléments de la scène + this.createGround(); + this.createWalls(); + const player = this.createPlayer(spriteWidth, spriteHeight); + this.createEnemies(); + this.createCoins(); + + // Crée l'UI de vie + this.createHealthUI(player); + + // Centre la caméra + this.camera.position = new Vector2(this.GRID_WIDTH / 2, this.GRID_HEIGHT / 2); + + // Appelle super.onLoad() pour marquer comme chargée + super.onLoad(); + + console.log('✅ GameScene initialisée'); + } + + /** + * Crée le sol avec tiles + */ + private createGround(): void { + const tileTextureSize = 80; + const groundFillColor = new Color(0.4, 0.3, 0.2, 1.0); // Marron + const groundBorderColor = new Color(0.2, 0.15, 0.1, 1.0); // Marron foncé + const groundTexture = TextureGenerator.createTileTextureWithBorder( + this.gl, + tileTextureSize, + groundFillColor, + groundBorderColor, + 2 + ); + + for (let x = 0; x < 10; x++) { + for (let y = 0; y < 10; y++) { + const ground = new GameObject(`Ground_${x}_${y}`); + ground.transform.position = new Vector2(x * this.TILE_SIZE, y * this.TILE_SIZE); + + const groundSprite = ground.addComponent(new SpriteRenderer()); + groundSprite.texture = groundTexture; + groundSprite.sourceRect = new Rect(0, 0, tileTextureSize, tileTextureSize); + groundSprite.tint = Color.white; + groundSprite.layer = -1; // Arrière-plan + groundSprite.pivot = new Vector2(0, 0); // Pivot en haut-gauche + + this.addGameObject(ground); + } + } + } + + /** + * Crée les murs autour de la grille + */ + private createWalls(): void { + const wallThickness = 60; + const wallColor = new Color(0.3, 0.3, 0.3, 1.0); // Gris foncé + + // Mur du haut + const wallTop = this.createWall( + 'Wall_Top', + new Vector2(this.GRID_WIDTH / 2, -wallThickness / 2), + this.GRID_WIDTH, + wallThickness, + wallColor + ); + this.addGameObject(wallTop); + + // Mur du bas + const wallBottom = this.createWall( + 'Wall_Bottom', + new Vector2(this.GRID_WIDTH / 2, this.GRID_HEIGHT + wallThickness / 2), + this.GRID_WIDTH, + wallThickness, + wallColor + ); + this.addGameObject(wallBottom); + + // Mur de gauche + const wallLeft = this.createWall( + 'Wall_Left', + new Vector2(-wallThickness / 2, this.GRID_HEIGHT / 2), + wallThickness, + this.GRID_HEIGHT, + wallColor + ); + this.addGameObject(wallLeft); + + // Mur de droite + const wallRight = this.createWall( + 'Wall_Right', + new Vector2(this.GRID_WIDTH + wallThickness / 2, this.GRID_HEIGHT / 2), + wallThickness, + this.GRID_HEIGHT, + wallColor + ); + this.addGameObject(wallRight); + } + + /** + * Helper pour créer un mur + */ + private createWall(name: string, position: Vector2, width: number, height: number, color: Color): GameObject { + const wall = new GameObject(name); + wall.transform.position = position; + wall.tag = 'Wall'; + + const wallTexture = TextureGenerator.createSolidColorTexture(this.gl, width, height, color); + const wallSprite = wall.addComponent(new SpriteRenderer()); + wallSprite.texture = wallTexture; + wallSprite.sourceRect = new Rect(0, 0, width, height); + wallSprite.tint = Color.white; + wallSprite.layer = 0; + wallSprite.pivot = new Vector2(0.5, 0.5); + + const wallCollider = wall.addComponent(new BoxCollider2D()); + wallCollider.size = new Vector2(width, height); + + return wall; + } + + /** + * Crée le joueur avec animations + */ + private createPlayer(spriteWidth: number, spriteHeight: number): GameObject { + const player = new GameObject('Player'); + player.transform.position = new Vector2(this.GRID_WIDTH / 2, this.GRID_HEIGHT / 2); + player.transform.scale = new Vector2(3, 3); // 3x plus grand + player.transform.rotation = 90; // Rotation 90° horaire pour orienter les sprites correctement + + // Sprite + const playerSprite = player.addComponent(new SpriteRenderer()); + const walkTexture = this.assetLoader.getTexture('walk_down'); + if (walkTexture) { + playerSprite.texture = walkTexture; + } + playerSprite.sourceRect = new Rect(0, 0, spriteWidth, spriteHeight); + playerSprite.tint = Color.white; + playerSprite.layer = 1; + + // Animator avec toutes les animations + const playerAnimator = player.addComponent(new Animator()); + this.createPlayerAnimations(playerAnimator, spriteWidth, spriteHeight); + + // Joue l'animation walk_down par défaut + playerAnimator.play('walk_down'); + + // Controller + const playerController = new PlayerController(this.engine.input, this.gl, this.assetLoader); + player.addComponent(playerController); + + // Physique + const playerCollider = player.addComponent(new BoxCollider2D()); + playerCollider.size = new Vector2(16, 16); + playerCollider.onCollisionEnter((other) => { + console.log(`Player collision avec ${other.gameObject.name}`); + }); + + const playerRigidbody = player.addComponent(new Rigidbody2D()); + playerRigidbody.drag = 15; + + // Système de vie + const playerHealth = player.addComponent(new HealthComponent(500)); // 500 points de vie = 5 cœurs + playerHealth.onDeath(() => { + console.log('💀 Le joueur est mort !'); + // TODO: Game Over + }); + + // Épée (enfant du joueur) + this.createSword(player, spriteWidth, spriteHeight); + + this.addGameObject(player); + return player; + } + + /** + * Crée toutes les animations du joueur + */ + private createPlayerAnimations(animator: Animator, spriteWidth: number, spriteHeight: number): void { + const directions = ['down', 'downleft', 'downright', 'left', 'right', 'up', 'upleft', 'upright']; + + // Walk animations + for (const dir of directions) { + const anim = this.createAnimationFromTexture(`walk_${dir}`, `walk_${dir}`, spriteWidth, spriteHeight, 0.12, true); + if (anim) animator.addAnimation(`walk_${dir}`, anim); + } + + // Roll animations + for (const dir of directions) { + const anim = this.createAnimationFromTexture(`roll_${dir}`, `roll_${dir}`, spriteWidth, spriteHeight, 0.08, false); + if (anim) animator.addAnimation(`roll_${dir}`, anim); + } + + // Slash animations (diagonales seulement) + const slashDirs = ['downleft', 'downright', 'upleft', 'upright']; + for (const dir of slashDirs) { + const anim = this.createAnimationFromTexture(`slash_${dir}`, `slash_${dir}`, spriteWidth, spriteHeight, 0.08, false); + if (anim) animator.addAnimation(`slash_${dir}`, anim); + } + + console.log('✅ Animations joueur créées: 8 walk + 8 roll + 4 slash'); + } + + /** + * Crée l'épée en tant qu'enfant du joueur + */ + private createSword(player: GameObject, spriteWidth: number, spriteHeight: number): void { + const sword = new GameObject('Sword'); + sword.transform.position = new Vector2(0, 0); + sword.active = false; // Désactivée par défaut + + // Sprite + const swordSprite = sword.addComponent(new SpriteRenderer()); + const swordTexture = this.assetLoader.getTexture('sword_downright'); + if (swordTexture) { + swordSprite.texture = swordTexture; + } + swordSprite.sourceRect = new Rect(0, 0, spriteWidth, spriteHeight); + swordSprite.tint = Color.white; + swordSprite.layer = 2; + + // Animator + const swordAnimator = sword.addComponent(new Animator()); + const swordDirs = ['downleft', 'downright', 'upleft', 'upright']; + for (const dir of swordDirs) { + const anim = this.createAnimationFromTexture(`sword_${dir}`, `sword_${dir}`, spriteWidth, spriteHeight, 0.08, false); + if (anim) swordAnimator.addAnimation(`sword_${dir}`, anim); + } + + // Collider + const swordCollider = sword.addComponent(new BoxCollider2D()); + swordCollider.size = new Vector2(24, 24); + swordCollider.isTrigger = true; + swordCollider.onTriggerEnter((other) => { + if (other.gameObject.tag === 'Enemy') { + console.log(`⚔️ Épée touche ${other.gameObject.name} !`); + other.gameObject.destroy(); + } + }); + + player.addChild(sword); + console.log('✅ Épée créée'); + } + + /** + * Helper pour créer une animation depuis une texture + */ + private createAnimationFromTexture( + textureName: string, + animationName: string, + spriteWidth: number, + spriteHeight: number, + frameDuration: number, + loop: boolean + ): Animation | null { + const texture = this.assetLoader.getTexture(textureName); + if (!texture) { + console.warn(`Texture "${textureName}" non trouvée`); + return null; + } + + const spriteSheet = new SpriteSheet(texture, spriteWidth, spriteHeight); + const numFrames = Math.min(spriteSheet.columns, 4); // Maximum 4 frames + + const frames: Rect[] = []; + for (let i = 0; i < numFrames; i++) { + const spriteRect = spriteSheet.getSpriteByIndex(i); + if (spriteRect) { + frames.push(spriteRect); + } + } + + if (frames.length === 0) { + console.warn(`Aucune frame trouvée pour "${textureName}"`); + return null; + } + + return new Animation(animationName, frames, frameDuration, loop); + } + + /** + * Crée les ennemis + */ + private createEnemies(): void { + const enemyTexture = TextureGenerator.createSolidColorTexture(this.gl, 48, 48, new Color(1.0, 0.2, 0.2, 1.0)); + + for (let i = 0; i < 5; i++) { + const enemy = new GameObject(`Enemy_${i}`); + enemy.transform.position = new Vector2(100 + i * 150, 100 + (i % 2) * 200); + enemy.tag = 'Enemy'; + + const enemySprite = enemy.addComponent(new SpriteRenderer()); + enemySprite.texture = enemyTexture; + enemySprite.sourceRect = new Rect(0, 0, 48, 48); + enemySprite.tint = Color.white; + enemySprite.layer = 0; + + const enemyCollider = enemy.addComponent(new BoxCollider2D()); + enemyCollider.size = new Vector2(48, 48); + enemyCollider.onCollisionEnter((other) => { + if (other.gameObject.name === 'Player') { + console.log(`Enemy ${i} touché par le joueur !`); + } + }); + + this.addGameObject(enemy); + } + } + + /** + * Crée l'UI de vie du joueur + */ + private createHealthUI(player: GameObject): void { + // Récupère le HealthComponent du joueur + const healthComponent = player.getComponents().find(c => c.constructor.name === 'HealthComponent') as HealthComponent; + + if (!healthComponent) { + console.error('❌ HealthComponent introuvable sur le joueur !'); + return; + } + + if (!this.uiCanvas) { + console.error('❌ UICanvas introuvable dans la scène !'); + return; + } + + // Crée un GameObject UI pour la barre de vie + const healthBarObject = new GameObject('HealthBar'); + const healthBar = healthBarObject.addComponent(new UIHealthBar(this.assetLoader, this.uiCanvas)); + + // Lie la barre de vie au composant de vie du joueur + healthBar.bindToHealth(healthComponent); + + // Ajoute au canvas UI (le HealthBar n'a pas besoin d'être ajouté, les cœurs seront ajoutés directement) + // this.uiCanvas.addGameObject(healthBarObject); + + console.log('✅ UI de vie créée'); + } + + /** + * Crée les pièces collectables + */ + private createCoins(): void { + const coinTexture = TextureGenerator.createSolidColorTexture(this.gl, 32, 32, new Color(1.0, 0.8, 0.0, 1.0)); + + for (let i = 0; i < 3; i++) { + const coin = new GameObject(`Coin_${i}`); + coin.transform.position = new Vector2(200 + i * 200, 400); + coin.tag = 'Coin'; + + const coinSprite = coin.addComponent(new SpriteRenderer()); + coinSprite.texture = coinTexture; + coinSprite.sourceRect = new Rect(0, 0, 32, 32); + coinSprite.tint = Color.white; + coinSprite.layer = 0; + + // Animation de rotation + const coinAnimator = coin.addComponent(new Animator()); + const coinAnim = new Animation('spin', [ + new Rect(0, 0, 32, 32), + new Rect(0, 0, 32, 64), + new Rect(0, 0, 32, 32), + ], 0.1, true); + coinAnimator.addAnimation('spin', coinAnim); + coinAnimator.play('spin'); + coinAnimator.speed = 2.0; + + // Collider + const coinCollider = coin.addComponent(new CircleCollider2D()); + coinCollider.radius = 16; + coinCollider.isTrigger = true; + coinCollider.onTriggerEnter((other) => { + if (other.gameObject.name === 'Player') { + console.log(`Pièce ${i} collectée !`); + + // Inflige 25 dégâts au joueur + const healthComponent = other.gameObject.getComponents().find(c => c.constructor.name === 'HealthComponent') as HealthComponent; + if (healthComponent) { + healthComponent.damage(25); + console.log(`💔 Joueur blessé ! Vie restante: ${healthComponent.health}/${healthComponent.maxHealth}`); + } + + coin.active = false; + } + }); + + this.addGameObject(coin); + } + } +} + diff --git a/src/game/ui/UIHealthBar.ts b/src/game/ui/UIHealthBar.ts new file mode 100644 index 0000000..ca6a087 --- /dev/null +++ b/src/game/ui/UIHealthBar.ts @@ -0,0 +1,165 @@ +/** + * UIHealthBar - Affiche la barre de vie avec des cœurs + */ + +import { UIComponent } from '../../engine/ui/UIComponent'; +import { UIImage } from '../../engine/ui/UIImage'; +import { UICanvas } from '../../engine/ui/UICanvas'; +import { GameObject } from '../../engine/entities/GameObject'; +import { Vector2 } from '../../engine/math/Vector2'; +import { Color } from '../../engine/math/Color'; +import { SpriteBatch } from '../../engine/rendering/SpriteBatch'; +import { AssetLoader } from '../../engine/assets/AssetLoader'; +import { HealthComponent } from '../components/HealthComponent'; + +export class UIHealthBar extends UIComponent { + private hearts: UIImage[] = []; + private assetLoader: AssetLoader; + private uiCanvas: UICanvas | null = null; + private lastHealthValue: number = -1; + private heartSize: number = 20; // Taille d'un cœur en pixels (réduit à 20px) + private heartSpacing: number = 2; // Espacement entre les cœurs (réduit à 2px) + + constructor(assetLoader: AssetLoader, uiCanvas: UICanvas) { + super(); + this.assetLoader = assetLoader; + this.uiCanvas = uiCanvas; + } + + /** + * Lie la barre de vie à un HealthComponent + */ + bindToHealth(healthComponent: HealthComponent): void { + // Callback pour mettre à jour l'UI quand la vie change + healthComponent.onHealthChanged((current, max) => { + this.updateHearts(current, max); + }); + + // Initialise l'affichage + this.updateHearts(healthComponent.health, healthComponent.maxHealth); + } + + /** + * Met à jour l'affichage des cœurs selon la vie actuelle + */ + private updateHearts(currentHealth: number, maxHealth: number): void { + // Évite de redessiner si la vie n'a pas changé + if (currentHealth === this.lastHealthValue) { + return; + } + + this.lastHealthValue = currentHealth; + + // Calcule le nombre de cœurs nécessaires (1 cœur = 100 points de vie) + const maxHearts = Math.ceil(maxHealth / 100); + + // Crée ou supprime des cœurs si nécessaire + while (this.hearts.length < maxHearts) { + this.addHeart(this.hearts.length); + } + while (this.hearts.length > maxHearts) { + const heart = this.hearts.pop(); + if (heart && this.uiCanvas) { + this.uiCanvas.removeGameObject(heart.gameObject); + } + } + + // Met à jour chaque cœur + let remainingHealth = currentHealth; + for (let i = 0; i < this.hearts.length; i++) { + const heart = this.hearts[i]; + if (!heart) continue; // Sécurité + + const heartHealth = Math.min(100, remainingHealth); + this.updateHeartSprite(heart, heartHealth); + remainingHealth -= 100; + } + } + + /** + * Ajoute un nouveau cœur à l'UI + */ + private addHeart(index: number): void { + // Crée un GameObject pour le cœur + const heartObject = new GameObject(`Heart_${index}`); + + // Ajoute le composant UIImage + const heart = heartObject.addComponent(new UIImage()); + + // Position du cœur (horizontalement de droite à gauche) + // On inverse l'ordre : le dernier cœur (index le plus élevé) est le plus à droite + const totalHearts = 5; // Max 5 cœurs pour 500 HP + const reverseIndex = totalHearts - 1 - index; // Inverse l'ordre + const xOffset = reverseIndex * (this.heartSize + this.heartSpacing); + + heart.rectTransform.anchorMin = new Vector2(1, 1); // Ancre en haut à droite + heart.rectTransform.anchorMax = new Vector2(1, 1); + heart.rectTransform.pivot = new Vector2(1, 1); + heart.rectTransform.anchoredPosition = new Vector2(-40 - xOffset, -40); // 40px de marge (plus vers la gauche et plus bas) + heart.rectTransform.size = new Vector2(this.heartSize, this.heartSize); + + // Rotation pour orienter le cœur correctement + // Valeurs à tester : 0, 90, 180, 270, -90 + heartObject.transform.rotation = 90; // Tourne de 90° (essaie d'abord celle-ci) + + // Texture par défaut (cœur plein) + const texture = this.assetLoader.getTexture('heart_full'); + if (texture) { + heart.texture = texture; + } + + // Active l'ombre légère autour du cœur + heart.shadowEnabled = true; + heart.shadowOffset = new Vector2(1, 1); // Décalage léger de 1px en bas à droite + heart.shadowColor = new Color(0, 0, 0, 0.4); // Noir avec 40% d'opacité + + this.hearts.push(heart); + + // Ajoute le GameObject au UICanvas + if (this.uiCanvas) { + this.uiCanvas.addGameObject(heartObject); + } + } + + /** + * Met à jour le sprite d'un cœur selon sa vie + */ + private updateHeartSprite(heart: UIImage, health: number): void { + let textureName: string; + + if (health <= 0) { + textureName = 'heart_empty'; // Hearts_Red_1.png + } else if (health <= 25) { + textureName = 'heart_quarter'; // Hearts_Red_2.png (1 quart) + } else if (health <= 50) { + textureName = 'heart_half'; // Hearts_Red_3.png (2 quarts) + } else if (health <= 75) { + textureName = 'heart_three_quarters'; // Hearts_Red_4.png (3 quarts) + } else { + textureName = 'heart_full'; // Hearts_Red_5.png (plein) + } + + const texture = this.assetLoader.getTexture(textureName); + if (texture) { + heart.texture = texture; + } + } + + /** + * Rend l'UI (méthode abstraite de UIComponent) + * Les cœurs sont déjà rendus par le UICanvas, donc cette méthode est vide + */ + renderUI(_spriteBatch: SpriteBatch): void { + // Les cœurs sont des GameObject UI séparés, rendus directement par le UICanvas + // Cette méthode est vide mais doit être implémentée + } + + awake(): void { + super.awake(); + } + + update(deltaTime: number): void { + super.update(deltaTime); + } +} + diff --git a/src/game/utils/TextureGenerator.ts b/src/game/utils/TextureGenerator.ts new file mode 100644 index 0000000..e25c381 --- /dev/null +++ b/src/game/utils/TextureGenerator.ts @@ -0,0 +1,118 @@ +/** + * TextureGenerator + * Utilitaires pour générer des textures procédurales (pour tests sans images externes) + */ + +import { Texture } from '../../engine/rendering/Texture'; +import { Color } from '../../engine/math/Color'; + +export class TextureGenerator { + /** + * Crée une texture de couleur solide + */ + static createSolidColorTexture( + gl: WebGLRenderingContext | WebGL2RenderingContext, + width: number, + height: number, + color: Color + ): Texture { + const texture = new Texture(gl); + + // Crée un canvas temporaire pour générer l'image + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Impossible de créer le contexte 2D'); + } + + // Remplit avec la couleur + ctx.fillStyle = color.toRGBA(); + ctx.fillRect(0, 0, width, height); + + // Upload directement depuis le canvas + const webglTexture = (texture as any)._texture; + if (!webglTexture) { + throw new Error('Texture WebGL non créée'); + } + + gl.bindTexture(gl.TEXTURE_2D, webglTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (texture as any)._width = width; + (texture as any)._height = height; + + console.log(`Texture créée: ${width}x${height}, couleur: ${color.toRGBA()}`); + return texture; + } + + /** + * Crée une texture de tile avec bordure pour délimitation + */ + static createTileTextureWithBorder( + gl: WebGLRenderingContext | WebGL2RenderingContext, + size: number, + fillColor: Color, + borderColor: Color, + borderWidth: number = 1 + ): Texture { + const texture = new Texture(gl); + + // Crée un canvas temporaire + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Impossible de créer le contexte 2D'); + } + + // Remplit avec la couleur de fond + ctx.fillStyle = fillColor.toRGBA(); + ctx.fillRect(0, 0, size, size); + + // Dessine la bordure (on ne dessine que les bords droits et bas pour éviter les doubles bordures entre tiles) + ctx.strokeStyle = borderColor.toRGBA(); + ctx.lineWidth = borderWidth; + + // Bordure droite + ctx.beginPath(); + ctx.moveTo(size - borderWidth / 2, 0); + ctx.lineTo(size - borderWidth / 2, size); + ctx.stroke(); + + // Bordure bas + ctx.beginPath(); + ctx.moveTo(0, size - borderWidth / 2); + ctx.lineTo(size, size - borderWidth / 2); + ctx.stroke(); + + // Upload directement depuis le canvas + const webglTexture = (texture as any)._texture; + if (!webglTexture) { + throw new Error('Texture WebGL non créée'); + } + + gl.bindTexture(gl.TEXTURE_2D, webglTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, null); + + // Met à jour la taille + (texture as any)._width = size; + (texture as any)._height = size; + + return texture; + } +} + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e947eb5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,39 @@ +/** + * CastleStorm Engine + * Point d'entrée principal + */ + +import { Game } from './game/Game'; + +console.log('CastleStorm Engine v0.1.0'); +console.log('Initialisation...'); + +/** + * Initialisation du jeu + */ +async function init() { + const canvas = document.getElementById('game-canvas') as HTMLCanvasElement; + if (!canvas) { + console.error('Canvas introuvable'); + return; + } + + // Crée le jeu + const game = new Game(canvas); + + // Initialise le jeu + if (!(await game.initialize())) { + console.error('Échec de l\'initialisation du jeu'); + return; + } + + // Démarre le jeu + game.start(); +} + +// Lance l'initialisation quand le DOM est prêt +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..af4af45 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,51 @@ +{ + "compilerOptions": { + /* Langage et émission */ + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + + /* Type Checking - STRICT MODE */ + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + /* Émission */ + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "removeComments": false, + + /* Autres options */ + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": [ + "src/**/*", + "*.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "vite.config.ts" + ] +} + diff --git a/vite.config.d.ts b/vite.config.d.ts new file mode 100644 index 0000000..b8460a1 --- /dev/null +++ b/vite.config.d.ts @@ -0,0 +1,3 @@ +declare const _default: import("vite").UserConfig; +export default _default; +//# sourceMappingURL=vite.config.d.ts.map \ No newline at end of file diff --git a/vite.config.d.ts.map b/vite.config.d.ts.map new file mode 100644 index 0000000..d4f6972 --- /dev/null +++ b/vite.config.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["vite.config.ts"],"names":[],"mappings":";AAEA,wBAaG"} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..e1eb933 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +export default defineConfig({ + server: { + port: 3000, + open: true + }, + build: { + outDir: 'dist', + rollupOptions: { + input: { + main: './index.html' + } + } + } +}); +//# sourceMappingURL=vite.config.js.map \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..14668fe --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + port: 3000, + open: true + }, + build: { + outDir: 'dist', + rollupOptions: { + input: { + main: './index.html' + } + }, + // Inclut les assets dans le build + assetsInclude: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif'] + }, + // Public directory pour les assets statiques + publicDir: 'assets' +}); +