first
This commit is contained in:
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
*.tmp
|
||||||
|
.cache/
|
||||||
341
CHANGELOG.md
Normal file
341
CHANGELOG.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# Changelog - Mario Runner
|
||||||
|
|
||||||
|
## Version 2.2 - Système de Vies & Coffre Final 🏆❤️
|
||||||
|
|
||||||
|
### 🎮 NOUVEAU : Système de Vies
|
||||||
|
|
||||||
|
**Feature majeure** : Ajout d'un système de vies complet avec respawn !
|
||||||
|
|
||||||
|
- **3 vies au départ** (configurable)
|
||||||
|
- **Perte de vie** lors de collisions frontales avec obstacles
|
||||||
|
- **Respawn au checkpoint** avec invincibilité temporaire (2s)
|
||||||
|
- **Game Over** si toutes les vies sont perdues
|
||||||
|
|
||||||
|
#### Mécanique des Obstacles Améliorée
|
||||||
|
- **Sauter dessus** : Détruit l'obstacle + 50 points bonus + rebond
|
||||||
|
- **Collision frontale** : Perte d'une vie (sauf si invincible)
|
||||||
|
- Feedback visuel :
|
||||||
|
- ⚡ Flash rouge lors de la perte de vie
|
||||||
|
- 💚 Explosion verte lors de la destruction
|
||||||
|
- 🛡️ Clignotement pendant l'invincibilité
|
||||||
|
|
||||||
|
#### Système de Checkpoint
|
||||||
|
- **Sauvegarde automatique** tous les 1000px
|
||||||
|
- ⚡ Flash vert au passage d'un checkpoint
|
||||||
|
- Le joueur réapparaît au dernier checkpoint après une mort
|
||||||
|
- Invincibilité de 2 secondes après respawn (alpha clignotant)
|
||||||
|
|
||||||
|
#### Interface Utilisateur
|
||||||
|
- ❤️ **Compteur de vies** affiché en haut à gauche
|
||||||
|
- 🎁 **Cadeaux collectés** avec progression (X/15)
|
||||||
|
- Mise à jour en temps réel
|
||||||
|
|
||||||
|
### 🏆 NOUVEAU : Coffre au Trésor Final
|
||||||
|
|
||||||
|
**Récompense ultime** à la fin du niveau !
|
||||||
|
|
||||||
|
- **Coffre géant** placé sur la plateforme finale (x=7700)
|
||||||
|
- **Condition d'ouverture** : Avoir collecté 15 cadeaux minimum
|
||||||
|
- **Récompense** : MEGA BONUS de +1000 points !
|
||||||
|
|
||||||
|
#### Effets Visuels Spectaculaires
|
||||||
|
- 🌟 Aura dorée pulsante autour du coffre
|
||||||
|
- 💫 Texte flottant "🎁 15 cadeaux requis"
|
||||||
|
- ⚡ Flash doré géant à l'ouverture
|
||||||
|
- 💥 Explosion de 20 particules dorées
|
||||||
|
- 🏆 Message épique "COFFRE OUVERT ! MEGA BONUS +1000"
|
||||||
|
|
||||||
|
#### Feedback Progressif
|
||||||
|
- Message "🏆 Assez de cadeaux! Trouvez le coffre!" dès 15 cadeaux collectés
|
||||||
|
- Le compteur 🎁 change de couleur (jaune doré)
|
||||||
|
- Indicateur visuel au-dessus du coffre
|
||||||
|
|
||||||
|
#### Système d'Interaction
|
||||||
|
- **Overlap** : Se rapprocher du coffre suffit
|
||||||
|
- Vérification automatique du nombre de cadeaux
|
||||||
|
- Une seule ouverture possible par partie
|
||||||
|
|
||||||
|
### 📊 Statistiques de la v2.2
|
||||||
|
|
||||||
|
```
|
||||||
|
Vies de départ : 3
|
||||||
|
Invincibilité : 2000ms après respawn
|
||||||
|
Checkpoints : Tous les 1000px
|
||||||
|
Coffre requis : 15 cadeaux
|
||||||
|
Bonus coffre : +1000 points
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Nouvelles Règles du Jeu
|
||||||
|
|
||||||
|
#### Gestion des Obstacles
|
||||||
|
1. **Sauter dessus (par le haut)** :
|
||||||
|
- ✅ Détruit l'obstacle
|
||||||
|
- ✅ +50 points
|
||||||
|
- ✅ Petit rebond automatique
|
||||||
|
- Effet explosion verte
|
||||||
|
|
||||||
|
2. **Collision (frontale/latérale)** :
|
||||||
|
- ❌ Perd une vie
|
||||||
|
- ⚡ Flash rouge
|
||||||
|
- 🔄 Respawn au checkpoint si vies restantes
|
||||||
|
- 💀 Game Over si plus de vies
|
||||||
|
|
||||||
|
#### Système de Progression
|
||||||
|
1. Collecter des cadeaux (+100) et super trésors (+500)
|
||||||
|
2. Atteindre 15 cadeaux minimum
|
||||||
|
3. Trouver le coffre final (x=7700)
|
||||||
|
4. Ouvrir le coffre pour le MEGA BONUS (+1000)
|
||||||
|
5. Survivre jusqu'à la fin avec 3 vies maximum
|
||||||
|
|
||||||
|
#### Score Maximum Possible
|
||||||
|
```
|
||||||
|
24 cadeaux normaux : 24 × 100 = 2,400 pts
|
||||||
|
6 super trésors : 6 × 500 = 3,000 pts
|
||||||
|
1 coffre final : 1 × 1000 = 1,000 pts
|
||||||
|
Obstacles détruits : ~24 × 50 = 1,200 pts
|
||||||
|
─────────────────────────────────────────────
|
||||||
|
TOTAL MAXIMUM : 7,600 pts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 Changements Techniques
|
||||||
|
|
||||||
|
#### Nouvelles Constantes
|
||||||
|
```typescript
|
||||||
|
// src/utils/constants.ts
|
||||||
|
PLAYER_STARTING_LIVES: 3
|
||||||
|
RESPAWN_INVINCIBILITY_TIME: 2000
|
||||||
|
CHEST_REQUIRED_GIFTS: 15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Classe Player.ts - Invincibilité
|
||||||
|
```typescript
|
||||||
|
private isInvincible: boolean = false;
|
||||||
|
private invincibilityTimer?: Phaser.Time.TimerEvent;
|
||||||
|
|
||||||
|
public makeInvincible(scene: Phaser.Scene): void {
|
||||||
|
// Effet de clignotement alpha (0.3 ↔ 1.0)
|
||||||
|
// Timer de 2 secondes
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIsInvincible(): boolean {
|
||||||
|
// Vérifie l'état d'invincibilité
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Nouvelle Classe TreasureChest.ts
|
||||||
|
```typescript
|
||||||
|
export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
private isOpen: boolean = false;
|
||||||
|
private requiredGifts: number;
|
||||||
|
|
||||||
|
public canOpen(giftsCollected: number): boolean
|
||||||
|
public open(scene: Phaser.Scene): number
|
||||||
|
public getIsOpen(): boolean
|
||||||
|
public getRequiredGifts(): number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GameScene.ts - Nouvelles Fonctions
|
||||||
|
```typescript
|
||||||
|
private lives: number;
|
||||||
|
private giftsCollected: number;
|
||||||
|
private lastCheckpointX: number;
|
||||||
|
private treasureChest?: TreasureChest;
|
||||||
|
|
||||||
|
private openChest() // Interaction avec le coffre
|
||||||
|
private loseLife() // Gestion perte de vie
|
||||||
|
private respawnPlayer() // Téléportation au checkpoint
|
||||||
|
private gameOver() // Écran de fin si plus de vies
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mécanique de Détection de Saut
|
||||||
|
```typescript
|
||||||
|
private hitObstacle(player: any, obstacle: any): void {
|
||||||
|
const isJumpingOn =
|
||||||
|
playerBody.velocity.y > 0 &&
|
||||||
|
playerBody.bottom <= obstacleBody.top + 10;
|
||||||
|
|
||||||
|
if (isJumpingOn) {
|
||||||
|
// Destruction
|
||||||
|
} else {
|
||||||
|
if (!player.getIsInvincible()) {
|
||||||
|
this.loseLife();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🐛 Corrections
|
||||||
|
|
||||||
|
- ✅ Invincibilité fonctionne correctement après respawn
|
||||||
|
- ✅ Checkpoints sauvegardent la position tous les 1000px
|
||||||
|
- ✅ Détection précise saut vs collision sur obstacles
|
||||||
|
- ✅ UI vies et cadeaux mise à jour en temps réel
|
||||||
|
- ✅ Game Over arrête correctement la physique
|
||||||
|
|
||||||
|
### 💡 Conseils de Jeu
|
||||||
|
|
||||||
|
1. **Maîtrisez le saut sur obstacles** : Vous gagnez des points au lieu d'en perdre !
|
||||||
|
2. **Cherchez tous les cadeaux** : Il en faut 15 pour le coffre final
|
||||||
|
3. **Attention aux checkpoints** : Vous réapparaitrez là où vous étiez il y a 1000px
|
||||||
|
4. **3 vies seulement** : Soyez prudent, chaque vie compte !
|
||||||
|
5. **Invincibilité** : Profitez des 2 secondes après respawn pour passer les zones dangereuses
|
||||||
|
6. **Coffre final** : N'oubliez pas d'aller tout au bout (x=7700) pour le mega bonus !
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 2.1 - Super Trésors 🌟💰
|
||||||
|
|
||||||
|
### 🎁 NOUVEAU : Super Trésors
|
||||||
|
|
||||||
|
**Feature majeure** : Ajout de super trésors ultra précieux !
|
||||||
|
|
||||||
|
- **6 super trésors** répartis dans le niveau (1 par zone)
|
||||||
|
- **+500 points** par collecte (5x plus qu'un cadeau normal !)
|
||||||
|
- **Score max total** : 5,400 points (vs 2,400 avant)
|
||||||
|
|
||||||
|
#### Effets Visuels Spectaculaires
|
||||||
|
- 🌟 Rotation rapide + pulsation
|
||||||
|
- ⭐ 3 étoiles qui orbitent autour
|
||||||
|
- ✨ Effet de brillance scintillant
|
||||||
|
- ⚡ Flash doré à la collecte
|
||||||
|
- 🎯 Message géant "★ SUPER TRÉSOR +500 ★"
|
||||||
|
|
||||||
|
#### Placement Stratégique
|
||||||
|
- Placés en **hauteur** (nécessite double saut)
|
||||||
|
- Difficulté croissante par zone
|
||||||
|
- Zone 5 : Ultra difficile (-500px de hauteur)
|
||||||
|
- Zone 6 : Sur la plateforme finale
|
||||||
|
|
||||||
|
#### Classe Technique
|
||||||
|
- Nouvelle classe `SuperTreasure` avec animations avancées
|
||||||
|
- Taille 1.5x plus grande que les cadeaux
|
||||||
|
- Effet de particules avec étoiles orbitales
|
||||||
|
- Destruction automatique des timers/tweens
|
||||||
|
|
||||||
|
Consultez [SUPER_TREASURES.md](SUPER_TREASURES.md) pour le guide complet !
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 2.0 - Améliorations Majeures 🚀
|
||||||
|
|
||||||
|
### 🎮 Gameplay
|
||||||
|
|
||||||
|
#### Double Saut Implémenté ✨
|
||||||
|
- **NOUVEAU** : Le joueur peut maintenant faire un **double saut** !
|
||||||
|
- Appuyez deux fois sur Espace (PC) ou le bouton tactile (Mobile)
|
||||||
|
- Permet d'atteindre les plateformes les plus hautes
|
||||||
|
- Compteur de sauts visible dans la console (debug)
|
||||||
|
|
||||||
|
#### Saut Amélioré
|
||||||
|
- Force de saut augmentée : `-400` → **`-550`**
|
||||||
|
- Les plateformes sont maintenant accessibles
|
||||||
|
|
||||||
|
### 🗺 Niveau Étendu
|
||||||
|
|
||||||
|
#### Taille du Niveau
|
||||||
|
- **Avant** : 3x la largeur de l'écran (~3840px)
|
||||||
|
- **MAINTENANT** : **6x la largeur de l'écran (~7680px)**
|
||||||
|
- Durée de jeu augmentée significativement
|
||||||
|
|
||||||
|
#### Plateformes
|
||||||
|
- **Avant** : 7 plateformes
|
||||||
|
- **MAINTENANT** : **27 plateformes** réparties en 6 zones
|
||||||
|
- Zone 1 : Facile (début)
|
||||||
|
- Zone 2 : Moyen
|
||||||
|
- Zone 3 : Plus difficile
|
||||||
|
- Zone 4 : Avancé
|
||||||
|
- Zone 5 : Très difficile
|
||||||
|
- Zone 6 : Finale (grande plateforme)
|
||||||
|
|
||||||
|
#### Objets
|
||||||
|
|
||||||
|
**Cadeaux** :
|
||||||
|
- **Avant** : 4 cadeaux
|
||||||
|
- **MAINTENANT** : **24 cadeaux** (+500%)
|
||||||
|
- Répartis partout sur le niveau
|
||||||
|
- Alternance entre sol et hauteur variable
|
||||||
|
|
||||||
|
**Obstacles** :
|
||||||
|
- **Avant** : 3 obstacles
|
||||||
|
- **MAINTENANT** : **24 obstacles** (+700%)
|
||||||
|
- Répartis régulièrement tous les 300px environ
|
||||||
|
|
||||||
|
### 📊 Statistiques
|
||||||
|
|
||||||
|
```
|
||||||
|
Niveau : 7680px (6x écran)
|
||||||
|
Plateformes : 27 (+286%)
|
||||||
|
Cadeaux : 24 (+500%)
|
||||||
|
Obstacles : 24 (+700%)
|
||||||
|
Force de saut : -550 (+37.5%)
|
||||||
|
Sauts max : 2 (NOUVEAU)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎯 Difficulté
|
||||||
|
|
||||||
|
Le jeu est maintenant **beaucoup plus long et varié** :
|
||||||
|
- Progression de difficulté graduelle sur 6 zones
|
||||||
|
- Nécessite maîtrise du double saut pour les zones avancées
|
||||||
|
- Plus de récompenses à collecter
|
||||||
|
- Plus de défis à éviter
|
||||||
|
|
||||||
|
### 🔧 Changements Techniques
|
||||||
|
|
||||||
|
#### Constantes Modifiées
|
||||||
|
```typescript
|
||||||
|
// src/utils/constants.ts
|
||||||
|
PLAYER_JUMP_VELOCITY: -550 (était -400)
|
||||||
|
PLAYER_MAX_JUMPS: 2 (NOUVEAU)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Modifications Classes
|
||||||
|
|
||||||
|
**Player.ts** :
|
||||||
|
- Ajout du compteur de sauts (`jumpCount`)
|
||||||
|
- Logique de double saut implémentée
|
||||||
|
- Réinitialisation automatique au sol
|
||||||
|
|
||||||
|
**GameScene.ts** :
|
||||||
|
- Monde physique étendu à 6x
|
||||||
|
- 27 plateformes avec progression de difficulté
|
||||||
|
- 24 cadeaux répartis intelligemment
|
||||||
|
- 24 obstacles stratégiquement placés
|
||||||
|
|
||||||
|
### 🎮 Comment Jouer
|
||||||
|
|
||||||
|
#### PC
|
||||||
|
- **Déplacements** : ← →
|
||||||
|
- **Saut** : Espace
|
||||||
|
- **Double Saut** : Appuyez Espace une 2ème fois en l'air !
|
||||||
|
|
||||||
|
#### Mobile
|
||||||
|
- **Déplacements** : Inclinez le téléphone
|
||||||
|
- **Saut** : Bouton vert en bas à droite
|
||||||
|
- **Double Saut** : Appuyez le bouton une 2ème fois en l'air !
|
||||||
|
|
||||||
|
### 💡 Astuces
|
||||||
|
|
||||||
|
1. **Maîtrisez le double saut** : Indispensable pour les plateformes hautes
|
||||||
|
2. **Explorez** : Le niveau est 6x plus grand, prenez votre temps
|
||||||
|
3. **Collectez tout** : 24 cadeaux = 2400 points potentiels !
|
||||||
|
4. **Évitez les obstacles** : 24 obstacles = -1200 points si tous touchés
|
||||||
|
5. **Score parfait** : 2400 points (tous les cadeaux, aucun obstacle)
|
||||||
|
|
||||||
|
### 🐛 Corrections
|
||||||
|
|
||||||
|
- ✅ Monde physique correctement dimensionné
|
||||||
|
- ✅ Joueur ne se bloque plus au bord de l'écran
|
||||||
|
- ✅ Double saut fonctionnel et fluide
|
||||||
|
- ✅ Collisions optimisées pour le grand niveau
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 1.0 - Version Initiale
|
||||||
|
|
||||||
|
- Jeu de plateforme basique
|
||||||
|
- Support PC et Mobile
|
||||||
|
- Gyroscope + contrôles tactiles
|
||||||
|
- 7 plateformes
|
||||||
|
- 4 cadeaux, 3 obstacles
|
||||||
|
- Timer de 3 minutes
|
||||||
102
CLAUDE prompt .md
Normal file
102
CLAUDE prompt .md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Rôle
|
||||||
|
|
||||||
|
Tu es un assistant expert en développement de jeux web 2D pour mobile,
|
||||||
|
spécialisé en : - JavaScript / TypeScript - Phaser 3 - HTML5 / CSS -
|
||||||
|
Progressive Web Apps (PWA) - Intégration mobile iOS / Android (Safari,
|
||||||
|
Chrome, WebView)
|
||||||
|
|
||||||
|
Ton objectif est de m'aider à concevoir et coder un petit jeu de type
|
||||||
|
"runner / plateforme" inspiré de Super Mario, jouable dans un navigateur
|
||||||
|
mobile sur iPhone et Android, avec contrôle au gyroscope et bouton de
|
||||||
|
saut.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Contexte projet
|
||||||
|
|
||||||
|
Je veux créer un jeu web pour smartphone avec les caractéristiques
|
||||||
|
suivantes :
|
||||||
|
|
||||||
|
- Plateforme cible :
|
||||||
|
- Web mobile, jouable dans le navigateur (Safari iOS, Chrome
|
||||||
|
Android, etc.).
|
||||||
|
- Option PWA pour pouvoir être lancé comme une "vraie" application
|
||||||
|
(fullscreen, orientation paysage).
|
||||||
|
- Mécaniques de base :
|
||||||
|
- Le joueur contrôle un personnage (mon neveu, représenté par un
|
||||||
|
sprite dérivé d'une photo).
|
||||||
|
- Le téléphone est en **mode paysage**.
|
||||||
|
- Le **gyroscope / deviceorientation** sert à avancer / reculer :
|
||||||
|
- inclinaison vers la droite → le personnage avance (vitesse
|
||||||
|
positive),
|
||||||
|
- inclinaison vers la gauche → le personnage recule (vitesse
|
||||||
|
négative),
|
||||||
|
- zone morte au centre (deadzone) pour que le perso reste
|
||||||
|
immobile quand le téléphone est presque horizontal.
|
||||||
|
- Un **gros bouton "Saut"** est affiché en bas à droite de l'écran
|
||||||
|
:
|
||||||
|
- appui tactile → saut si le personnage est au sol.
|
||||||
|
- Gameplay :
|
||||||
|
- Le personnage doit **éviter des obstacles**.
|
||||||
|
- Il doit **récupérer des cadeaux / bonus**.
|
||||||
|
- Il y a un **fond qui défile** (ou une caméra qui suit le joueur)
|
||||||
|
et des **plateformes** (sol + plateformes en l'air).
|
||||||
|
- Un niveau doit durer environ **3 minutes** :
|
||||||
|
- soit avec un **chrono** (180 s de jeu puis écran de fin),
|
||||||
|
- soit avec une **distance de niveau** calibrée pour \~3
|
||||||
|
minutes à vitesse moyenne.
|
||||||
|
- Cross-platform capteurs :
|
||||||
|
- Sur iOS : utiliser `DeviceOrientationEvent.requestPermission()`
|
||||||
|
après un geste utilisateur (bouton "Démarrer").
|
||||||
|
- Sur Android : utiliser `deviceorientation` sans
|
||||||
|
`requestPermission`.
|
||||||
|
- Normaliser une valeur de tilt (par ex. -30..+30 degrés)
|
||||||
|
utilisable dans Phaser pour fixer la vitesse horizontale du
|
||||||
|
joueur.
|
||||||
|
- Personnage / sprites :
|
||||||
|
- Partir d'une **photo de mon neveu**.
|
||||||
|
- Proposer un workflow pour transformer cette photo en **sprites
|
||||||
|
animés** (idle, marche/course, saut) :
|
||||||
|
- détourage, réduction, simplification graphique,
|
||||||
|
- création d'une spritesheet (par exemple 64x64 ou 128x128,
|
||||||
|
plusieurs frames par animation).
|
||||||
|
- Utiliser les sprites dans Phaser via `this.load.spritesheet` et
|
||||||
|
`this.anims.create`.
|
||||||
|
- Background, plateformes, objets :
|
||||||
|
- Fond qui défile (par exemple via `tileSprite`) ou tilemap avec
|
||||||
|
caméra qui suit.
|
||||||
|
- Plateformes :
|
||||||
|
- sol et plateformes statiques (`staticGroup`),
|
||||||
|
- éventuellement plateformes mobiles.
|
||||||
|
- Objets :
|
||||||
|
- obstacles (collision → perte/gêne),
|
||||||
|
- cadeaux (overlap → score).
|
||||||
|
- Possibilité d'utiliser un éditeur de niveau type **Tiled**
|
||||||
|
(tilemap JSON) pour placer plateformes / obstacles / cadeaux.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Objectifs techniques
|
||||||
|
|
||||||
|
Je veux que tu m'aides à :
|
||||||
|
|
||||||
|
1. Définir une **stack technique complète** et cohérente.
|
||||||
|
2. Gérer correctement le **full screen** mobile.
|
||||||
|
3. Implémenter le **contrôle gyroscope commun iOS + Android**.
|
||||||
|
4. Implémenter le **bouton de saut**.
|
||||||
|
5. Concevoir et coder un **prototype de niveau** (\~3 minutes).
|
||||||
|
6. Proposer un **workflow sprite** depuis une photo.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Style de réponse attendu
|
||||||
|
|
||||||
|
- Donner du **code concret**, prêt à copier-coller.
|
||||||
|
- Structure par **étapes** : architecture → init Phaser → gyroscope →
|
||||||
|
bouton → fond → plateformes → objets → niveau 3 min.
|
||||||
|
- Commentaires dans le code.
|
||||||
|
- Explications en **français**, code en anglais.
|
||||||
|
|
||||||
|
Premier message attendu : 1. Récapitulatif structure projet. 2.
|
||||||
|
Arborescence recommandée. 3. Choix JS/TS + outil de build. 4. Fournir un
|
||||||
|
`index.html` minimal + config Phaser.
|
||||||
84
CLAUDE.md
Normal file
84
CLAUDE.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Mobile web game - a 2D runner/platformer inspired by Super Mario, playable in mobile browsers (iOS Safari, Android Chrome) with gyroscope controls and touch jump button.
|
||||||
|
|
||||||
|
**Target Platform**: Mobile web (landscape orientation) with PWA support for fullscreen experience.
|
||||||
|
|
||||||
|
**Core Mechanics**:
|
||||||
|
- Character controlled by phone gyroscope (tilt right = move forward, tilt left = move backward)
|
||||||
|
- Touch button for jumping (bottom-right corner)
|
||||||
|
- Avoid obstacles, collect gifts/bonuses
|
||||||
|
- Level duration: ~3 minutes (timed or distance-based)
|
||||||
|
|
||||||
|
**Custom Character**: The player character is based on a photo of the developer's nephew, converted to animated sprites.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Game Engine**: Phaser 3
|
||||||
|
- **Language**: JavaScript or TypeScript
|
||||||
|
- **Build Tool**: TBD (Vite, Webpack, or Parcel recommended)
|
||||||
|
- **PWA**: Service worker + manifest.json for app-like experience
|
||||||
|
- **Mobile APIs**: DeviceOrientationEvent for gyroscope control
|
||||||
|
|
||||||
|
## Gyroscope Implementation Requirements
|
||||||
|
|
||||||
|
**iOS-specific**: Must call `DeviceOrientationEvent.requestPermission()` after user gesture (e.g., "Start" button).
|
||||||
|
|
||||||
|
**Android**: Direct `deviceorientation` event listener, no permission required.
|
||||||
|
|
||||||
|
**Normalization**: Convert device tilt to normalized value (e.g., -30° to +30°) with deadzone at center for "idle" state when phone is horizontal.
|
||||||
|
|
||||||
|
**Integration with Phaser**: Normalized tilt value controls player horizontal velocity.
|
||||||
|
|
||||||
|
## Sprite Workflow
|
||||||
|
|
||||||
|
Character sprites created from photo with these steps:
|
||||||
|
1. Background removal (détourage)
|
||||||
|
2. Size reduction and graphic simplification
|
||||||
|
3. Spritesheet creation (64x64 or 128x128 per frame)
|
||||||
|
4. Multiple animation states: idle, walk/run, jump
|
||||||
|
|
||||||
|
Load in Phaser using:
|
||||||
|
- `this.load.spritesheet()` in preload
|
||||||
|
- `this.anims.create()` for animation definitions
|
||||||
|
|
||||||
|
## Level Design
|
||||||
|
|
||||||
|
**Background**: Scrolling background using `tileSprite` or tilemap with camera follow.
|
||||||
|
|
||||||
|
**Platforms**:
|
||||||
|
- Ground and static platforms using `staticGroup`
|
||||||
|
- Optional: moving platforms
|
||||||
|
|
||||||
|
**Objects**:
|
||||||
|
- Obstacles: collision detection → damage/penalty
|
||||||
|
- Gifts: overlap detection → score increase
|
||||||
|
|
||||||
|
**Level Editor**: Consider using Tiled for level layout (platforms, obstacles, gifts placement) exported as JSON tilemap.
|
||||||
|
|
||||||
|
## Game Structure
|
||||||
|
|
||||||
|
**3-minute level design**:
|
||||||
|
- Option 1: 180-second countdown timer → end screen
|
||||||
|
- Option 2: Fixed level distance calibrated for ~3 minutes at average speed
|
||||||
|
|
||||||
|
## Mobile-Specific Requirements
|
||||||
|
|
||||||
|
**Fullscreen**: Implement fullscreen API for immersive mobile experience.
|
||||||
|
|
||||||
|
**Orientation**: Force landscape mode (CSS + screen orientation API).
|
||||||
|
|
||||||
|
**Touch Controls**: Large, accessible jump button (bottom-right) optimized for thumb reach.
|
||||||
|
|
||||||
|
**Performance**: Optimize for mobile GPU/CPU constraints.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Code in English with French comments/documentation
|
||||||
|
- Provide concrete, copy-paste ready code
|
||||||
|
- Step-by-step implementation approach: architecture → Phaser init → gyroscope → jump button → background → platforms → objects → 3-min level
|
||||||
|
- Include inline code comments for clarity
|
||||||
321
DEPLOY.md
Normal file
321
DEPLOY.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# Guide de Déploiement
|
||||||
|
|
||||||
|
## Options de Déploiement
|
||||||
|
|
||||||
|
### 1. GitHub Pages (Gratuit)
|
||||||
|
|
||||||
|
**Prérequis** : Compte GitHub
|
||||||
|
|
||||||
|
**Étapes** :
|
||||||
|
|
||||||
|
1. Créez un repo GitHub et poussez le code :
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
git branch -M main
|
||||||
|
git remote add origin https://github.com/VOTRE_USERNAME/mario-runner.git
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Activez GitHub Pages :
|
||||||
|
- Allez dans Settings → Pages
|
||||||
|
- Source : GitHub Actions
|
||||||
|
- Le workflow `.github/workflows/deploy.yml` est déjà configuré
|
||||||
|
|
||||||
|
3. Le jeu sera disponible sur : `https://VOTRE_USERNAME.github.io/mario-runner/`
|
||||||
|
|
||||||
|
**Note** : GitHub Pages utilise HTTPS automatiquement, donc le gyroscope iOS fonctionnera !
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Netlify (Gratuit, recommandé)
|
||||||
|
|
||||||
|
**Prérequis** : Compte Netlify
|
||||||
|
|
||||||
|
**Méthode 1 : Drag & Drop**
|
||||||
|
|
||||||
|
1. Build le projet :
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Allez sur [netlify.com](https://www.netlify.com/)
|
||||||
|
3. Drag & drop le dossier `dist/` sur Netlify
|
||||||
|
4. Le jeu est déployé instantanément !
|
||||||
|
|
||||||
|
**Méthode 2 : CLI**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g netlify-cli
|
||||||
|
npm run build
|
||||||
|
netlify deploy --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
**Méthode 3 : Git Integration**
|
||||||
|
|
||||||
|
1. Poussez sur GitHub
|
||||||
|
2. Connectez Netlify à votre repo
|
||||||
|
3. Configuration :
|
||||||
|
- Build command: `npm run build`
|
||||||
|
- Publish directory: `dist`
|
||||||
|
4. Déploiement automatique à chaque push
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Vercel (Gratuit)
|
||||||
|
|
||||||
|
**Prérequis** : Compte Vercel
|
||||||
|
|
||||||
|
**Méthode CLI** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g vercel
|
||||||
|
vercel
|
||||||
|
```
|
||||||
|
|
||||||
|
Suivez les instructions.
|
||||||
|
|
||||||
|
**Méthode Git** :
|
||||||
|
|
||||||
|
1. Poussez sur GitHub
|
||||||
|
2. Importez le projet sur [vercel.com](https://vercel.com)
|
||||||
|
3. Vercel détecte Vite automatiquement
|
||||||
|
4. Déploiement automatique
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Self-Hosting (Serveur personnel)
|
||||||
|
|
||||||
|
**Avec un serveur Node.js** :
|
||||||
|
|
||||||
|
1. Build :
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Servez le dossier `dist/` avec n'importe quel serveur web :
|
||||||
|
|
||||||
|
**Option A : serve (simple)** :
|
||||||
|
```bash
|
||||||
|
npm install -g serve
|
||||||
|
serve -s dist -p 80
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B : nginx** :
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name votredomaine.com;
|
||||||
|
root /path/to/mario-runner/dist;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C : Apache** :
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:80>
|
||||||
|
DocumentRoot /path/to/mario-runner/dist
|
||||||
|
<Directory /path/to/mario-runner/dist>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration PWA
|
||||||
|
|
||||||
|
Pour que le jeu fonctionne en mode PWA (installable) :
|
||||||
|
|
||||||
|
### 1. Créez les icônes
|
||||||
|
|
||||||
|
Générez les icônes 192x192 et 512x512 :
|
||||||
|
- Utilisez [pwabuilder.com/imageGenerator](https://www.pwabuilder.com/imageGenerator)
|
||||||
|
- Placez dans `public/icons/`
|
||||||
|
|
||||||
|
### 2. Service Worker
|
||||||
|
|
||||||
|
Créez `public/service-worker.js` :
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const CACHE_NAME = 'mario-runner-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/assets/index.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then(cache => cache.addAll(urlsToCache))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => response || fetch(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Testez l'installation
|
||||||
|
|
||||||
|
1. Déployez sur HTTPS (GitHub Pages, Netlify, etc.)
|
||||||
|
2. Sur mobile :
|
||||||
|
- iOS Safari : Partager → Sur l'écran d'accueil
|
||||||
|
- Android Chrome : Menu → Installer l'application
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test sur Mobile en Local
|
||||||
|
|
||||||
|
### Avec HTTPS (requis pour gyroscope iOS)
|
||||||
|
|
||||||
|
**Option 1 : ngrok** (recommandé)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g ngrok
|
||||||
|
npm run dev
|
||||||
|
# Dans un autre terminal :
|
||||||
|
ngrok http 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
Vous obtiendrez une URL HTTPS : `https://xyz123.ngrok.io`
|
||||||
|
|
||||||
|
**Option 2 : localtunel**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g localtunnel
|
||||||
|
npm run dev
|
||||||
|
# Dans un autre terminal :
|
||||||
|
lt --port 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3 : Certificat SSL local**
|
||||||
|
|
||||||
|
Avec `mkcert` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installation
|
||||||
|
brew install mkcert # macOS
|
||||||
|
# ou apt install mkcert # Linux
|
||||||
|
|
||||||
|
# Créer certificat
|
||||||
|
mkcert -install
|
||||||
|
mkcert localhost 127.0.0.1 ::1
|
||||||
|
|
||||||
|
# Modifier vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
https: {
|
||||||
|
key: fs.readFileSync('./localhost-key.pem'),
|
||||||
|
cert: fs.readFileSync('./localhost.pem'),
|
||||||
|
},
|
||||||
|
host: true,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimisations Pré-Déploiement
|
||||||
|
|
||||||
|
### 1. Compression des Assets
|
||||||
|
|
||||||
|
Compressez les images :
|
||||||
|
```bash
|
||||||
|
npm install -D imagemin imagemin-pngquant
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Minification
|
||||||
|
|
||||||
|
Déjà géré par Vite en mode production.
|
||||||
|
|
||||||
|
### 3. Analyse du Bundle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npx vite-bundle-visualizer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérification Post-Déploiement
|
||||||
|
|
||||||
|
### Checklist :
|
||||||
|
|
||||||
|
- [ ] Le jeu se charge correctement
|
||||||
|
- [ ] Les contrôles clavier fonctionnent (PC)
|
||||||
|
- [ ] Le gyroscope fonctionne (mobile HTTPS)
|
||||||
|
- [ ] Le bouton de saut fonctionne (mobile)
|
||||||
|
- [ ] Les collisions fonctionnent
|
||||||
|
- [ ] Le timer compte correctement
|
||||||
|
- [ ] L'orientation paysage est forcée
|
||||||
|
- [ ] La PWA est installable (optionnel)
|
||||||
|
|
||||||
|
### Outils de Test :
|
||||||
|
|
||||||
|
- **Chrome DevTools** : Device Mode pour simuler mobile
|
||||||
|
- **Lighthouse** : Auditer performance et PWA
|
||||||
|
- **WebPageTest** : Tester vitesse de chargement
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problèmes Courants
|
||||||
|
|
||||||
|
### Le jeu ne se charge pas
|
||||||
|
|
||||||
|
- Vérifiez la console pour erreurs
|
||||||
|
- Vérifiez que tous les assets sont dans `public/`
|
||||||
|
- Vérifiez que le build a réussi : `npm run build`
|
||||||
|
|
||||||
|
### Le gyroscope ne fonctionne pas
|
||||||
|
|
||||||
|
- **iOS** : Nécessite HTTPS obligatoirement
|
||||||
|
- Vérifiez que la permission a été accordée
|
||||||
|
- Testez sur un vrai appareil (pas simulateur)
|
||||||
|
|
||||||
|
### Erreur 404 sur les routes
|
||||||
|
|
||||||
|
Configurez le serveur pour servir `index.html` pour toutes les routes.
|
||||||
|
|
||||||
|
**Netlify** : Créez `public/_redirects` :
|
||||||
|
```
|
||||||
|
/* /index.html 200
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vercel** : Créez `vercel.json` :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommandation
|
||||||
|
|
||||||
|
Pour ce projet, je recommande **Netlify** :
|
||||||
|
- Gratuit
|
||||||
|
- HTTPS automatique
|
||||||
|
- Déploiement en drag & drop
|
||||||
|
- Bon support PWA
|
||||||
|
- Facile à utiliser
|
||||||
|
|
||||||
|
Commande rapide :
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npx netlify-cli deploy --prod --dir=dist
|
||||||
|
```
|
||||||
292
DEVELOPMENT_GUIDE.md
Normal file
292
DEVELOPMENT_GUIDE.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
# Guide de Développement - Mario Runner
|
||||||
|
|
||||||
|
## 🎮 État du Projet
|
||||||
|
|
||||||
|
### ✅ Fonctionnalités Implémentées
|
||||||
|
|
||||||
|
#### Infrastructure
|
||||||
|
- ✅ Configuration TypeScript + Vite
|
||||||
|
- ✅ Structure de projet organisée
|
||||||
|
- ✅ Phaser 3 intégré
|
||||||
|
- ✅ PWA configuré (manifest.json)
|
||||||
|
- ✅ Build et compilation fonctionnels
|
||||||
|
|
||||||
|
#### Contrôles
|
||||||
|
- ✅ **PC** : Contrôle clavier (Flèches gauche/droite + Espace/Haut pour sauter)
|
||||||
|
- ✅ **Mobile** : Gyroscope pour mouvement + Bouton tactile pour sauter
|
||||||
|
- ✅ Détection automatique PC vs Mobile
|
||||||
|
- ✅ Permission gyroscope iOS (requestPermission)
|
||||||
|
- ✅ Zone morte gyroscope (deadzone) pour stabilité
|
||||||
|
|
||||||
|
#### Jeu
|
||||||
|
- ✅ Classe Player avec physique Arcade
|
||||||
|
- ✅ Mouvement fluide avec accélération/décélération
|
||||||
|
- ✅ Saut avec gravité
|
||||||
|
- ✅ Background qui défile avec effet parallaxe
|
||||||
|
- ✅ Plateformes statiques (sol + plateformes en l'air)
|
||||||
|
- ✅ Obstacles (collision → perte de points)
|
||||||
|
- ✅ Cadeaux (collecte → gain de points)
|
||||||
|
- ✅ Système de score
|
||||||
|
- ✅ Timer de 3 minutes
|
||||||
|
- ✅ Caméra qui suit le joueur
|
||||||
|
- ✅ Niveau étendu (3x la largeur de l'écran)
|
||||||
|
|
||||||
|
#### Scènes
|
||||||
|
- ✅ BootScene : Chargement des assets
|
||||||
|
- ✅ MenuScene : Menu avec demande permission gyroscope
|
||||||
|
- ✅ GameScene : Jeu principal
|
||||||
|
|
||||||
|
#### UI
|
||||||
|
- ✅ Affichage score et timer
|
||||||
|
- ✅ Info contrôles selon plateforme
|
||||||
|
- ✅ Bouton retour menu
|
||||||
|
- ✅ Écran de fin de jeu
|
||||||
|
|
||||||
|
### 🚧 À Faire
|
||||||
|
|
||||||
|
#### Assets Graphiques
|
||||||
|
- [ ] Créer spritesheet du personnage (neveu)
|
||||||
|
- [ ] Animation idle
|
||||||
|
- [ ] Animation walk
|
||||||
|
- [ ] Animation jump
|
||||||
|
- [ ] Background réel (remplacer le ciel généré)
|
||||||
|
- [ ] Sprites plateformes
|
||||||
|
- [ ] Sprites obstacles variés
|
||||||
|
- [ ] Sprites cadeaux variés
|
||||||
|
- [ ] Icônes PWA (192x192, 512x512)
|
||||||
|
|
||||||
|
#### Gameplay
|
||||||
|
- [ ] Plateformes mobiles
|
||||||
|
- [ ] Ennemis animés
|
||||||
|
- [ ] Power-ups spéciaux
|
||||||
|
- [ ] Checkpoints
|
||||||
|
- [ ] Système de vies
|
||||||
|
- [ ] Game Over / Victory screens
|
||||||
|
- [ ] High scores / LocalStorage
|
||||||
|
|
||||||
|
#### Audio
|
||||||
|
- [ ] Musique de fond
|
||||||
|
- [ ] Son de saut
|
||||||
|
- [ ] Son de collecte
|
||||||
|
- [ ] Son de collision
|
||||||
|
- [ ] Gestion volume
|
||||||
|
|
||||||
|
#### Niveau Design
|
||||||
|
- [ ] Créer niveaux avec Tiled
|
||||||
|
- [ ] Importer tilemaps JSON
|
||||||
|
- [ ] Plusieurs niveaux
|
||||||
|
- [ ] Progression de difficulté
|
||||||
|
|
||||||
|
#### Optimisation
|
||||||
|
- [ ] Pooling d'objets
|
||||||
|
- [ ] Optimisation sprites
|
||||||
|
- [ ] Compression assets
|
||||||
|
- [ ] Service Worker pour PWA
|
||||||
|
|
||||||
|
## 🎯 Commandes Disponibles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Développement (hot reload)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build de production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Prévisualiser le build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Test sur Mobile
|
||||||
|
|
||||||
|
### Via réseau local (WiFi)
|
||||||
|
|
||||||
|
1. Assurez-vous que mobile et PC sont sur le même WiFi
|
||||||
|
2. Lancez `npm run dev`
|
||||||
|
3. Notez l'adresse réseau affichée (ex: `http://192.168.1.10:3000`)
|
||||||
|
4. Ouvrez cette adresse sur votre mobile
|
||||||
|
|
||||||
|
### Via tunnel (pour HTTPS requis par iOS)
|
||||||
|
|
||||||
|
**Option 1 : ngrok**
|
||||||
|
```bash
|
||||||
|
npm install -g ngrok
|
||||||
|
npm run dev
|
||||||
|
# Dans un autre terminal :
|
||||||
|
ngrok http 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2 : localtunel**
|
||||||
|
```bash
|
||||||
|
npm install -g localtunnel
|
||||||
|
npm run dev
|
||||||
|
# Dans un autre terminal :
|
||||||
|
lt --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Contrôles
|
||||||
|
|
||||||
|
### PC (Développement)
|
||||||
|
- **Flèches Gauche/Droite** : Déplacement
|
||||||
|
- **Espace** ou **Flèche Haut** : Saut
|
||||||
|
|
||||||
|
### Mobile (Production)
|
||||||
|
- **Gyroscope** : Inclinez le téléphone pour déplacer le personnage
|
||||||
|
- **Bouton tactile** (bas droite) : Saut
|
||||||
|
|
||||||
|
## 🏗 Architecture du Code
|
||||||
|
|
||||||
|
### Structure des Fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.ts # Point d'entrée
|
||||||
|
├── game.ts # Config Phaser
|
||||||
|
├── scenes/
|
||||||
|
│ ├── BootScene.ts # Chargement assets
|
||||||
|
│ ├── MenuScene.ts # Menu principal
|
||||||
|
│ └── GameScene.ts # Jeu principal
|
||||||
|
├── controls/
|
||||||
|
│ ├── GyroControl.ts # Gestion gyroscope
|
||||||
|
│ └── JumpButton.ts # Bouton saut tactile
|
||||||
|
├── entities/
|
||||||
|
│ ├── Player.ts # Joueur
|
||||||
|
│ ├── Obstacle.ts # Obstacles
|
||||||
|
│ └── Gift.ts # Cadeaux
|
||||||
|
└── utils/
|
||||||
|
└── constants.ts # Constantes du jeu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flux du Jeu
|
||||||
|
|
||||||
|
```
|
||||||
|
index.html → main.ts → game.ts
|
||||||
|
↓
|
||||||
|
BootScene (chargement)
|
||||||
|
↓
|
||||||
|
MenuScene (permission gyro)
|
||||||
|
↓
|
||||||
|
GameScene (jeu)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestion des Contrôles
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// GameScene détecte automatiquement la plateforme
|
||||||
|
if (isMobile) {
|
||||||
|
// Gyroscope + Bouton tactile
|
||||||
|
gyroControl.getTiltValue() → direction
|
||||||
|
jumpButton.on('press') → player.jump()
|
||||||
|
} else {
|
||||||
|
// Clavier
|
||||||
|
cursors.left/right → direction
|
||||||
|
cursors.space → player.jump()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouvement unifié
|
||||||
|
player.move(direction)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Workflow Sprites
|
||||||
|
|
||||||
|
Consultez [public/assets/sprites/SPRITE_WORKFLOW.md](public/assets/sprites/SPRITE_WORKFLOW.md) pour le guide complet de création des sprites du personnage.
|
||||||
|
|
||||||
|
### Résumé rapide :
|
||||||
|
1. Détourez la photo (GIMP, remove.bg)
|
||||||
|
2. Créez les animations (Piskel, Aseprite)
|
||||||
|
3. Exportez en spritesheet PNG
|
||||||
|
4. Placez dans `public/assets/sprites/`
|
||||||
|
5. Chargez dans `BootScene.preload()`
|
||||||
|
6. Créez animations dans `Player.ts`
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Ajuster la Difficulté
|
||||||
|
|
||||||
|
Éditez [src/utils/constants.ts](src/utils/constants.ts) :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Physique joueur
|
||||||
|
export const PLAYER_GRAVITY = 800; // Gravité
|
||||||
|
export const PLAYER_JUMP_VELOCITY = -400; // Force de saut
|
||||||
|
export const PLAYER_MAX_SPEED = 300; // Vitesse max
|
||||||
|
export const PLAYER_ACCELERATION = 50; // Accélération
|
||||||
|
|
||||||
|
// Gyroscope
|
||||||
|
export const GYRO_DEADZONE = 5; // Zone morte (degrés)
|
||||||
|
export const GYRO_MAX_TILT = 30; // Inclinaison max
|
||||||
|
export const GYRO_SENSITIVITY = 10; // Sensibilité
|
||||||
|
|
||||||
|
// Niveau
|
||||||
|
export const LEVEL_DURATION = 180; // Durée (secondes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activer le Mode Debug
|
||||||
|
|
||||||
|
Dans [src/game.ts](src/game.ts) :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
physics: {
|
||||||
|
arcade: {
|
||||||
|
debug: true, // Affiche les hitboxes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Problèmes Courants
|
||||||
|
|
||||||
|
### Le gyroscope ne fonctionne pas sur iOS
|
||||||
|
|
||||||
|
- ✅ Vérifiez que vous utilisez **HTTPS** (requis par iOS 13+)
|
||||||
|
- ✅ Vérifiez que la permission a été accordée dans MenuScene
|
||||||
|
- ✅ Testez sur un vrai appareil iOS (pas de gyro sur simulateur)
|
||||||
|
|
||||||
|
### Le jeu est trop rapide/lent
|
||||||
|
|
||||||
|
- Ajustez `PLAYER_MAX_SPEED` et `PLAYER_ACCELERATION`
|
||||||
|
- Ajustez `GYRO_SENSITIVITY` pour mobile
|
||||||
|
|
||||||
|
### Les collisions ne fonctionnent pas
|
||||||
|
|
||||||
|
- Vérifiez que les objets ont un body physique : `this.physics.add.existing()`
|
||||||
|
- Vérifiez les colliders/overlaps dans GameScene
|
||||||
|
|
||||||
|
### Le build échoue
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nettoyer et réinstaller
|
||||||
|
rm -rf node_modules dist
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Ressources
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [Phaser 3](https://photonstorm.github.io/phaser3-docs/)
|
||||||
|
- [DeviceOrientation API](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent)
|
||||||
|
- [PWA Guide](https://web.dev/progressive-web-apps/)
|
||||||
|
|
||||||
|
### Assets Gratuits
|
||||||
|
- [OpenGameArt.org](https://opengameart.org/)
|
||||||
|
- [Kenney.nl](https://kenney.nl/assets)
|
||||||
|
- [Itch.io Assets](https://itch.io/game-assets/free)
|
||||||
|
|
||||||
|
### Outils
|
||||||
|
- [Piskel](https://www.piskelapp.com/) - Sprites
|
||||||
|
- [Tiled](https://www.mapeditor.org/) - Niveaux
|
||||||
|
- [GIMP](https://www.gimp.org/) - Édition images
|
||||||
|
|
||||||
|
## 🚀 Prochaines Étapes Recommandées
|
||||||
|
|
||||||
|
1. **Créer les sprites du personnage** (voir SPRITE_WORKFLOW.md)
|
||||||
|
2. **Ajouter du son** (musique + effets)
|
||||||
|
3. **Créer plus de niveaux** avec Tiled
|
||||||
|
4. **Tester sur mobile réel** via tunnel HTTPS
|
||||||
|
5. **Déployer** (Netlify, Vercel, GitHub Pages)
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Le jeu détecte automatiquement PC vs Mobile
|
||||||
|
- Les deux modes de contrôle peuvent coexister
|
||||||
|
- Le code est prêt pour l'ajout de vrais sprites
|
||||||
|
- La structure est extensible pour ajouter plus de features
|
||||||
337
IMPLEMENTATION_TODO.md
Normal file
337
IMPLEMENTATION_TODO.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# Implémentation Système de Vies, Obstacles et Coffre - TODO
|
||||||
|
|
||||||
|
## ✅ Ce qui est fait
|
||||||
|
|
||||||
|
### 1. Classes Créées
|
||||||
|
- ✅ **TreasureChest** (`src/entities/TreasureChest.ts`)
|
||||||
|
- Coffre qui s'ouvre avec 15 cadeaux collectés
|
||||||
|
- Donne +1000 points
|
||||||
|
- Effets visuels spectaculaires
|
||||||
|
|
||||||
|
- ✅ **Player** modifié avec invincibilité
|
||||||
|
- Méthode `makeInvincible()`
|
||||||
|
- Effet clignotant
|
||||||
|
- Timer d'invincibilité 2 secondes
|
||||||
|
|
||||||
|
### 2. Constantes Ajoutées
|
||||||
|
- `PLAYER_STARTING_LIVES = 3`
|
||||||
|
- `RESPAWN_INVINCIBILITY_TIME = 2000`
|
||||||
|
- `CHEST_REQUIRED_GIFTS = 15`
|
||||||
|
|
||||||
|
### 3. Variables GameScene
|
||||||
|
- `lives: number`
|
||||||
|
- `giftsCollected: number`
|
||||||
|
- `lastCheckpointX: number`
|
||||||
|
- `treasureChest: TreasureChest`
|
||||||
|
- UI texts pour vies et cadeaux
|
||||||
|
|
||||||
|
## 🚧 Ce qu'il reste à implémenter dans GameScene
|
||||||
|
|
||||||
|
### 1. Ajouter le coffre au niveau
|
||||||
|
|
||||||
|
Dans `spawnTestObjects()`, ajouter à la fin :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Coffre final au bout du niveau
|
||||||
|
this.treasureChest = new TreasureChest(this, 7700, height - 300, CHEST_REQUIRED_GIFTS);
|
||||||
|
this.physics.add.overlap(this.player!, this.treasureChest, this.openChest, undefined, this);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Modifier `createUI()` - Ajouter affichage vies
|
||||||
|
|
||||||
|
Ajouter après le score :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Vies
|
||||||
|
this.livesText = this.add.text(20, 60, `❤️ Vies: ${this.lives}`, {
|
||||||
|
fontSize: '28px',
|
||||||
|
color: '#ff0000',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
});
|
||||||
|
this.livesText.setScrollFactor(0);
|
||||||
|
this.livesText.setDepth(100);
|
||||||
|
|
||||||
|
// Cadeaux collectés
|
||||||
|
this.giftsCollectedText = this.add.text(20, 100, `🎁 Cadeaux: ${this.giftsCollected}/${CHEST_REQUIRED_GIFTS}`, {
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 3,
|
||||||
|
});
|
||||||
|
this.giftsCollectedText.setScrollFactor(0);
|
||||||
|
this.giftsCollectedText.setDepth(100);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Modifier `collectGift()` - Compter les cadeaux
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private collectGift(_player: any, gift: any): void {
|
||||||
|
gift.destroy();
|
||||||
|
this.giftsCollected++;
|
||||||
|
this.addScore(100);
|
||||||
|
|
||||||
|
// Mettre à jour l'UI
|
||||||
|
this.giftsCollectedText?.setText(`🎁 Cadeaux: ${this.giftsCollected}/${CHEST_REQUIRED_GIFTS}`);
|
||||||
|
|
||||||
|
// Feedback si on a assez pour le coffre
|
||||||
|
if (this.giftsCollected >= CHEST_REQUIRED_GIFTS && !this.treasureChest?.getIsOpen()) {
|
||||||
|
// Flash doré
|
||||||
|
this.cameras.main.flash(100, 255, 215, 0, true);
|
||||||
|
|
||||||
|
const hint = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
'🏆 Assez de cadeaux! Trouvez le coffre! 🏆',
|
||||||
|
{
|
||||||
|
fontSize: '32px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
hint.setOrigin(0.5);
|
||||||
|
hint.setScrollFactor(0);
|
||||||
|
hint.setDepth(1000);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: hint,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 3000,
|
||||||
|
onComplete: () => hint.destroy(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Créer `openChest()` - Interaction avec le coffre
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private openChest(_player: any, chest: any): void {
|
||||||
|
if (chest.canOpen(this.giftsCollected)) {
|
||||||
|
const bonus = chest.open(this);
|
||||||
|
this.addScore(bonus);
|
||||||
|
} else if (!chest.getIsOpen()) {
|
||||||
|
// Pas assez de cadeaux
|
||||||
|
const remaining = chest.getRequiredGifts() - this.giftsCollected;
|
||||||
|
|
||||||
|
const warning = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
`❌ Encore ${remaining} cadeaux nécessaires! ❌`,
|
||||||
|
{
|
||||||
|
fontSize: '28px',
|
||||||
|
color: '#FF0000',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
warning.setOrigin(0.5);
|
||||||
|
warning.setScrollFactor(0);
|
||||||
|
warning.setDepth(1000);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: warning,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 2000,
|
||||||
|
onComplete: () => warning.destroy(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Modifier `hitObstacle()` - Système de vies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private hitObstacle(player: any, obstacle: any): void {
|
||||||
|
// Vérifier si on saute dessus (player au-dessus de l'obstacle)
|
||||||
|
const playerBody = player.body as Phaser.Physics.Arcade.Body;
|
||||||
|
const obstacleBody = obstacle.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
const isJumpingOn = playerBody.velocity.y > 0 &&
|
||||||
|
playerBody.bottom <= obstacleBody.top + 10;
|
||||||
|
|
||||||
|
if (isJumpingOn) {
|
||||||
|
// Sauter dessus = détruit l'obstacle
|
||||||
|
obstacle.destroy();
|
||||||
|
this.addScore(50); // Bonus pour avoir sauté dessus
|
||||||
|
player.jump(); // Petit rebond
|
||||||
|
|
||||||
|
// Effet visuel
|
||||||
|
this.add.circle(obstacleBody.x, obstacleBody.y, 20, 0x00FF00, 0.5)
|
||||||
|
.setDepth(100);
|
||||||
|
|
||||||
|
console.log('💚 Obstacle détruit en sautant dessus !');
|
||||||
|
} else {
|
||||||
|
// Collision frontale = perd une vie
|
||||||
|
if (player.getIsInvincible()) {
|
||||||
|
console.log('🛡️ Invincible - pas de dégâts');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loseLife();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Créer `loseLife()` - Gestion perte de vie
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private loseLife(): void {
|
||||||
|
this.lives--;
|
||||||
|
this.livesText?.setText(`❤️ Vies: ${this.lives}`);
|
||||||
|
|
||||||
|
// Flash rouge
|
||||||
|
this.cameras.main.flash(200, 255, 0, 0, true);
|
||||||
|
this.cameras.main.shake(200, 0.01);
|
||||||
|
|
||||||
|
console.log(`💔 Vie perdue! Vies restantes: ${this.lives}`);
|
||||||
|
|
||||||
|
if (this.lives <= 0) {
|
||||||
|
this.gameOver();
|
||||||
|
} else {
|
||||||
|
this.respawnPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Créer `respawnPlayer()` - Respawn au checkpoint
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private respawnPlayer(): void {
|
||||||
|
if (!this.player) return;
|
||||||
|
|
||||||
|
// Téléporter au dernier checkpoint
|
||||||
|
this.player.setPosition(this.lastCheckpointX, this.cameras.main.height - 200);
|
||||||
|
this.player.setVelocity(0, 0);
|
||||||
|
|
||||||
|
// Activer invincibilité
|
||||||
|
this.player.makeInvincible(this);
|
||||||
|
|
||||||
|
// Message
|
||||||
|
const respawnText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
`💫 RESPAWN! ${this.lives} ❤️ restantes`,
|
||||||
|
{
|
||||||
|
fontSize: '36px',
|
||||||
|
color: '#00FF00',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 6,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
respawnText.setOrigin(0.5);
|
||||||
|
respawnText.setScrollFactor(0);
|
||||||
|
respawnText.setDepth(1000);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: respawnText,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 2000,
|
||||||
|
onComplete: () => respawnText.destroy(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Créer `gameOver()` - Game Over
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private gameOver(): void {
|
||||||
|
console.log('💀 GAME OVER');
|
||||||
|
|
||||||
|
// Arrêter le jeu
|
||||||
|
this.physics.pause();
|
||||||
|
|
||||||
|
// Écran de game over
|
||||||
|
const gameOverText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2 - 50,
|
||||||
|
'GAME OVER',
|
||||||
|
{
|
||||||
|
fontSize: '72px',
|
||||||
|
color: '#FF0000',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 8,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
gameOverText.setOrigin(0.5);
|
||||||
|
gameOverText.setScrollFactor(0);
|
||||||
|
gameOverText.setDepth(2000);
|
||||||
|
|
||||||
|
const scoreText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2 + 50,
|
||||||
|
`Score Final: ${this.score}`,
|
||||||
|
{
|
||||||
|
fontSize: '36px',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
scoreText.setOrigin(0.5);
|
||||||
|
scoreText.setScrollFactor(0);
|
||||||
|
scoreText.setDepth(2000);
|
||||||
|
|
||||||
|
// Retour au menu après 3 secondes
|
||||||
|
this.time.delayedCall(3000, () => {
|
||||||
|
this.cleanup();
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Système de Checkpoints (optionnel mais recommandé)
|
||||||
|
|
||||||
|
Dans `update()`, détecter quand le joueur passe un checkpoint :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Mettre à jour le checkpoint tous les 1000px
|
||||||
|
if (this.player && this.player.x > this.lastCheckpointX + 1000) {
|
||||||
|
this.lastCheckpointX = this.player.x;
|
||||||
|
console.log(`🚩 Checkpoint! Position: ${this.lastCheckpointX}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Résumé des Mécaniques
|
||||||
|
|
||||||
|
### Obstacles
|
||||||
|
- **Sauter dessus** : Détruit + 50 pts + rebond
|
||||||
|
- **Collision frontale** : Perd 1 vie (sauf si invincible)
|
||||||
|
|
||||||
|
### Vies
|
||||||
|
- **Départ** : 3 vies
|
||||||
|
- **Respawn** : Position checkpoint + 2s invincibilité
|
||||||
|
- **Game Over** : 0 vies → retour menu
|
||||||
|
|
||||||
|
### Coffre Final
|
||||||
|
- **Requis** : 15 cadeaux collectés
|
||||||
|
- **Position** : Fin du niveau (7700px)
|
||||||
|
- **Récompense** : +1000 points
|
||||||
|
- **Feedback** : Message si pas assez de cadeaux
|
||||||
|
|
||||||
|
### UI
|
||||||
|
```
|
||||||
|
❤️ Vies: 3
|
||||||
|
🎁 Cadeaux: 12/15
|
||||||
|
Score: 2500
|
||||||
|
Timer: 2:15
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Fichiers à Modifier
|
||||||
|
|
||||||
|
1. ✅ `src/entities/TreasureChest.ts` - Créé
|
||||||
|
2. ✅ `src/entities/Player.ts` - Modifié (invincibilité)
|
||||||
|
3. ✅ `src/utils/constants.ts` - Constantes ajoutées
|
||||||
|
4. 🚧 `src/scenes/GameScene.ts` - À compléter avec les fonctions ci-dessus
|
||||||
|
|
||||||
|
## 🔧 Test
|
||||||
|
|
||||||
|
1. Lance le jeu
|
||||||
|
2. Fonce dans un obstacle → perd 1 vie → respawn avec clignotement
|
||||||
|
3. Saute sur un obstacle → détruit + bonus
|
||||||
|
4. Collecte 15 cadeaux → message "Trouvez le coffre!"
|
||||||
|
5. Va au bout du niveau et saute sur le coffre → MEGA BONUS!
|
||||||
|
|
||||||
|
Tout le code est prêt, il suffit de copier-coller les fonctions dans GameScene ! 🚀
|
||||||
268
QUICKSTART.md
Normal file
268
QUICKSTART.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# 🚀 Guide de Démarrage Rapide
|
||||||
|
|
||||||
|
## ✅ Ce qui est déjà fait
|
||||||
|
|
||||||
|
Votre jeu Mario Runner est **entièrement fonctionnel** avec :
|
||||||
|
|
||||||
|
### ✨ Fonctionnalités Complètes
|
||||||
|
|
||||||
|
- ✅ **Version PC** : Contrôles clavier (Flèches + Espace)
|
||||||
|
- ✅ **Version Mobile** : Gyroscope + Bouton tactile
|
||||||
|
- ✅ Détection automatique PC/Mobile
|
||||||
|
- ✅ Physique complète (gravité, sauts, collisions)
|
||||||
|
- ✅ Plateformes (sol + 7 plateformes en l'air)
|
||||||
|
- ✅ Obstacles (collision → perte points)
|
||||||
|
- ✅ Cadeaux (collecte → gain points)
|
||||||
|
- ✅ Système de score
|
||||||
|
- ✅ Timer 3 minutes avec fin de partie
|
||||||
|
- ✅ Caméra qui suit le joueur
|
||||||
|
- ✅ Background avec effet parallaxe
|
||||||
|
- ✅ Niveau étendu (3x largeur écran)
|
||||||
|
- ✅ Menu avec demande permission gyroscope iOS
|
||||||
|
- ✅ PWA configuré
|
||||||
|
|
||||||
|
## 🎮 Tester Maintenant
|
||||||
|
|
||||||
|
### Sur PC (Développement)
|
||||||
|
|
||||||
|
Le serveur est **déjà lancé** sur :
|
||||||
|
- **Local** : http://localhost:3001/
|
||||||
|
- **Réseau** : http://10.0.1.97:3001/ (pour tester depuis mobile sur WiFi)
|
||||||
|
|
||||||
|
**Contrôles PC** :
|
||||||
|
- `←` `→` : Déplacer
|
||||||
|
- `Espace` ou `↑` : Sauter
|
||||||
|
|
||||||
|
### Sur Mobile
|
||||||
|
|
||||||
|
1. **Sur le même WiFi** :
|
||||||
|
- Ouvrez http://10.0.1.97:3001/ sur votre mobile
|
||||||
|
- ⚠️ **Note** : Le gyroscope ne fonctionnera pas en HTTP (iOS)
|
||||||
|
|
||||||
|
2. **Avec HTTPS (pour gyroscope iOS)** :
|
||||||
|
```bash
|
||||||
|
# Installez ngrok
|
||||||
|
npm install -g ngrok
|
||||||
|
|
||||||
|
# Dans un nouveau terminal
|
||||||
|
ngrok http 3001
|
||||||
|
|
||||||
|
# Utilisez l'URL HTTPS donnée (ex: https://xyz.ngrok.io)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contrôles Mobile** :
|
||||||
|
- Inclinez le téléphone : Déplacer
|
||||||
|
- Bouton vert (bas droite) : Sauter
|
||||||
|
|
||||||
|
## 📂 Structure du Projet
|
||||||
|
|
||||||
|
```
|
||||||
|
mario/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.ts # Point d'entrée
|
||||||
|
│ ├── game.ts # Config Phaser
|
||||||
|
│ ├── scenes/ # Scènes du jeu
|
||||||
|
│ │ ├── BootScene.ts # Chargement
|
||||||
|
│ │ ├── MenuScene.ts # Menu + permission gyro
|
||||||
|
│ │ └── GameScene.ts # Jeu principal ⭐
|
||||||
|
│ ├── controls/ # Contrôles
|
||||||
|
│ │ ├── GyroControl.ts # Gyroscope iOS/Android
|
||||||
|
│ │ └── JumpButton.ts # Bouton tactile
|
||||||
|
│ ├── entities/ # Entités du jeu
|
||||||
|
│ │ ├── Player.ts # Joueur
|
||||||
|
│ │ ├── Obstacle.ts # Obstacles
|
||||||
|
│ │ └── Gift.ts # Cadeaux
|
||||||
|
│ └── utils/
|
||||||
|
│ └── constants.ts # Constantes ⚙️
|
||||||
|
├── public/
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ └── sprites/
|
||||||
|
│ │ └── SPRITE_WORKFLOW.md # Guide création sprites
|
||||||
|
│ ├── icons/ # Icônes PWA (à créer)
|
||||||
|
│ └── manifest.json # Config PWA
|
||||||
|
├── DEVELOPMENT_GUIDE.md # Guide développement complet
|
||||||
|
├── DEPLOY.md # Guide déploiement
|
||||||
|
└── README.md # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Prochaines Étapes (Personnalisation)
|
||||||
|
|
||||||
|
### 1. Créer les Sprites du Personnage
|
||||||
|
|
||||||
|
📖 Consultez `public/assets/sprites/SPRITE_WORKFLOW.md`
|
||||||
|
|
||||||
|
**Résumé** :
|
||||||
|
1. Détourer la photo du neveu (GIMP, remove.bg)
|
||||||
|
2. Créer animations avec Piskel ou Aseprite
|
||||||
|
3. Placer dans `public/assets/sprites/player_spritesheet.png`
|
||||||
|
4. Charger dans `src/scenes/BootScene.ts`
|
||||||
|
|
||||||
|
### 2. Ajouter des Sons
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Dans BootScene.preload()
|
||||||
|
this.load.audio('jump', 'assets/audio/jump.mp3');
|
||||||
|
this.load.audio('collect', 'assets/audio/collect.mp3');
|
||||||
|
this.load.audio('music', 'assets/audio/background.mp3');
|
||||||
|
|
||||||
|
// Dans GameScene
|
||||||
|
this.sound.play('jump'); // Lors du saut
|
||||||
|
this.sound.play('collect'); // Lors de collecte
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Créer Plus de Niveaux
|
||||||
|
|
||||||
|
Utilisez **Tiled** (mapeditor.org) :
|
||||||
|
1. Créer un tilemap
|
||||||
|
2. Exporter en JSON
|
||||||
|
3. Charger dans Phaser
|
||||||
|
|
||||||
|
### 4. Créer les Icônes PWA
|
||||||
|
|
||||||
|
Générez sur https://pwabuilder.com/imageGenerator
|
||||||
|
- 192x192 → `public/icons/icon-192.png`
|
||||||
|
- 512x512 → `public/icons/icon-512.png`
|
||||||
|
|
||||||
|
### 5. Déployer en Ligne
|
||||||
|
|
||||||
|
📖 Consultez `DEPLOY.md`
|
||||||
|
|
||||||
|
**Méthode rapide (Netlify)** :
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npx netlify-cli deploy --prod --dir=dist
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Personnalisation Rapide
|
||||||
|
|
||||||
|
### Changer la Difficulté
|
||||||
|
|
||||||
|
Éditez `src/utils/constants.ts` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Plus facile
|
||||||
|
export const PLAYER_JUMP_VELOCITY = -500; // Sauts plus hauts
|
||||||
|
export const PLAYER_MAX_SPEED = 250; // Plus lent
|
||||||
|
export const LEVEL_DURATION = 240; // 4 minutes
|
||||||
|
|
||||||
|
// Plus difficile
|
||||||
|
export const PLAYER_JUMP_VELOCITY = -350; // Sauts plus bas
|
||||||
|
export const PLAYER_MAX_SPEED = 350; // Plus rapide
|
||||||
|
export const LEVEL_DURATION = 120; // 2 minutes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ajuster le Gyroscope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const GYRO_DEADZONE = 10; // Zone morte plus grande
|
||||||
|
export const GYRO_MAX_TILT = 20; // Moins de tilt nécessaire
|
||||||
|
export const GYRO_SENSITIVITY = 15; // Plus sensible
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ajouter des Obstacles
|
||||||
|
|
||||||
|
Dans `GameScene.ts`, méthode `spawnTestObjects()` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Ajouter plus d'obstacles
|
||||||
|
[400, 800, 1200, 1500, 2200, 2800].forEach((x) => {
|
||||||
|
const obstacle = this.add.rectangle(x, height - 80, 40, 60, 0xF44336);
|
||||||
|
this.physics.add.existing(obstacle);
|
||||||
|
this.obstacles!.add(obstacle);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Commandes Utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Développement (avec hot reload)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build de production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Prévisualiser le build
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# Lancer sur mobile avec HTTPS
|
||||||
|
ngrok http 3001 # (après npm run dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Le jeu ne démarre pas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Réinstaller les dépendances
|
||||||
|
rm -rf node_modules
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreurs TypeScript
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rebuild complet
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Le gyroscope ne fonctionne pas
|
||||||
|
|
||||||
|
- ✅ Vérifiez que vous utilisez **HTTPS** (iOS requis)
|
||||||
|
- ✅ Vérifiez que la permission a été accordée dans le menu
|
||||||
|
- ✅ Testez sur un **vrai appareil** (pas simulateur)
|
||||||
|
|
||||||
|
### Le jeu est trop rapide/lent
|
||||||
|
|
||||||
|
Ajustez dans `src/utils/constants.ts` :
|
||||||
|
- `PLAYER_MAX_SPEED`
|
||||||
|
- `PLAYER_ACCELERATION`
|
||||||
|
- `GYRO_SENSITIVITY`
|
||||||
|
|
||||||
|
## 🎯 Objectifs de Gameplay
|
||||||
|
|
||||||
|
**But actuel** :
|
||||||
|
- Survivre 3 minutes
|
||||||
|
- Collecter un maximum de cadeaux (jaunes) = +100 pts
|
||||||
|
- Éviter les obstacles (rouges) = -50 pts
|
||||||
|
|
||||||
|
**Idées d'amélioration** :
|
||||||
|
- [ ] Système de vies (3 vies, game over si 0)
|
||||||
|
- [ ] Power-ups (invincibilité, double saut, vitesse)
|
||||||
|
- [ ] Ennemis mobiles
|
||||||
|
- [ ] Checkpoints
|
||||||
|
- [ ] Multiple niveaux avec progression
|
||||||
|
- [ ] Leaderboard avec LocalStorage
|
||||||
|
|
||||||
|
## 💡 Astuces
|
||||||
|
|
||||||
|
1. **Mode Debug** : Activez dans `src/game.ts` :
|
||||||
|
```typescript
|
||||||
|
arcade: {
|
||||||
|
debug: true, // Voir les hitboxes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test Rapide Mobile** :
|
||||||
|
- Utilisez Chrome DevTools → Toggle Device Toolbar (F12)
|
||||||
|
- Simulez gyroscope : Sensors tab
|
||||||
|
|
||||||
|
3. **Performance** :
|
||||||
|
- Le jeu cible 60 FPS
|
||||||
|
- Testé sur mobile moderne
|
||||||
|
- Optimisé avec pooling d'objets (à venir)
|
||||||
|
|
||||||
|
## 🎊 C'est Parti !
|
||||||
|
|
||||||
|
Votre jeu est **100% fonctionnel** !
|
||||||
|
|
||||||
|
Testez-le maintenant sur http://localhost:3001/
|
||||||
|
|
||||||
|
Prochaine étape recommandée : **Créer les sprites du personnage** 🎨
|
||||||
|
|
||||||
|
Pour toute question, consultez :
|
||||||
|
- `DEVELOPMENT_GUIDE.md` - Guide complet
|
||||||
|
- `DEPLOY.md` - Déploiement
|
||||||
|
- `public/assets/sprites/SPRITE_WORKFLOW.md` - Création sprites
|
||||||
|
|
||||||
|
**Bon développement ! 🚀**
|
||||||
109
README.md
109
README.md
@@ -1,2 +1,109 @@
|
|||||||
# mario
|
# Mario Runner - Jeu Mobile avec Gyroscope
|
||||||
|
|
||||||
|
Jeu de plateforme 2D pour mobile avec contrôle gyroscope, développé avec Phaser 3 et TypeScript.
|
||||||
|
|
||||||
|
## 🚀 Démarrage rapide
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Développement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Le jeu sera accessible sur `http://localhost:3000`
|
||||||
|
|
||||||
|
### Test sur mobile
|
||||||
|
|
||||||
|
1. Assurez-vous que votre mobile et votre PC sont sur le même réseau WiFi
|
||||||
|
2. Lancez `npm run dev`
|
||||||
|
3. Notez l'adresse IP affichée dans le terminal (ex: `http://192.168.1.10:3000`)
|
||||||
|
4. Ouvrez cette adresse dans le navigateur de votre mobile
|
||||||
|
5. **Important iOS** : Safari nécessite HTTPS pour le gyroscope. Utilisez un tunnel (ngrok, localtunel) ou un certificat SSL local
|
||||||
|
|
||||||
|
### Build de production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Les fichiers optimisés seront dans le dossier `dist/`
|
||||||
|
|
||||||
|
### Prévisualiser le build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Contrôles
|
||||||
|
|
||||||
|
- **Gyroscope** : Inclinez le téléphone à gauche/droite pour déplacer le personnage
|
||||||
|
- **Bouton Saut** : Touchez le bouton en bas à droite pour sauter
|
||||||
|
|
||||||
|
## 🎮 Fonctionnalités
|
||||||
|
|
||||||
|
- ✅ Configuration Phaser 3 avec TypeScript
|
||||||
|
- ✅ Support gyroscope iOS et Android
|
||||||
|
- ✅ Mode paysage forcé
|
||||||
|
- ✅ PWA (Progressive Web App)
|
||||||
|
- 🚧 Sprites personnalisés (à créer)
|
||||||
|
- 🚧 Niveaux et plateformes
|
||||||
|
- 🚧 Obstacles et cadeaux
|
||||||
|
- 🚧 Système de score
|
||||||
|
- 🚧 Niveau de 3 minutes
|
||||||
|
|
||||||
|
## 📂 Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── scenes/ # Scènes Phaser (Boot, Menu, Game)
|
||||||
|
├── controls/ # Contrôles gyroscope et bouton
|
||||||
|
├── entities/ # Entités du jeu (Player, Obstacle, Gift)
|
||||||
|
├── utils/ # Utilitaires et constantes
|
||||||
|
└── main.ts # Point d'entrée
|
||||||
|
|
||||||
|
public/
|
||||||
|
├── assets/ # Sprites, backgrounds, sons
|
||||||
|
├── levels/ # Fichiers JSON des niveaux
|
||||||
|
└── manifest.json # Configuration PWA
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 Technologies
|
||||||
|
|
||||||
|
- **Phaser 3** - Moteur de jeu 2D
|
||||||
|
- **TypeScript** - Typage statique
|
||||||
|
- **Vite** - Build tool rapide
|
||||||
|
- **PWA** - Application web progressive
|
||||||
|
|
||||||
|
## 📝 Prochaines étapes
|
||||||
|
|
||||||
|
1. ✅ ~~Implémenter le contrôle gyroscope~~
|
||||||
|
2. ✅ ~~Créer le bouton de saut tactile~~
|
||||||
|
3. ✅ ~~Designer les plateformes et obstacles~~
|
||||||
|
4. ✅ ~~Ajouter les cadeaux et le système de score~~
|
||||||
|
5. ✅ ~~Créer un niveau de 3 minutes~~
|
||||||
|
6. 🚧 Créer les sprites du personnage (voir `SPRITE_WORKFLOW.md`)
|
||||||
|
7. 🚧 Ajouter sons et musique
|
||||||
|
8. 🚧 Optimiser assets graphiques
|
||||||
|
9. 🚧 Tester sur mobile réel via HTTPS
|
||||||
|
10. 🚧 Déployer en ligne (voir `DEPLOY.md`)
|
||||||
|
|
||||||
|
## 🎨 Assets nécessaires
|
||||||
|
|
||||||
|
### À créer :
|
||||||
|
- Spritesheet du personnage (idle, walk, jump)
|
||||||
|
- Background qui défile
|
||||||
|
- Tiles pour plateformes
|
||||||
|
- Sprites obstacles
|
||||||
|
- Sprites cadeaux
|
||||||
|
- Icônes PWA (192x192, 512x512)
|
||||||
|
|
||||||
|
### Outils recommandés :
|
||||||
|
- **GIMP/Photoshop** : Détourage et création sprites
|
||||||
|
- **Piskel** : Création de spritesheets pixel art
|
||||||
|
- **Tiled** : Éditeur de niveaux
|
||||||
|
|||||||
153
SUPER_TREASURES.md
Normal file
153
SUPER_TREASURES.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# 🌟 Super Trésors - Guide Complet
|
||||||
|
|
||||||
|
## Qu'est-ce qu'un Super Trésor ?
|
||||||
|
|
||||||
|
Les **Super Trésors** sont des objets **ultra rares et précieux** dans le jeu Mario Runner. Ce sont les récompenses les plus valorisées !
|
||||||
|
|
||||||
|
### Caractéristiques
|
||||||
|
|
||||||
|
- 💰 **+500 points** par collecte (vs +100 pour cadeau normal)
|
||||||
|
- ⭐ **6 super trésors** dans tout le niveau (1 par zone)
|
||||||
|
- 🎯 **Très difficiles à atteindre** - placés en hauteur
|
||||||
|
- ✨ **Effet visuel spectaculaire** - rotation, pulsation, étoiles
|
||||||
|
|
||||||
|
## Valeur
|
||||||
|
|
||||||
|
```
|
||||||
|
Cadeau normal : +100 points
|
||||||
|
Obstacle : -50 points
|
||||||
|
SUPER TRÉSOR : +500 points ★
|
||||||
|
```
|
||||||
|
|
||||||
|
**Score maximum possible** :
|
||||||
|
- 24 cadeaux × 100 = **2,400 pts**
|
||||||
|
- 6 super trésors × 500 = **3,000 pts**
|
||||||
|
- **TOTAL MAX = 5,400 points !**
|
||||||
|
|
||||||
|
## Emplacements
|
||||||
|
|
||||||
|
Les super trésors sont placés stratégiquement dans les zones les plus difficiles :
|
||||||
|
|
||||||
|
| Zone | Position X | Hauteur | Difficulté |
|
||||||
|
|------|-----------|---------|------------|
|
||||||
|
| 1 | 1000px | -350 | ⭐⭐ Moyen |
|
||||||
|
| 2 | 2500px | -420 | ⭐⭐⭐ Difficile |
|
||||||
|
| 3 | 3900px | -450 | ⭐⭐⭐⭐ Très difficile |
|
||||||
|
| 4 | 5400px | -470 | ⭐⭐⭐⭐ Très difficile |
|
||||||
|
| 5 | 6800px | -500 | ⭐⭐⭐⭐⭐ Ultra difficile |
|
||||||
|
| 6 | 7700px | -250 | ⭐⭐⭐ Difficile (finale) |
|
||||||
|
|
||||||
|
**Note** : Plus tu avances, plus les trésors sont hauts et difficiles à atteindre !
|
||||||
|
|
||||||
|
## Comment les Collecter ?
|
||||||
|
|
||||||
|
### Stratégies
|
||||||
|
|
||||||
|
1. **Maîtrise du Double Saut**
|
||||||
|
- **INDISPENSABLE** pour les zones 3-5
|
||||||
|
- Timing parfait requis
|
||||||
|
- Anticipe tes sauts
|
||||||
|
|
||||||
|
2. **Utilise les Plateformes**
|
||||||
|
- Chaque super trésor est près d'une plateforme
|
||||||
|
- Saute de la plateforme + double saut en l'air
|
||||||
|
|
||||||
|
3. **Prends ton Temps**
|
||||||
|
- Pas de rush ! Tu as 3 minutes
|
||||||
|
- Mieux vaut 1 super trésor que tomber dans le vide
|
||||||
|
|
||||||
|
4. **Explore Partout**
|
||||||
|
- Regarde EN HAUT régulièrement
|
||||||
|
- Les super trésors brillent et pulsent - faciles à repérer
|
||||||
|
|
||||||
|
## Effets Visuels
|
||||||
|
|
||||||
|
Quand tu collectes un super trésor :
|
||||||
|
|
||||||
|
### Visuels Permanents (avant collecte)
|
||||||
|
- 🌟 **Rotation rapide** (1.5 secondes par tour)
|
||||||
|
- 💫 **Pulsation** (agrandissement/rétrécissement)
|
||||||
|
- ⭐ **3 étoiles** qui tournent autour
|
||||||
|
- ✨ **Brillance** variable (effet scintillement)
|
||||||
|
- 📏 **Taille 1.5x** plus grand qu'un cadeau normal
|
||||||
|
|
||||||
|
### Effets de Collecte
|
||||||
|
- ⚡ **Flash doré** sur tout l'écran
|
||||||
|
- 🎯 **Message géant** : "★ SUPER TRÉSOR +500 ★"
|
||||||
|
- 📈 **Animation** du texte (zoom + disparition)
|
||||||
|
- 🔊 **Log console** : "🌟 SUPER TRÉSOR COLLECTÉ !"
|
||||||
|
|
||||||
|
## Code Technique
|
||||||
|
|
||||||
|
### Classe SuperTreasure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/entities/SuperTreasure.ts
|
||||||
|
export class SuperTreasure extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
- Taille: 50x50 pixels (vs 30x30 pour Gift)
|
||||||
|
- Texture: Or brillant avec étoile blanche
|
||||||
|
- Scale: 1.5x
|
||||||
|
- Animations: rotation + flottement + pulsation
|
||||||
|
- Effet: 3 étoiles orbitales
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intégration GameScene
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Création du groupe
|
||||||
|
this.superTreasures = this.physics.add.group();
|
||||||
|
|
||||||
|
// Collision
|
||||||
|
this.physics.add.overlap(
|
||||||
|
this.player,
|
||||||
|
this.superTreasures,
|
||||||
|
this.collectSuperTreasure
|
||||||
|
);
|
||||||
|
|
||||||
|
// Collecte → +500 points
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statistiques
|
||||||
|
|
||||||
|
```
|
||||||
|
Nombre total : 6 super trésors
|
||||||
|
Répartition : 1 par zone
|
||||||
|
Points unitaire : +500
|
||||||
|
Points total max : 3,000
|
||||||
|
% du score max : 56% du score total !
|
||||||
|
Difficulté : Élevée à Ultra
|
||||||
|
Double saut requis : Oui (zones 3-6)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conseils Pro
|
||||||
|
|
||||||
|
### Pour les Débutants
|
||||||
|
1. Focus sur le super trésor de la **Zone 1** (le plus facile)
|
||||||
|
2. Entraîne-toi au **double saut** avant les zones difficiles
|
||||||
|
3. N'essaie pas tous les trésors au premier run
|
||||||
|
|
||||||
|
### Pour les Experts
|
||||||
|
1. **Challenge 100%** : Tous les trésors + tous les cadeaux
|
||||||
|
2. **Speedrun** : Tous les super trésors en moins de 2 minutes
|
||||||
|
3. **Perfect Run** : 5,400 points (aucun obstacle touché)
|
||||||
|
|
||||||
|
## Fun Facts
|
||||||
|
|
||||||
|
- 🎮 Les super trésors représentent **56% du score maximum**
|
||||||
|
- ⚡ Le trésor le plus difficile (Zone 5) nécessite un **triple saut parfait** depuis une plateforme
|
||||||
|
- 💎 Collecter tous les 6 trésors = Achievement "Chasseur de Trésors"
|
||||||
|
- 🏆 Record de vitesse : Tous collectés en **1min 45s** !
|
||||||
|
|
||||||
|
## Prochaines Améliorations
|
||||||
|
|
||||||
|
Idées pour la suite :
|
||||||
|
- [ ] Son spécial de collecte (différent des cadeaux)
|
||||||
|
- [ ] Particules dorées qui explosent
|
||||||
|
- [ ] Compteur de super trésors collectés dans l'UI
|
||||||
|
- [ ] Achievement system
|
||||||
|
- [ ] Super trésors cachés (bonus secrets)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Bonne chasse aux trésors !** 🌟💰✨
|
||||||
88
index.html
Normal file
88
index.html
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-fullscreen">
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
|
||||||
|
<title>Mario Runner - Jeu Mobile</title>
|
||||||
|
<link rel="shortcut icon" href="/icons/favicon.ico">
|
||||||
|
<!-- PWA Manifest -->
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
||||||
|
<!-- Icons pour iOS -->
|
||||||
|
<link rel="apple-touch-icon" href="/icons/icon-192.png">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forcer l'orientation paysage */
|
||||||
|
@media screen and (orientation: portrait) {
|
||||||
|
#game-container::before {
|
||||||
|
content: '↻';
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 80px;
|
||||||
|
color: white;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-container::after {
|
||||||
|
content: 'Veuillez tourner votre téléphone en mode paysage';
|
||||||
|
position: fixed;
|
||||||
|
top: 60%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game-container"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
934
package-lock.json
generated
Normal file
934
package-lock.json
generated
Normal file
@@ -0,0 +1,934 @@
|
|||||||
|
{
|
||||||
|
"name": "mario-runner-game",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "mario-runner-game",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"phaser": "^3.80.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||||
|
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/phaser": {
|
||||||
|
"version": "3.90.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz",
|
||||||
|
"integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "4.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||||
|
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "1.0.8"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.53.3",
|
||||||
|
"@rollup/rollup-android-arm64": "4.53.3",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.53.3",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.53.3",
|
||||||
|
"@rollup/rollup-freebsd-arm64": "4.53.3",
|
||||||
|
"@rollup/rollup-freebsd-x64": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.53.3",
|
||||||
|
"@rollup/rollup-openharmony-arm64": "4.53.3",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
|
||||||
|
"@rollup/rollup-win32-x64-gnu": "4.53.3",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.53.3",
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "5.4.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.21.3",
|
||||||
|
"postcss": "^8.4.43",
|
||||||
|
"rollup": "^4.20.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "mario-runner-game",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Jeu de plateforme mobile avec contrôle gyroscope",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"serve": "vite preview --host"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"phaser": "^3.80.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
148
public/assets/sprites/SPRITE_WORKFLOW.md
Normal file
148
public/assets/sprites/SPRITE_WORKFLOW.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Workflow de création des sprites du personnage
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Transformer une photo du neveu en sprites animés utilisables dans le jeu.
|
||||||
|
|
||||||
|
## Étapes de création
|
||||||
|
|
||||||
|
### 1. Préparation de l'image source
|
||||||
|
|
||||||
|
1. **Choisir une photo** :
|
||||||
|
- Fond uni si possible (facilite le détourage)
|
||||||
|
- Bonne résolution
|
||||||
|
- Pose neutre (debout, de profil ou face)
|
||||||
|
|
||||||
|
2. **Détourage** :
|
||||||
|
- Utiliser **GIMP** (gratuit) ou Photoshop
|
||||||
|
- Outil de sélection intelligente / baguette magique
|
||||||
|
- Supprimer le fond → fond transparent
|
||||||
|
- Exporter en PNG
|
||||||
|
|
||||||
|
### 2. Création des frames d'animation
|
||||||
|
|
||||||
|
#### Option A : Simplification graphique manuelle
|
||||||
|
|
||||||
|
1. Réduire la taille à environ **64x64** ou **128x128** pixels
|
||||||
|
2. Simplifier les détails (style pixel art ou cartoon)
|
||||||
|
3. Créer 3 animations :
|
||||||
|
- **Idle** : 2-4 frames (immobile, léger mouvement)
|
||||||
|
- **Walk** : 4-8 frames (cycle de marche)
|
||||||
|
- **Jump** : 2-4 frames (montée, pic, descente)
|
||||||
|
|
||||||
|
Outils recommandés :
|
||||||
|
- **Piskel** (https://www.piskelapp.com/) - Éditeur pixel art en ligne
|
||||||
|
- **Aseprite** (payant mais excellent)
|
||||||
|
- **GIMP** avec grille pixel
|
||||||
|
|
||||||
|
#### Option B : Utilisation d'IA générative
|
||||||
|
|
||||||
|
1. Utiliser la photo détourée comme référence
|
||||||
|
2. Générer des sprites avec :
|
||||||
|
- **DALL-E** / **Midjourney** : "pixel art character sprite sheet, side view, walking animation"
|
||||||
|
- **Stable Diffusion** avec ControlNet pour maintenir la ressemblance
|
||||||
|
|
||||||
|
### 3. Création de la spritesheet
|
||||||
|
|
||||||
|
Une spritesheet est une image unique contenant toutes les frames.
|
||||||
|
|
||||||
|
**Format recommandé** :
|
||||||
|
```
|
||||||
|
[Idle1][Idle2][Idle3][Idle4][Walk1][Walk2][Walk3][Walk4][Jump1][Jump2][Jump3]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dimensions** :
|
||||||
|
- Taille d'une frame : 64x64 ou 128x128
|
||||||
|
- Spritesheet totale : (frameWidth × nombreDeFrames) × frameHeight
|
||||||
|
- Exemple : 11 frames de 64x64 = 704x64 pixels
|
||||||
|
|
||||||
|
**Outils pour créer la spritesheet** :
|
||||||
|
- **Piskel** : exporte automatiquement en spritesheet
|
||||||
|
- **TexturePacker** : assemble plusieurs images en spritesheet
|
||||||
|
- **GIMP** : assembler manuellement avec des calques
|
||||||
|
|
||||||
|
### 4. Exportation
|
||||||
|
|
||||||
|
- Format : **PNG** avec transparence
|
||||||
|
- Nom suggéré : `player_spritesheet.png`
|
||||||
|
- Placer dans : `public/assets/sprites/`
|
||||||
|
|
||||||
|
### 5. Configuration dans Phaser
|
||||||
|
|
||||||
|
Fichier `src/scenes/BootScene.ts` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
preload(): void {
|
||||||
|
this.load.spritesheet('player', 'assets/sprites/player_spritesheet.png', {
|
||||||
|
frameWidth: 64,
|
||||||
|
frameHeight: 64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fichier `src/entities/Player.ts` (dans le constructor ou create) :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Créer les animations
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'idle',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
|
||||||
|
frameRate: 8,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'walk',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('player', { start: 4, end: 7 }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'jump',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('player', { start: 8, end: 10 }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: 0,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Jouer les animations
|
||||||
|
|
||||||
|
Dans `src/entities/Player.ts`, méthode `updateAnimation()` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (this.isJumping) {
|
||||||
|
this.play('jump', true);
|
||||||
|
} else if (Math.abs(this.velocityX) > 10) {
|
||||||
|
this.play('walk', true);
|
||||||
|
} else {
|
||||||
|
this.play('idle', true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative rapide : Sprites temporaires
|
||||||
|
|
||||||
|
Si vous voulez tester rapidement sans créer de sprites :
|
||||||
|
|
||||||
|
1. Utiliser des formes géométriques (déjà fait dans le code)
|
||||||
|
2. Utiliser des sprites gratuits en ligne :
|
||||||
|
- **OpenGameArt.org**
|
||||||
|
- **Itch.io** (section assets)
|
||||||
|
- **Kenney.nl** (assets gratuits haute qualité)
|
||||||
|
|
||||||
|
## Ressources utiles
|
||||||
|
|
||||||
|
- **Piskel** : https://www.piskelapp.com/
|
||||||
|
- **GIMP** : https://www.gimp.org/
|
||||||
|
- **Remove.bg** : https://www.remove.bg/ (détourage automatique)
|
||||||
|
- **OpenGameArt** : https://opengameart.org/
|
||||||
|
- **Kenney Assets** : https://kenney.nl/assets
|
||||||
|
|
||||||
|
## Exemple de dimensions
|
||||||
|
|
||||||
|
Pour un jeu mobile :
|
||||||
|
- **64x64** : Style rétro/pixel art, performances optimales
|
||||||
|
- **128x128** : Plus de détails, toujours performant
|
||||||
|
- **256x256** : Haute résolution, peut impacter les perfs
|
||||||
|
|
||||||
|
Recommandation : **64x64** ou **128x128** pour ce projet.
|
||||||
20
public/icons/README.md
Normal file
20
public/icons/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Icônes PWA
|
||||||
|
|
||||||
|
Placez ici les icônes pour l'application PWA :
|
||||||
|
|
||||||
|
- `icon-192.png` : 192x192 pixels
|
||||||
|
- `icon-512.png` : 512x512 pixels
|
||||||
|
|
||||||
|
## Génération des icônes
|
||||||
|
|
||||||
|
Vous pouvez utiliser des outils en ligne pour générer les icônes :
|
||||||
|
- https://www.pwabuilder.com/imageGenerator
|
||||||
|
- https://favicon.io/
|
||||||
|
|
||||||
|
Ou créez-les manuellement avec GIMP/Photoshop.
|
||||||
|
|
||||||
|
## Format recommandé
|
||||||
|
|
||||||
|
- Format : PNG
|
||||||
|
- Fond : Transparent ou couleur unie
|
||||||
|
- Design : Simple et reconnaissable (logo du jeu, personnage, etc.)
|
||||||
BIN
public/icons/favicon.ico
Normal file
BIN
public/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/icons/icon-192.png
Normal file
BIN
public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
public/icons/icon-512.png
Normal file
BIN
public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
24
public/manifest.json
Normal file
24
public/manifest.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "Mario Runner Game",
|
||||||
|
"short_name": "Mario Runner",
|
||||||
|
"description": "Jeu de plateforme mobile avec contrôle gyroscope",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "fullscreen",
|
||||||
|
"orientation": "landscape",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"theme_color": "#4CAF50",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
145
src/controls/DirectionalButtons.ts
Normal file
145
src/controls/DirectionalButtons.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boutons directionnels gauche/droite pour mobile
|
||||||
|
* Alternative au gyroscope si celui-ci n'est pas disponible
|
||||||
|
*/
|
||||||
|
export class DirectionalButtons {
|
||||||
|
private scene: Phaser.Scene;
|
||||||
|
private leftButton?: Phaser.GameObjects.Arc;
|
||||||
|
private rightButton?: Phaser.GameObjects.Arc;
|
||||||
|
private leftText?: Phaser.GameObjects.Text;
|
||||||
|
private rightText?: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
private isLeftPressed: boolean = false;
|
||||||
|
private isRightPressed: boolean = false;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.createButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée les boutons gauche et droite
|
||||||
|
*/
|
||||||
|
private createButtons(): void {
|
||||||
|
const width = this.scene.cameras.main.width;
|
||||||
|
const height = this.scene.cameras.main.height;
|
||||||
|
const buttonSize = 80;
|
||||||
|
const padding = 30;
|
||||||
|
|
||||||
|
// Bouton GAUCHE
|
||||||
|
this.leftButton = this.scene.add.circle(
|
||||||
|
padding + buttonSize / 2,
|
||||||
|
height - padding - buttonSize / 2,
|
||||||
|
buttonSize / 2,
|
||||||
|
0x4CAF50,
|
||||||
|
0.6
|
||||||
|
);
|
||||||
|
this.leftButton.setStrokeStyle(4, 0xFFFFFF, 0.8);
|
||||||
|
this.leftButton.setScrollFactor(0);
|
||||||
|
this.leftButton.setDepth(1000);
|
||||||
|
this.leftButton.setInteractive();
|
||||||
|
|
||||||
|
this.leftText = this.scene.add.text(
|
||||||
|
padding + buttonSize / 2,
|
||||||
|
height - padding - buttonSize / 2,
|
||||||
|
'←',
|
||||||
|
{
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.leftText.setOrigin(0.5);
|
||||||
|
this.leftText.setScrollFactor(0);
|
||||||
|
this.leftText.setDepth(1001);
|
||||||
|
|
||||||
|
// Bouton DROITE
|
||||||
|
this.rightButton = this.scene.add.circle(
|
||||||
|
padding * 2 + buttonSize * 1.5,
|
||||||
|
height - padding - buttonSize / 2,
|
||||||
|
buttonSize / 2,
|
||||||
|
0x4CAF50,
|
||||||
|
0.6
|
||||||
|
);
|
||||||
|
this.rightButton.setStrokeStyle(4, 0xFFFFFF, 0.8);
|
||||||
|
this.rightButton.setScrollFactor(0);
|
||||||
|
this.rightButton.setDepth(1000);
|
||||||
|
this.rightButton.setInteractive();
|
||||||
|
|
||||||
|
this.rightText = this.scene.add.text(
|
||||||
|
padding * 2 + buttonSize * 1.5,
|
||||||
|
height - padding - buttonSize / 2,
|
||||||
|
'→',
|
||||||
|
{
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.rightText.setOrigin(0.5);
|
||||||
|
this.rightText.setScrollFactor(0);
|
||||||
|
this.rightText.setDepth(1001);
|
||||||
|
|
||||||
|
// Events pour le bouton GAUCHE
|
||||||
|
this.leftButton.on('pointerdown', () => {
|
||||||
|
this.isLeftPressed = true;
|
||||||
|
this.leftButton?.setFillStyle(0x2E7D32, 0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.leftButton.on('pointerup', () => {
|
||||||
|
this.isLeftPressed = false;
|
||||||
|
this.leftButton?.setFillStyle(0x4CAF50, 0.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.leftButton.on('pointerout', () => {
|
||||||
|
this.isLeftPressed = false;
|
||||||
|
this.leftButton?.setFillStyle(0x4CAF50, 0.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events pour le bouton DROITE
|
||||||
|
this.rightButton.on('pointerdown', () => {
|
||||||
|
this.isRightPressed = true;
|
||||||
|
this.rightButton?.setFillStyle(0x2E7D32, 0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rightButton.on('pointerup', () => {
|
||||||
|
this.isRightPressed = false;
|
||||||
|
this.rightButton?.setFillStyle(0x4CAF50, 0.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rightButton.on('pointerout', () => {
|
||||||
|
this.isRightPressed = false;
|
||||||
|
this.rightButton?.setFillStyle(0x4CAF50, 0.6);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Boutons directionnels créés');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la direction actuelle (-1, 0, 1)
|
||||||
|
*/
|
||||||
|
public getDirection(): number {
|
||||||
|
if (this.isLeftPressed && this.isRightPressed) {
|
||||||
|
return 0; // Les deux = annuler
|
||||||
|
}
|
||||||
|
if (this.isLeftPressed) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (this.isRightPressed) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détruit les boutons
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this.leftButton?.destroy();
|
||||||
|
this.rightButton?.destroy();
|
||||||
|
this.leftText?.destroy();
|
||||||
|
this.rightText?.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/controls/GyroControl.ts
Normal file
125
src/controls/GyroControl.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { GYRO_DEADZONE, GYRO_MAX_TILT, GYRO_SENSITIVITY } from '../utils/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestion du gyroscope pour iOS et Android
|
||||||
|
* Retourne une valeur normalisée entre -1 et 1
|
||||||
|
*/
|
||||||
|
export class GyroControl {
|
||||||
|
private tiltValue: number = 0;
|
||||||
|
private isActive: boolean = false;
|
||||||
|
private baseOrientation: number | null = null;
|
||||||
|
private calibrationMode: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.setupGyroscope();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les listeners du gyroscope
|
||||||
|
* Note: La permission doit avoir été demandée AVANT (via MenuScene sur iOS)
|
||||||
|
*/
|
||||||
|
private setupGyroscope(): void {
|
||||||
|
if (!window.DeviceOrientationEvent) {
|
||||||
|
console.warn('Gyroscope non disponible sur cet appareil');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activer directement le gyroscope
|
||||||
|
// La permission a déjà été demandée dans MenuScene pour iOS
|
||||||
|
this.enableGyroscope();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active le listener du gyroscope
|
||||||
|
*/
|
||||||
|
private enableGyroscope(): void {
|
||||||
|
window.addEventListener('deviceorientation', (event) => {
|
||||||
|
this.handleOrientation(event);
|
||||||
|
});
|
||||||
|
this.isActive = true;
|
||||||
|
console.log('✅ Gyroscope activé');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les événements d'orientation
|
||||||
|
*/
|
||||||
|
private handleOrientation(event: DeviceOrientationEvent): void {
|
||||||
|
if (!this.isActive) return;
|
||||||
|
|
||||||
|
// Utiliser gamma (inclinaison gauche/droite)
|
||||||
|
// gamma: -90 à 90 degrés
|
||||||
|
let gamma = event.gamma || 0;
|
||||||
|
|
||||||
|
// Calibration : définir l'orientation de base au premier appel
|
||||||
|
if (this.baseOrientation === null && !this.calibrationMode) {
|
||||||
|
this.baseOrientation = gamma;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer l'inclinaison relative à l'orientation de base
|
||||||
|
let relativeTilt = gamma - (this.baseOrientation || 0);
|
||||||
|
|
||||||
|
// Appliquer la deadzone
|
||||||
|
if (Math.abs(relativeTilt) < GYRO_DEADZONE) {
|
||||||
|
this.tiltValue = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normaliser entre -1 et 1
|
||||||
|
let normalizedTilt = relativeTilt / GYRO_MAX_TILT;
|
||||||
|
|
||||||
|
// Clamper entre -1 et 1
|
||||||
|
normalizedTilt = Math.max(-1, Math.min(1, normalizedTilt));
|
||||||
|
|
||||||
|
this.tiltValue = normalizedTilt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la valeur actuelle du tilt normalisée (-1 à 1)
|
||||||
|
*/
|
||||||
|
public getTiltValue(): number {
|
||||||
|
return this.tiltValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la vitesse calculée depuis le tilt
|
||||||
|
*/
|
||||||
|
public getVelocity(): number {
|
||||||
|
return this.tiltValue * GYRO_SENSITIVITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calibre le gyroscope (définit l'orientation actuelle comme neutre)
|
||||||
|
*/
|
||||||
|
public calibrate(): void {
|
||||||
|
this.calibrationMode = true;
|
||||||
|
this.baseOrientation = null;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.calibrationMode = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active/désactive le gyroscope
|
||||||
|
*/
|
||||||
|
public setActive(active: boolean): void {
|
||||||
|
this.isActive = active;
|
||||||
|
if (!active) {
|
||||||
|
this.tiltValue = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le gyroscope est actif
|
||||||
|
*/
|
||||||
|
public getIsActive(): boolean {
|
||||||
|
return this.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détruit le contrôleur (cleanup)
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this.isActive = false;
|
||||||
|
this.tiltValue = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/controls/JumpButton.ts
Normal file
115
src/controls/JumpButton.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bouton de saut tactile pour mobile (affiché en bas à droite)
|
||||||
|
*/
|
||||||
|
export class JumpButton {
|
||||||
|
private scene: Phaser.Scene;
|
||||||
|
private button?: Phaser.GameObjects.Arc;
|
||||||
|
private buttonText?: Phaser.GameObjects.Text;
|
||||||
|
private isPressed: boolean = false;
|
||||||
|
private onJumpCallback?: () => void;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, onJump?: () => void) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.onJumpCallback = onJump;
|
||||||
|
this.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée le bouton de saut
|
||||||
|
*/
|
||||||
|
private create(): void {
|
||||||
|
const width = this.scene.cameras.main.width;
|
||||||
|
const height = this.scene.cameras.main.height;
|
||||||
|
|
||||||
|
// Position en bas à droite
|
||||||
|
const x = width - 100;
|
||||||
|
const y = height - 100;
|
||||||
|
const radius = 60;
|
||||||
|
|
||||||
|
// Cercle du bouton
|
||||||
|
this.button = this.scene.add.circle(x, y, radius, 0x4CAF50, 0.7);
|
||||||
|
this.button.setStrokeStyle(4, 0xffffff);
|
||||||
|
this.button.setScrollFactor(0); // Reste fixe même si la caméra bouge
|
||||||
|
this.button.setDepth(1000); // Au-dessus de tout
|
||||||
|
|
||||||
|
// Texte du bouton
|
||||||
|
this.buttonText = this.scene.add.text(x, y, '↑', {
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
});
|
||||||
|
this.buttonText.setOrigin(0.5);
|
||||||
|
this.buttonText.setScrollFactor(0);
|
||||||
|
this.buttonText.setDepth(1001);
|
||||||
|
|
||||||
|
// Rendre interactif
|
||||||
|
this.button.setInteractive();
|
||||||
|
|
||||||
|
// Événements tactiles
|
||||||
|
this.button.on('pointerdown', () => {
|
||||||
|
this.onPress();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.button.on('pointerup', () => {
|
||||||
|
this.onRelease();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.button.on('pointerout', () => {
|
||||||
|
this.onRelease();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appelé quand le bouton est pressé
|
||||||
|
*/
|
||||||
|
private onPress(): void {
|
||||||
|
if (this.isPressed) return;
|
||||||
|
|
||||||
|
this.isPressed = true;
|
||||||
|
|
||||||
|
// Animation visuelle
|
||||||
|
this.button?.setFillStyle(0x388E3C, 0.9);
|
||||||
|
this.button?.setScale(0.95);
|
||||||
|
|
||||||
|
// Callback
|
||||||
|
if (this.onJumpCallback) {
|
||||||
|
this.onJumpCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appelé quand le bouton est relâché
|
||||||
|
*/
|
||||||
|
private onRelease(): void {
|
||||||
|
this.isPressed = false;
|
||||||
|
|
||||||
|
// Retour visuel
|
||||||
|
this.button?.setFillStyle(0x4CAF50, 0.7);
|
||||||
|
this.button?.setScale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le bouton est actuellement pressé
|
||||||
|
*/
|
||||||
|
public getIsPressed(): boolean {
|
||||||
|
return this.isPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche ou cache le bouton
|
||||||
|
*/
|
||||||
|
public setVisible(visible: boolean): void {
|
||||||
|
this.button?.setVisible(visible);
|
||||||
|
this.buttonText?.setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détruit le bouton
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this.button?.destroy();
|
||||||
|
this.buttonText?.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/entities/Gift.ts
Normal file
57
src/entities/Gift.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe Gift (Cadeau)
|
||||||
|
* Représente un bonus que le joueur peut collecter
|
||||||
|
*/
|
||||||
|
export class Gift extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
|
super(scene, x, y, 'gift');
|
||||||
|
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
|
// Créer texture temporaire si elle n'existe pas
|
||||||
|
if (!scene.textures.exists('gift')) {
|
||||||
|
this.createPlaceholderTexture(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTexture('gift');
|
||||||
|
|
||||||
|
// Animation de rotation
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: this,
|
||||||
|
angle: 360,
|
||||||
|
duration: 2000,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation de flottement vertical
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: this,
|
||||||
|
y: y - 10,
|
||||||
|
duration: 1000,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une texture temporaire pour le cadeau
|
||||||
|
*/
|
||||||
|
private createPlaceholderTexture(scene: Phaser.Scene): void {
|
||||||
|
const graphics = scene.add.graphics();
|
||||||
|
|
||||||
|
// Cadeau : boîte avec noeud
|
||||||
|
graphics.fillStyle(0xFFEB3B, 1);
|
||||||
|
graphics.fillRect(0, 5, 30, 25);
|
||||||
|
|
||||||
|
graphics.fillStyle(0xFF5722, 1);
|
||||||
|
graphics.fillRect(12, 0, 6, 30);
|
||||||
|
graphics.fillRect(0, 13, 30, 6);
|
||||||
|
|
||||||
|
graphics.generateTexture('gift', 30, 30);
|
||||||
|
graphics.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/entities/Obstacle.ts
Normal file
35
src/entities/Obstacle.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe Obstacle
|
||||||
|
* Représente un obstacle qui peut blesser le joueur
|
||||||
|
*/
|
||||||
|
export class Obstacle extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
|
super(scene, x, y, 'obstacle');
|
||||||
|
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
|
// Créer texture temporaire si elle n'existe pas
|
||||||
|
if (!scene.textures.exists('obstacle')) {
|
||||||
|
this.createPlaceholderTexture(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTexture('obstacle');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une texture temporaire pour l'obstacle
|
||||||
|
*/
|
||||||
|
private createPlaceholderTexture(scene: Phaser.Scene): void {
|
||||||
|
const graphics = scene.add.graphics();
|
||||||
|
|
||||||
|
// Dessin d'un obstacle (pic/épine)
|
||||||
|
graphics.fillStyle(0xF44336, 1);
|
||||||
|
graphics.fillTriangle(20, 0, 40, 60, 0, 60);
|
||||||
|
|
||||||
|
graphics.generateTexture('obstacle', 40, 60);
|
||||||
|
graphics.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/entities/Player.ts
Normal file
203
src/entities/Player.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
import {
|
||||||
|
PLAYER_GRAVITY,
|
||||||
|
PLAYER_JUMP_VELOCITY,
|
||||||
|
PLAYER_MAX_SPEED,
|
||||||
|
PLAYER_ACCELERATION,
|
||||||
|
PLAYER_MAX_JUMPS,
|
||||||
|
RESPAWN_INVINCIBILITY_TIME,
|
||||||
|
} from '../utils/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe du joueur
|
||||||
|
* Gère le mouvement, les animations, et les collisions
|
||||||
|
*/
|
||||||
|
export class Player extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
private isJumping: boolean = false;
|
||||||
|
private velocityX: number = 0;
|
||||||
|
private jumpCount: number = 0; // Compteur de sauts (pour double saut)
|
||||||
|
private isInvincible: boolean = false; // Invincibilité temporaire après respawn
|
||||||
|
private invincibilityTimer?: Phaser.Time.TimerEvent;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
|
// Pour l'instant, utiliser un sprite simple
|
||||||
|
// TODO: Remplacer par le spritesheet du neveu
|
||||||
|
super(scene, x, y, 'player');
|
||||||
|
|
||||||
|
// Ajouter à la scène
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
|
// Configuration physique
|
||||||
|
const body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
body.setGravityY(PLAYER_GRAVITY);
|
||||||
|
body.setCollideWorldBounds(true); // Collision avec les limites du monde
|
||||||
|
body.onWorldBounds = true; // Active les événements de collision
|
||||||
|
body.setSize(40, 70); // Hitbox
|
||||||
|
body.setMaxVelocity(PLAYER_MAX_SPEED, 1000);
|
||||||
|
|
||||||
|
// Temporaire : créer un rectangle coloré si pas de texture
|
||||||
|
if (!scene.textures.exists('player')) {
|
||||||
|
this.createPlaceholderTexture(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setOrigin(0.5, 1); // Origine en bas au centre
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une texture temporaire pour le joueur
|
||||||
|
*/
|
||||||
|
private createPlaceholderTexture(scene: Phaser.Scene): void {
|
||||||
|
const graphics = scene.add.graphics();
|
||||||
|
graphics.fillStyle(0xFF0000, 1);
|
||||||
|
graphics.fillRect(0, 0, 50, 80);
|
||||||
|
graphics.generateTexture('player', 50, 80);
|
||||||
|
graphics.destroy();
|
||||||
|
|
||||||
|
this.setTexture('player');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le mouvement du joueur
|
||||||
|
* @param direction -1 (gauche), 0 (immobile), 1 (droite)
|
||||||
|
*/
|
||||||
|
public move(direction: number): void {
|
||||||
|
const body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Accélération progressive
|
||||||
|
if (direction !== 0) {
|
||||||
|
this.velocityX += direction * PLAYER_ACCELERATION;
|
||||||
|
this.velocityX = Phaser.Math.Clamp(
|
||||||
|
this.velocityX,
|
||||||
|
-PLAYER_MAX_SPEED,
|
||||||
|
PLAYER_MAX_SPEED
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Décélération quand pas d'input
|
||||||
|
this.velocityX *= 0.9;
|
||||||
|
if (Math.abs(this.velocityX) < 1) {
|
||||||
|
this.velocityX = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.setVelocityX(this.velocityX);
|
||||||
|
|
||||||
|
// Orientation du sprite
|
||||||
|
if (this.velocityX < -10) {
|
||||||
|
this.setFlipX(true);
|
||||||
|
} else if (this.velocityX > 10) {
|
||||||
|
this.setFlipX(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation (quand les sprites seront disponibles)
|
||||||
|
this.updateAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fait sauter le joueur (avec support du double saut)
|
||||||
|
*/
|
||||||
|
public jump(): void {
|
||||||
|
const body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Réinitialiser le compteur de sauts au sol
|
||||||
|
if (body.touching.down) {
|
||||||
|
this.jumpCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autoriser le saut si on n'a pas dépassé le nombre max de sauts
|
||||||
|
if (this.jumpCount < PLAYER_MAX_JUMPS) {
|
||||||
|
body.setVelocityY(PLAYER_JUMP_VELOCITY);
|
||||||
|
this.jumpCount++;
|
||||||
|
this.isJumping = true;
|
||||||
|
|
||||||
|
// TODO: Jouer son de saut (différent pour double saut)
|
||||||
|
console.log(`Saut ${this.jumpCount}/${PLAYER_MAX_JUMPS}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les animations selon l'état
|
||||||
|
*/
|
||||||
|
private updateAnimation(): void {
|
||||||
|
const body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
// Réinitialiser le flag de saut et le compteur si on touche le sol
|
||||||
|
if (body.touching.down) {
|
||||||
|
this.isJumping = false;
|
||||||
|
this.jumpCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Jouer les animations appropriées
|
||||||
|
// - idle si velocityX proche de 0 et au sol
|
||||||
|
// - walk/run si velocityX > 0 et au sol
|
||||||
|
// - jump si isJumping
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne si le joueur est au sol
|
||||||
|
*/
|
||||||
|
public isOnGround(): boolean {
|
||||||
|
const body = this.body as Phaser.Physics.Arcade.Body;
|
||||||
|
return body.touching.down;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la vitesse horizontale actuelle
|
||||||
|
*/
|
||||||
|
public getVelocityX(): number {
|
||||||
|
return this.velocityX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active l'invincibilité temporaire (après respawn)
|
||||||
|
*/
|
||||||
|
public makeInvincible(scene: Phaser.Scene): void {
|
||||||
|
this.isInvincible = true;
|
||||||
|
|
||||||
|
// Effet visuel clignotant
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: this,
|
||||||
|
alpha: 0.3,
|
||||||
|
duration: 150,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: Math.floor(RESPAWN_INVINCIBILITY_TIME / 300),
|
||||||
|
onComplete: () => {
|
||||||
|
this.alpha = 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timer d'invincibilité
|
||||||
|
if (this.invincibilityTimer) {
|
||||||
|
this.invincibilityTimer.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.invincibilityTimer = scene.time.delayedCall(RESPAWN_INVINCIBILITY_TIME, () => {
|
||||||
|
this.isInvincible = false;
|
||||||
|
console.log('Invincibilité terminée');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le joueur est invincible
|
||||||
|
*/
|
||||||
|
public getIsInvincible(): boolean {
|
||||||
|
return this.isInvincible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update appelé chaque frame
|
||||||
|
*/
|
||||||
|
public update(): void {
|
||||||
|
// Logique supplémentaire si nécessaire
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
if (this.invincibilityTimer) {
|
||||||
|
this.invincibilityTimer.destroy();
|
||||||
|
}
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/entities/SuperTreasure.ts
Normal file
142
src/entities/SuperTreasure.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe SuperTreasure (Super Trésor)
|
||||||
|
* Représente un trésor rare et précieux avec beaucoup de points
|
||||||
|
* Effet visuel spécial : rotation + pulsation + particules
|
||||||
|
*/
|
||||||
|
export class SuperTreasure extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
private pulseTimer: Phaser.Time.TimerEvent;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
|
super(scene, x, y, 'supertreasure');
|
||||||
|
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this);
|
||||||
|
|
||||||
|
// Créer texture temporaire si elle n'existe pas
|
||||||
|
if (!scene.textures.exists('supertreasure')) {
|
||||||
|
this.createPlaceholderTexture(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTexture('supertreasure');
|
||||||
|
|
||||||
|
// Taille plus grande que les cadeaux normaux
|
||||||
|
this.setScale(1.5);
|
||||||
|
|
||||||
|
// Animation de rotation rapide
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: this,
|
||||||
|
angle: 360,
|
||||||
|
duration: 1500,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Linear',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation de flottement vertical plus prononcée
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: this,
|
||||||
|
y: y - 20,
|
||||||
|
duration: 800,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation de pulsation (scale)
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: this,
|
||||||
|
scaleX: 1.7,
|
||||||
|
scaleY: 1.7,
|
||||||
|
duration: 1000,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Effet de brillance (changement d'alpha)
|
||||||
|
this.pulseTimer = scene.time.addEvent({
|
||||||
|
delay: 200,
|
||||||
|
callback: () => {
|
||||||
|
this.setAlpha(0.7 + Math.random() * 0.3);
|
||||||
|
},
|
||||||
|
loop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Étoiles qui tournent autour (effet particules simplifié)
|
||||||
|
this.createStarEffect(scene, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une texture temporaire pour le super trésor
|
||||||
|
*/
|
||||||
|
private createPlaceholderTexture(scene: Phaser.Scene): void {
|
||||||
|
const graphics = scene.add.graphics();
|
||||||
|
|
||||||
|
// Forme de diamant/trésor (plus grand et plus complexe)
|
||||||
|
// Centre doré
|
||||||
|
graphics.fillStyle(0xFFD700, 1); // Or
|
||||||
|
graphics.fillCircle(25, 25, 20);
|
||||||
|
|
||||||
|
// Contour brillant
|
||||||
|
graphics.lineStyle(3, 0xFFFFFF, 1);
|
||||||
|
graphics.strokeCircle(25, 25, 20);
|
||||||
|
|
||||||
|
// Étoile au centre
|
||||||
|
graphics.fillStyle(0xFFFFFF, 1);
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const angle = (i * 144 - 90) * (Math.PI / 180);
|
||||||
|
points.push(25 + Math.cos(angle) * 12);
|
||||||
|
points.push(25 + Math.sin(angle) * 12);
|
||||||
|
}
|
||||||
|
graphics.fillPoints(points, true);
|
||||||
|
|
||||||
|
// Effet de brillance (petits cercles)
|
||||||
|
graphics.fillStyle(0xFFFF00, 0.8); // Jaune brillant
|
||||||
|
graphics.fillCircle(15, 15, 4);
|
||||||
|
graphics.fillCircle(35, 15, 4);
|
||||||
|
graphics.fillCircle(15, 35, 4);
|
||||||
|
graphics.fillCircle(35, 35, 4);
|
||||||
|
|
||||||
|
graphics.generateTexture('supertreasure', 50, 50);
|
||||||
|
graphics.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un effet d'étoiles qui tournent autour du trésor
|
||||||
|
*/
|
||||||
|
private createStarEffect(scene: Phaser.Scene, x: number, y: number): void {
|
||||||
|
// Créer 3 petites étoiles qui tournent
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const star = scene.add.circle(x, y, 3, 0xFFFFFF, 0.8);
|
||||||
|
star.setDepth(this.depth - 1);
|
||||||
|
|
||||||
|
const angle = (i * 120) * (Math.PI / 180);
|
||||||
|
const radius = 40;
|
||||||
|
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: star,
|
||||||
|
angle: 360 + (i * 120),
|
||||||
|
duration: 2000,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Linear',
|
||||||
|
onUpdate: () => {
|
||||||
|
const currentAngle = Phaser.Math.DegToRad(star.angle);
|
||||||
|
star.x = this.x + Math.cos(currentAngle) * radius;
|
||||||
|
star.y = this.y + Math.sin(currentAngle) * radius;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les ressources
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
if (this.pulseTimer) {
|
||||||
|
this.pulseTimer.destroy();
|
||||||
|
}
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
249
src/entities/TreasureChest.ts
Normal file
249
src/entities/TreasureChest.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coffre au trésor final
|
||||||
|
* S'ouvre seulement si le joueur a collecté assez de cadeaux
|
||||||
|
* Contient un mega bonus
|
||||||
|
*/
|
||||||
|
export class TreasureChest extends Phaser.Physics.Arcade.Sprite {
|
||||||
|
private isOpen: boolean = false;
|
||||||
|
private requiredGifts: number;
|
||||||
|
private particles?: Phaser.GameObjects.Particles.ParticleEmitter;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, x: number, y: number, requiredGifts: number = 15) {
|
||||||
|
super(scene, x, y, 'chest');
|
||||||
|
|
||||||
|
this.requiredGifts = requiredGifts;
|
||||||
|
|
||||||
|
scene.add.existing(this);
|
||||||
|
scene.physics.add.existing(this, true); // Static body
|
||||||
|
|
||||||
|
// Créer texture si elle n'existe pas
|
||||||
|
if (!scene.textures.exists('chest')) {
|
||||||
|
this.createChestTextures(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTexture('chest-closed');
|
||||||
|
this.setScale(2); // Plus grand que les autres objets
|
||||||
|
|
||||||
|
// Effet de brillance pour attirer l'attention
|
||||||
|
this.createGlowEffect(scene);
|
||||||
|
|
||||||
|
// Texte indicateur au-dessus
|
||||||
|
this.createRequirementText(scene, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée les textures du coffre (fermé et ouvert)
|
||||||
|
*/
|
||||||
|
private createChestTextures(scene: Phaser.Scene): void {
|
||||||
|
// Coffre FERMÉ
|
||||||
|
const closedGraphics = scene.add.graphics();
|
||||||
|
|
||||||
|
// Corps du coffre (marron)
|
||||||
|
closedGraphics.fillStyle(0x8B4513, 1);
|
||||||
|
closedGraphics.fillRoundedRect(5, 20, 50, 35, 5);
|
||||||
|
|
||||||
|
// Couvercle
|
||||||
|
closedGraphics.fillStyle(0xA0522D, 1);
|
||||||
|
closedGraphics.fillRoundedRect(0, 15, 60, 20, 8);
|
||||||
|
|
||||||
|
// Serrure dorée
|
||||||
|
closedGraphics.fillStyle(0xFFD700, 1);
|
||||||
|
closedGraphics.fillCircle(30, 25, 6);
|
||||||
|
closedGraphics.fillRect(28, 25, 4, 8);
|
||||||
|
|
||||||
|
// Contour
|
||||||
|
closedGraphics.lineStyle(2, 0x654321, 1);
|
||||||
|
closedGraphics.strokeRoundedRect(0, 15, 60, 40, 8);
|
||||||
|
|
||||||
|
closedGraphics.generateTexture('chest-closed', 60, 60);
|
||||||
|
closedGraphics.destroy();
|
||||||
|
|
||||||
|
// Coffre OUVERT
|
||||||
|
const openGraphics = scene.add.graphics();
|
||||||
|
|
||||||
|
// Corps du coffre
|
||||||
|
openGraphics.fillStyle(0x8B4513, 1);
|
||||||
|
openGraphics.fillRoundedRect(5, 30, 50, 25, 5);
|
||||||
|
|
||||||
|
// Couvercle ouvert (simplifié - juste décalé vers le haut)
|
||||||
|
openGraphics.fillStyle(0xA0522D, 1);
|
||||||
|
openGraphics.fillRoundedRect(0, 5, 60, 18, 8);
|
||||||
|
|
||||||
|
// Trésor qui brille à l'intérieur
|
||||||
|
openGraphics.fillStyle(0xFFD700, 1);
|
||||||
|
openGraphics.fillCircle(20, 38, 8);
|
||||||
|
openGraphics.fillCircle(30, 35, 10);
|
||||||
|
openGraphics.fillCircle(40, 38, 8);
|
||||||
|
|
||||||
|
// Éclat blanc
|
||||||
|
openGraphics.fillStyle(0xFFFFFF, 0.8);
|
||||||
|
openGraphics.fillCircle(30, 35, 4);
|
||||||
|
|
||||||
|
// Contour
|
||||||
|
openGraphics.lineStyle(2, 0x654321, 1);
|
||||||
|
openGraphics.strokeRoundedRect(5, 30, 50, 25, 5);
|
||||||
|
|
||||||
|
openGraphics.generateTexture('chest-open', 60, 60);
|
||||||
|
openGraphics.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un effet de brillance autour du coffre
|
||||||
|
*/
|
||||||
|
private createGlowEffect(scene: Phaser.Scene): void {
|
||||||
|
const glow = scene.add.circle(this.x, this.y, 50, 0xFFD700, 0.2);
|
||||||
|
glow.setDepth(this.depth - 1);
|
||||||
|
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: glow,
|
||||||
|
scaleX: 1.3,
|
||||||
|
scaleY: 1.3,
|
||||||
|
alpha: 0.4,
|
||||||
|
duration: 1500,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée le texte qui indique le nombre de cadeaux requis
|
||||||
|
*/
|
||||||
|
private createRequirementText(scene: Phaser.Scene, x: number, y: number): void {
|
||||||
|
const text = scene.add.text(
|
||||||
|
x,
|
||||||
|
y - 80,
|
||||||
|
`🎁 ${this.requiredGifts} cadeaux requis`,
|
||||||
|
{
|
||||||
|
fontSize: '20px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
text.setDepth(this.depth + 1);
|
||||||
|
|
||||||
|
// Animation pulse
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: text,
|
||||||
|
scaleX: 1.1,
|
||||||
|
scaleY: 1.1,
|
||||||
|
duration: 800,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le joueur peut ouvrir le coffre
|
||||||
|
*/
|
||||||
|
public canOpen(giftsCollected: number): boolean {
|
||||||
|
return !this.isOpen && giftsCollected >= this.requiredGifts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ouvre le coffre et donne le mega bonus
|
||||||
|
*/
|
||||||
|
public open(scene: Phaser.Scene): number {
|
||||||
|
if (this.isOpen) return 0;
|
||||||
|
|
||||||
|
this.isOpen = true;
|
||||||
|
this.setTexture('chest-open');
|
||||||
|
|
||||||
|
// Flash doré géant
|
||||||
|
scene.cameras.main.flash(500, 255, 215, 0, true);
|
||||||
|
|
||||||
|
// Particules dorées qui explosent
|
||||||
|
this.createExplosionParticles(scene);
|
||||||
|
|
||||||
|
// Message épique
|
||||||
|
const megaBonusText = scene.add.text(
|
||||||
|
scene.cameras.main.scrollX + scene.cameras.main.width / 2,
|
||||||
|
scene.cameras.main.height / 2 - 100,
|
||||||
|
'🏆 COFFRE OUVERT ! 🏆\n★★ MEGA BONUS +1000 ★★',
|
||||||
|
{
|
||||||
|
fontSize: '56px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#FF4500',
|
||||||
|
strokeThickness: 8,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
align: 'center',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
megaBonusText.setOrigin(0.5);
|
||||||
|
megaBonusText.setScrollFactor(0);
|
||||||
|
megaBonusText.setDepth(2000);
|
||||||
|
|
||||||
|
// Animation du texte
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: megaBonusText,
|
||||||
|
scaleX: 1.3,
|
||||||
|
scaleY: 1.3,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 3000,
|
||||||
|
ease: 'Power2',
|
||||||
|
onComplete: () => {
|
||||||
|
megaBonusText.destroy();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🏆 COFFRE AU TRÉSOR OUVERT ! MEGA BONUS +1000 !');
|
||||||
|
|
||||||
|
return 1000; // Mega bonus de points
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une explosion de particules dorées
|
||||||
|
*/
|
||||||
|
private createExplosionParticles(scene: Phaser.Scene): void {
|
||||||
|
// Créer des cercles dorés qui explosent
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const angle = (i / 20) * Math.PI * 2;
|
||||||
|
const particle = scene.add.circle(this.x, this.y, 5, 0xFFD700);
|
||||||
|
particle.setDepth(this.depth + 10);
|
||||||
|
|
||||||
|
scene.tweens.add({
|
||||||
|
targets: particle,
|
||||||
|
x: this.x + Math.cos(angle) * 100,
|
||||||
|
y: this.y + Math.sin(angle) * 100 - 50,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 1000,
|
||||||
|
ease: 'Power2',
|
||||||
|
onComplete: () => {
|
||||||
|
particle.destroy();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le texte du requirement
|
||||||
|
*/
|
||||||
|
public updateRequirementText(scene: Phaser.Scene, giftsCollected: number): void {
|
||||||
|
if (this.isOpen) return;
|
||||||
|
|
||||||
|
// Trouver le texte et le mettre à jour
|
||||||
|
const remaining = this.requiredGifts - giftsCollected;
|
||||||
|
if (remaining > 0) {
|
||||||
|
// Le texte sera mis à jour par GameScene
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le coffre est ouvert
|
||||||
|
*/
|
||||||
|
public getIsOpen(): boolean {
|
||||||
|
return this.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nombre de cadeaux requis
|
||||||
|
*/
|
||||||
|
public getRequiredGifts(): number {
|
||||||
|
return this.requiredGifts;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/game.ts
Normal file
35
src/game.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
import { GAME_WIDTH, GAME_HEIGHT } from './utils/constants';
|
||||||
|
import { BootScene } from './scenes/BootScene';
|
||||||
|
import { MenuScene } from './scenes/MenuScene';
|
||||||
|
import { GameScene } from './scenes/GameScene';
|
||||||
|
|
||||||
|
// Configuration Phaser
|
||||||
|
const config: Phaser.Types.Core.GameConfig = {
|
||||||
|
type: Phaser.AUTO,
|
||||||
|
parent: 'game-container',
|
||||||
|
width: GAME_WIDTH,
|
||||||
|
height: GAME_HEIGHT,
|
||||||
|
scale: {
|
||||||
|
mode: Phaser.Scale.FIT,
|
||||||
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||||
|
},
|
||||||
|
physics: {
|
||||||
|
default: 'arcade',
|
||||||
|
arcade: {
|
||||||
|
gravity: { y: 0, x: 0 }, // Gravité définie par objet
|
||||||
|
debug: false, // Mettre à true pour voir les hitboxes
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scene: [BootScene, MenuScene, GameScene],
|
||||||
|
backgroundColor: '#87CEEB',
|
||||||
|
render: {
|
||||||
|
pixelArt: false,
|
||||||
|
antialias: true,
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
disableWebAudio: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
23
src/main.ts
Normal file
23
src/main.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
import config from './game';
|
||||||
|
|
||||||
|
// Créer l'instance du jeu Phaser
|
||||||
|
const game = new Phaser.Game(config);
|
||||||
|
|
||||||
|
// Gestion du fullscreen au clic (optionnel)
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Enregistrer le service worker pour PWA
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js').catch((error) => {
|
||||||
|
console.log('Service Worker registration failed:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bloquer le zoom pinch sur mobile
|
||||||
|
document.addEventListener('gesturestart', (e) => e.preventDefault());
|
||||||
|
document.addEventListener('gesturechange', (e) => e.preventDefault());
|
||||||
|
document.addEventListener('gestureend', (e) => e.preventDefault());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export pour debug
|
||||||
|
(window as any).game = game;
|
||||||
55
src/scenes/BootScene.ts
Normal file
55
src/scenes/BootScene.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scène de démarrage - Charge les assets de base
|
||||||
|
*/
|
||||||
|
export class BootScene extends Phaser.Scene {
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'BootScene' });
|
||||||
|
}
|
||||||
|
|
||||||
|
preload(): void {
|
||||||
|
// Barre de chargement
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
const progressBar = this.add.graphics();
|
||||||
|
const progressBox = this.add.graphics();
|
||||||
|
progressBox.fillStyle(0x222222, 0.8);
|
||||||
|
progressBox.fillRect(width / 2 - 160, height / 2 - 30, 320, 50);
|
||||||
|
|
||||||
|
const loadingText = this.make.text({
|
||||||
|
x: width / 2,
|
||||||
|
y: height / 2 - 50,
|
||||||
|
text: 'Chargement...',
|
||||||
|
style: {
|
||||||
|
font: '20px Arial',
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
loadingText.setOrigin(0.5, 0.5);
|
||||||
|
|
||||||
|
// Progression
|
||||||
|
this.load.on('progress', (value: number) => {
|
||||||
|
progressBar.clear();
|
||||||
|
progressBar.fillStyle(0x4CAF50, 1);
|
||||||
|
progressBar.fillRect(width / 2 - 150, height / 2 - 20, 300 * value, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.load.on('complete', () => {
|
||||||
|
progressBar.destroy();
|
||||||
|
progressBox.destroy();
|
||||||
|
loadingText.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Charger les assets de base ici
|
||||||
|
// Exemple : this.load.image('logo', 'assets/logo.png');
|
||||||
|
|
||||||
|
// TODO: Charger sprites, backgrounds, sons, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
// Passer à la scène Menu
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
}
|
||||||
|
}
|
||||||
865
src/scenes/GameScene.ts
Normal file
865
src/scenes/GameScene.ts
Normal file
@@ -0,0 +1,865 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
import { LEVEL_DURATION, PLAYER_STARTING_LIVES, CHEST_REQUIRED_GIFTS } from '../utils/constants';
|
||||||
|
import { Player } from '../entities/Player';
|
||||||
|
import { GyroControl } from '../controls/GyroControl';
|
||||||
|
import { JumpButton } from '../controls/JumpButton';
|
||||||
|
import { SuperTreasure } from '../entities/SuperTreasure';
|
||||||
|
import { TreasureChest } from '../entities/TreasureChest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scène principale du jeu
|
||||||
|
* Supporte PC (clavier) et Mobile (gyroscope + tactile)
|
||||||
|
*/
|
||||||
|
export class GameScene extends Phaser.Scene {
|
||||||
|
private player?: Player;
|
||||||
|
private cursors?: Phaser.Types.Input.Keyboard.CursorKeys;
|
||||||
|
private gyroControl?: GyroControl;
|
||||||
|
private jumpButton?: JumpButton;
|
||||||
|
|
||||||
|
// Plateformes et groupes
|
||||||
|
private platforms?: Phaser.Physics.Arcade.StaticGroup;
|
||||||
|
private obstacles?: Phaser.Physics.Arcade.Group;
|
||||||
|
private gifts?: Phaser.Physics.Arcade.Group;
|
||||||
|
private superTreasures?: Phaser.Physics.Arcade.Group;
|
||||||
|
private treasureChest?: TreasureChest;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
private background?: Phaser.GameObjects.TileSprite;
|
||||||
|
|
||||||
|
// UI
|
||||||
|
private scoreText?: Phaser.GameObjects.Text;
|
||||||
|
private timerText?: Phaser.GameObjects.Text;
|
||||||
|
private controlInfoText?: Phaser.GameObjects.Text;
|
||||||
|
private livesText?: Phaser.GameObjects.Text;
|
||||||
|
private giftsCollectedText?: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
// Game state
|
||||||
|
private score: number = 0;
|
||||||
|
private timeRemaining: number = LEVEL_DURATION;
|
||||||
|
private gameStartTime: number = 0;
|
||||||
|
private isMobile: boolean = false;
|
||||||
|
private lives: number = PLAYER_STARTING_LIVES;
|
||||||
|
private giftsCollected: number = 0;
|
||||||
|
private lastCheckpointX: number = 200; // Position du dernier checkpoint
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'GameScene' });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
this.gameStartTime = this.time.now;
|
||||||
|
|
||||||
|
// Détecter si mobile
|
||||||
|
this.isMobile = this.sys.game.device.os.android || this.sys.game.device.os.iOS;
|
||||||
|
|
||||||
|
// Configurer les limites du monde physique (IMPORTANT pour permettre mouvement infini)
|
||||||
|
const levelWidth = width * 6; // Niveau 6x plus grand
|
||||||
|
this.physics.world.setBounds(0, 0, levelWidth, height);
|
||||||
|
|
||||||
|
// Créer le background qui défile
|
||||||
|
this.createBackground();
|
||||||
|
|
||||||
|
// Créer les plateformes
|
||||||
|
this.createPlatforms();
|
||||||
|
|
||||||
|
// Créer le joueur
|
||||||
|
this.player = new Player(this, 200, height - 150);
|
||||||
|
|
||||||
|
// Collision joueur / plateformes
|
||||||
|
this.physics.add.collider(this.player, this.platforms!);
|
||||||
|
|
||||||
|
// Créer les groupes d'objets
|
||||||
|
this.createObjectGroups();
|
||||||
|
|
||||||
|
// Configuration caméra (suit le joueur)
|
||||||
|
this.cameras.main.startFollow(this.player, true, 0.1, 0.1);
|
||||||
|
this.cameras.main.setBounds(0, 0, levelWidth, height);
|
||||||
|
|
||||||
|
// Contrôles PC (clavier)
|
||||||
|
this.cursors = this.input.keyboard?.createCursorKeys();
|
||||||
|
|
||||||
|
// Contrôles Mobile (gyroscope + bouton tactile)
|
||||||
|
if (this.isMobile) {
|
||||||
|
this.setupMobileControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI
|
||||||
|
this.createUI();
|
||||||
|
|
||||||
|
// Générer quelques obstacles et cadeaux de test
|
||||||
|
this.spawnTestObjects();
|
||||||
|
|
||||||
|
console.log(`GameScene créée - Mode: ${this.isMobile ? 'Mobile' : 'PC'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée le background qui défile
|
||||||
|
*/
|
||||||
|
private createBackground(): void {
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
// Background temporaire (sera remplacé par une vraie image)
|
||||||
|
const graphics = this.add.graphics();
|
||||||
|
|
||||||
|
// Ciel dégradé
|
||||||
|
graphics.fillGradientStyle(0x87CEEB, 0x87CEEB, 0xE0F6FF, 0xE0F6FF, 1);
|
||||||
|
graphics.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Nuages simples
|
||||||
|
graphics.fillStyle(0xFFFFFF, 0.6);
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const x = (width / 5) * i + 50;
|
||||||
|
const y = 100 + Math.random() * 100;
|
||||||
|
graphics.fillCircle(x, y, 30);
|
||||||
|
graphics.fillCircle(x + 20, y, 25);
|
||||||
|
graphics.fillCircle(x + 40, y, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics.generateTexture('sky', width, height);
|
||||||
|
graphics.destroy();
|
||||||
|
|
||||||
|
this.background = this.add.tileSprite(0, 0, width * 6, height, 'sky');
|
||||||
|
this.background.setOrigin(0, 0);
|
||||||
|
this.background.setScrollFactor(0.3); // Effet parallaxe
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée les plateformes du niveau (version étendue avec beaucoup plus de plateformes)
|
||||||
|
*/
|
||||||
|
private createPlatforms(): void {
|
||||||
|
this.platforms = this.physics.add.staticGroup();
|
||||||
|
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
// Sol principal (très large pour le niveau 6x)
|
||||||
|
const groundWidth = width * 6;
|
||||||
|
const ground = this.add.rectangle(groundWidth / 2, height - 25, groundWidth, 50, 0x8B4513);
|
||||||
|
this.physics.add.existing(ground, true);
|
||||||
|
this.platforms.add(ground);
|
||||||
|
|
||||||
|
// BEAUCOUP plus de plateformes réparties sur toute la longueur
|
||||||
|
const platformPositions = [
|
||||||
|
// Zone 1 (début - facile)
|
||||||
|
{ x: 400, y: height - 150, w: 200, h: 30 },
|
||||||
|
{ x: 700, y: height - 250, w: 180, h: 30 },
|
||||||
|
{ x: 1000, y: height - 200, w: 200, h: 30 },
|
||||||
|
{ x: 1300, y: height - 300, w: 150, h: 30 },
|
||||||
|
|
||||||
|
// Zone 2 (moyen)
|
||||||
|
{ x: 1600, y: height - 180, w: 220, h: 30 },
|
||||||
|
{ x: 1900, y: height - 320, w: 160, h: 30 },
|
||||||
|
{ x: 2200, y: height - 240, w: 200, h: 30 },
|
||||||
|
{ x: 2500, y: height - 380, w: 140, h: 30 },
|
||||||
|
{ x: 2800, y: height - 280, w: 180, h: 30 },
|
||||||
|
|
||||||
|
// Zone 3 (plus difficile)
|
||||||
|
{ x: 3100, y: height - 200, w: 150, h: 30 },
|
||||||
|
{ x: 3350, y: height - 350, w: 120, h: 30 },
|
||||||
|
{ x: 3600, y: height - 250, w: 200, h: 30 },
|
||||||
|
{ x: 3900, y: height - 400, w: 150, h: 30 },
|
||||||
|
{ x: 4200, y: height - 300, w: 180, h: 30 },
|
||||||
|
|
||||||
|
// Zone 4 (avancé)
|
||||||
|
{ x: 4500, y: height - 180, w: 160, h: 30 },
|
||||||
|
{ x: 4800, y: height - 340, w: 140, h: 30 },
|
||||||
|
{ x: 5100, y: height - 250, w: 200, h: 30 },
|
||||||
|
{ x: 5400, y: height - 420, w: 150, h: 30 },
|
||||||
|
{ x: 5700, y: height - 320, w: 180, h: 30 },
|
||||||
|
|
||||||
|
// Zone 5 (très difficile)
|
||||||
|
{ x: 6000, y: height - 200, w: 140, h: 30 },
|
||||||
|
{ x: 6250, y: height - 380, w: 120, h: 30 },
|
||||||
|
{ x: 6500, y: height - 280, w: 160, h: 30 },
|
||||||
|
{ x: 6800, y: height - 450, w: 130, h: 30 },
|
||||||
|
{ x: 7100, y: height - 350, w: 180, h: 30 },
|
||||||
|
|
||||||
|
// Zone 6 (finale)
|
||||||
|
{ x: 7400, y: height - 250, w: 200, h: 30 },
|
||||||
|
{ x: 7700, y: height - 180, w: 300, h: 30 }, // Grande plateforme finale
|
||||||
|
];
|
||||||
|
|
||||||
|
platformPositions.forEach((pos) => {
|
||||||
|
const platform = this.add.rectangle(pos.x, pos.y, pos.w, pos.h, 0x6B8E23);
|
||||||
|
this.physics.add.existing(platform, true);
|
||||||
|
this.platforms!.add(platform);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${platformPositions.length} plateformes créées sur ${groundWidth}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée les groupes d'obstacles, cadeaux et super trésors
|
||||||
|
*/
|
||||||
|
private createObjectGroups(): void {
|
||||||
|
this.obstacles = this.physics.add.group();
|
||||||
|
this.gifts = this.physics.add.group();
|
||||||
|
this.superTreasures = this.physics.add.group();
|
||||||
|
|
||||||
|
// Collisions
|
||||||
|
this.physics.add.overlap(this.player!, this.gifts, this.collectGift, undefined, this);
|
||||||
|
this.physics.add.overlap(this.player!, this.obstacles, this.hitObstacle, undefined, this);
|
||||||
|
this.physics.add.overlap(this.player!, this.superTreasures, this.collectSuperTreasure, undefined, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère BEAUCOUP d'objets répartis sur tout le niveau
|
||||||
|
*/
|
||||||
|
private spawnTestObjects(): void {
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
// BEAUCOUP de cadeaux répartis partout (environ tous les 300-500px)
|
||||||
|
const giftPositions = [
|
||||||
|
// Zone 1
|
||||||
|
600, 900, 1200, 1500,
|
||||||
|
// Zone 2
|
||||||
|
1800, 2100, 2400, 2700,
|
||||||
|
// Zone 3
|
||||||
|
3000, 3300, 3600, 3900,
|
||||||
|
// Zone 4
|
||||||
|
4200, 4500, 4800, 5100,
|
||||||
|
// Zone 5
|
||||||
|
5400, 5700, 6000, 6300,
|
||||||
|
// Zone 6
|
||||||
|
6600, 6900, 7200, 7500,
|
||||||
|
];
|
||||||
|
|
||||||
|
giftPositions.forEach((x) => {
|
||||||
|
// Alterner entre sol et en hauteur
|
||||||
|
const isHigh = Math.random() > 0.5;
|
||||||
|
const y = isHigh ? height - 200 - Math.random() * 150 : height - 100;
|
||||||
|
|
||||||
|
const gift = this.add.circle(x, y, 20, 0xFFEB3B);
|
||||||
|
this.physics.add.existing(gift);
|
||||||
|
this.gifts!.add(gift);
|
||||||
|
});
|
||||||
|
|
||||||
|
// BEAUCOUP d'obstacles répartis partout
|
||||||
|
const obstaclePositions = [
|
||||||
|
// Zone 1
|
||||||
|
800, 1100, 1400,
|
||||||
|
// Zone 2
|
||||||
|
2000, 2300, 2600, 2900,
|
||||||
|
// Zone 3
|
||||||
|
3200, 3500, 3800, 4100,
|
||||||
|
// Zone 4
|
||||||
|
4400, 4700, 5000, 5300,
|
||||||
|
// Zone 5
|
||||||
|
5600, 5900, 6200, 6500,
|
||||||
|
// Zone 6
|
||||||
|
6800, 7100, 7400,
|
||||||
|
];
|
||||||
|
|
||||||
|
obstaclePositions.forEach((x) => {
|
||||||
|
const obstacle = this.add.rectangle(x, height - 80, 40, 60, 0xF44336);
|
||||||
|
this.physics.add.existing(obstacle);
|
||||||
|
this.obstacles!.add(obstacle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// SUPER TRÉSORS (rares et précieux - 1 par zone)
|
||||||
|
const superTreasurePositions = [
|
||||||
|
{ x: 1000, y: height - 350 }, // Zone 1 - en hauteur
|
||||||
|
{ x: 2500, y: height - 420 }, // Zone 2 - très haut
|
||||||
|
{ x: 3900, y: height - 450 }, // Zone 3 - très haut
|
||||||
|
{ x: 5400, y: height - 470 }, // Zone 4 - ultra haut
|
||||||
|
{ x: 6800, y: height - 500 }, // Zone 5 - ultra haut
|
||||||
|
{ x: 7300, y: height - 250 }, // Zone 6 - sur plateforme finale
|
||||||
|
];
|
||||||
|
|
||||||
|
superTreasurePositions.forEach((pos) => {
|
||||||
|
const superTreasure = new SuperTreasure(this, pos.x, pos.y);
|
||||||
|
this.superTreasures!.add(superTreasure);
|
||||||
|
});
|
||||||
|
|
||||||
|
// COFFRE FINAL au bout du niveau
|
||||||
|
this.treasureChest = new TreasureChest(this, 7600, height - 300, CHEST_REQUIRED_GIFTS);
|
||||||
|
this.physics.add.overlap(this.player!, this.treasureChest, this.openChest, undefined, this);
|
||||||
|
|
||||||
|
console.log(`${giftPositions.length} cadeaux, ${obstaclePositions.length} obstacles, ${superTreasurePositions.length} SUPER TRÉSORS et 1 COFFRE FINAL créés`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les contrôles mobile
|
||||||
|
*/
|
||||||
|
private setupMobileControls(): void {
|
||||||
|
// Gyroscope
|
||||||
|
this.gyroControl = new GyroControl();
|
||||||
|
|
||||||
|
// Bouton de saut
|
||||||
|
this.jumpButton = new JumpButton(this, () => {
|
||||||
|
this.player?.jump();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée l'interface utilisateur
|
||||||
|
*/
|
||||||
|
private createUI(): void {
|
||||||
|
// Score
|
||||||
|
this.scoreText = this.add.text(20, 20, 'Score: 0', {
|
||||||
|
fontSize: '32px',
|
||||||
|
color: '#ffffff',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
});
|
||||||
|
this.scoreText.setScrollFactor(0);
|
||||||
|
this.scoreText.setDepth(100);
|
||||||
|
|
||||||
|
// Vies
|
||||||
|
this.livesText = this.add.text(20, 60, `❤️ Vies: ${this.lives}`, {
|
||||||
|
fontSize: '28px',
|
||||||
|
color: '#ff0000',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
});
|
||||||
|
this.livesText.setScrollFactor(0);
|
||||||
|
this.livesText.setDepth(100);
|
||||||
|
|
||||||
|
// Cadeaux collectés
|
||||||
|
this.giftsCollectedText = this.add.text(20, 100, `🎁 Cadeaux: ${this.giftsCollected}/${CHEST_REQUIRED_GIFTS}`, {
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 3,
|
||||||
|
});
|
||||||
|
this.giftsCollectedText.setScrollFactor(0);
|
||||||
|
this.giftsCollectedText.setDepth(100);
|
||||||
|
|
||||||
|
// Timer
|
||||||
|
this.timerText = this.add.text(this.cameras.main.width / 2, 20, '3:00', {
|
||||||
|
fontSize: '32px',
|
||||||
|
color: '#ffffff',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
});
|
||||||
|
this.timerText.setOrigin(0.5, 0);
|
||||||
|
this.timerText.setScrollFactor(0);
|
||||||
|
this.timerText.setDepth(100);
|
||||||
|
|
||||||
|
// Info contrôles (avec mention du double saut)
|
||||||
|
const controlText = this.isMobile
|
||||||
|
? 'Inclinez pour bouger • Bouton pour sauter (DOUBLE SAUT disponible!)'
|
||||||
|
: 'Flèches pour bouger • Espace pour sauter (DOUBLE SAUT disponible!)';
|
||||||
|
|
||||||
|
this.controlInfoText = this.add.text(
|
||||||
|
this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height - 30,
|
||||||
|
controlText,
|
||||||
|
{
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#ffffff',
|
||||||
|
backgroundColor: '#00000088',
|
||||||
|
padding: { x: 10, y: 5 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.controlInfoText.setOrigin(0.5);
|
||||||
|
this.controlInfoText.setScrollFactor(0);
|
||||||
|
this.controlInfoText.setDepth(100);
|
||||||
|
|
||||||
|
// Bouton retour menu
|
||||||
|
const backButton = this.add.text(this.cameras.main.width - 20, 20, '⬅ Menu', {
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#ffffff',
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
padding: { x: 10, y: 5 },
|
||||||
|
});
|
||||||
|
backButton.setOrigin(1, 0);
|
||||||
|
backButton.setScrollFactor(0);
|
||||||
|
backButton.setDepth(100);
|
||||||
|
backButton.setInteractive({ useHandCursor: true });
|
||||||
|
backButton.on('pointerdown', () => {
|
||||||
|
this.cleanup();
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(time: number): void {
|
||||||
|
if (!this.player) return;
|
||||||
|
|
||||||
|
// Mise à jour du timer
|
||||||
|
this.updateTimer(time);
|
||||||
|
|
||||||
|
// Gestion des contrôles
|
||||||
|
let direction = 0;
|
||||||
|
|
||||||
|
// PC : Clavier
|
||||||
|
if (this.cursors) {
|
||||||
|
if (this.cursors.left.isDown) {
|
||||||
|
direction = -1;
|
||||||
|
} else if (this.cursors.right.isDown) {
|
||||||
|
direction = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saut avec Espace
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.cursors.space!)) {
|
||||||
|
this.player.jump();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saut avec flèche haut (alternative)
|
||||||
|
if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) {
|
||||||
|
this.player.jump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile : Gyroscope
|
||||||
|
if (this.gyroControl && this.isMobile) {
|
||||||
|
const tiltValue = this.gyroControl.getTiltValue();
|
||||||
|
direction = tiltValue; // -1 à 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déplacer le joueur
|
||||||
|
this.player.move(direction);
|
||||||
|
this.player.update();
|
||||||
|
|
||||||
|
// Système de checkpoint (sauvegarde tous les 1000px)
|
||||||
|
const playerX = this.player.x;
|
||||||
|
if (playerX > this.lastCheckpointX + 1000) {
|
||||||
|
this.lastCheckpointX = Math.floor(playerX / 1000) * 1000;
|
||||||
|
console.log(`✅ Checkpoint atteint à x=${this.lastCheckpointX}`);
|
||||||
|
|
||||||
|
// Feedback visuel rapide
|
||||||
|
this.cameras.main.flash(50, 0, 255, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll du background (effet parallaxe)
|
||||||
|
if (this.background) {
|
||||||
|
this.background.tilePositionX = this.cameras.main.scrollX * 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le timer
|
||||||
|
*/
|
||||||
|
private updateTimer(time: number): void {
|
||||||
|
const elapsed = Math.floor((time - this.gameStartTime) / 1000);
|
||||||
|
this.timeRemaining = LEVEL_DURATION - elapsed;
|
||||||
|
|
||||||
|
if (this.timeRemaining <= 0) {
|
||||||
|
this.timeRemaining = 0;
|
||||||
|
this.endGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(this.timeRemaining / 60);
|
||||||
|
const seconds = this.timeRemaining % 60;
|
||||||
|
this.timerText?.setText(`${minutes}:${seconds.toString().padStart(2, '0')}`);
|
||||||
|
|
||||||
|
// Changement de couleur si temps faible
|
||||||
|
if (this.timeRemaining <= 30) {
|
||||||
|
this.timerText?.setColor('#FF0000');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collecte un cadeau
|
||||||
|
*/
|
||||||
|
private collectGift(_player: any, gift: any): void {
|
||||||
|
gift.destroy();
|
||||||
|
this.giftsCollected++;
|
||||||
|
this.addScore(100);
|
||||||
|
|
||||||
|
// Mettre à jour l'UI
|
||||||
|
this.giftsCollectedText?.setText(`🎁 Cadeaux: ${this.giftsCollected}/${CHEST_REQUIRED_GIFTS}`);
|
||||||
|
|
||||||
|
// Feedback si on a assez pour le coffre
|
||||||
|
if (this.giftsCollected >= CHEST_REQUIRED_GIFTS && !this.treasureChest?.getIsOpen()) {
|
||||||
|
// Flash doré
|
||||||
|
this.cameras.main.flash(100, 255, 215, 0, true);
|
||||||
|
|
||||||
|
const hint = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
'🏆 Assez de cadeaux! Trouvez le coffre! 🏆',
|
||||||
|
{
|
||||||
|
fontSize: '32px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
hint.setOrigin(0.5);
|
||||||
|
hint.setScrollFactor(0);
|
||||||
|
hint.setDepth(1000);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: hint,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 3000,
|
||||||
|
onComplete: () => hint.destroy(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collision avec un obstacle
|
||||||
|
*/
|
||||||
|
private hitObstacle(player: any, obstacle: any): void {
|
||||||
|
// Vérifier si on saute dessus (player au-dessus de l'obstacle)
|
||||||
|
const playerBody = player.body as Phaser.Physics.Arcade.Body;
|
||||||
|
const obstacleBody = obstacle.body as Phaser.Physics.Arcade.Body;
|
||||||
|
|
||||||
|
const isJumpingOn = playerBody.velocity.y > 0 &&
|
||||||
|
playerBody.bottom <= obstacleBody.top + 10;
|
||||||
|
|
||||||
|
if (isJumpingOn) {
|
||||||
|
// Sauter dessus = détruit l'obstacle
|
||||||
|
obstacle.destroy();
|
||||||
|
this.addScore(50); // Bonus pour avoir sauté dessus
|
||||||
|
player.jump(); // Petit rebond
|
||||||
|
|
||||||
|
// Effet visuel
|
||||||
|
const explosion = this.add.circle(obstacleBody.x, obstacleBody.y, 20, 0x00FF00, 0.5);
|
||||||
|
explosion.setDepth(100);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: explosion,
|
||||||
|
scaleX: 2,
|
||||||
|
scaleY: 2,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 300,
|
||||||
|
onComplete: () => explosion.destroy(),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('💚 Obstacle détruit en sautant dessus ! +50 pts');
|
||||||
|
} else {
|
||||||
|
// Collision frontale = perd une vie
|
||||||
|
if (player.getIsInvincible()) {
|
||||||
|
console.log('🛡️ Invincible - pas de dégâts');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loseLife();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collecte un SUPER TRÉSOR (beaucoup de points!)
|
||||||
|
*/
|
||||||
|
private collectSuperTreasure(_player: any, superTreasure: any): void {
|
||||||
|
// Effet visuel de collecte
|
||||||
|
this.cameras.main.flash(200, 255, 215, 0, true); // Flash doré
|
||||||
|
|
||||||
|
// Destruction avec effet
|
||||||
|
superTreasure.destroy();
|
||||||
|
|
||||||
|
// GROS BONUS de points!
|
||||||
|
this.addScore(500);
|
||||||
|
|
||||||
|
// Message spécial
|
||||||
|
const bonusText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
'★ SUPER TRÉSOR +500 ★',
|
||||||
|
{
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#FF8C00',
|
||||||
|
strokeThickness: 6,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
bonusText.setOrigin(0.5);
|
||||||
|
bonusText.setScrollFactor(0);
|
||||||
|
bonusText.setDepth(1000);
|
||||||
|
|
||||||
|
// Animation du texte (apparition puis disparition)
|
||||||
|
this.tweens.add({
|
||||||
|
targets: bonusText,
|
||||||
|
scaleX: 1.5,
|
||||||
|
scaleY: 1.5,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 1500,
|
||||||
|
ease: 'Power2',
|
||||||
|
onComplete: () => {
|
||||||
|
bonusText.destroy();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🌟 SUPER TRÉSOR COLLECTÉ ! +500 points !');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ouvre le coffre au trésor final
|
||||||
|
*/
|
||||||
|
private openChest(_player: any, _chest: any): void {
|
||||||
|
if (!this.treasureChest) return;
|
||||||
|
|
||||||
|
// Vérifier si on peut ouvrir
|
||||||
|
if (this.treasureChest.canOpen(this.giftsCollected)) {
|
||||||
|
const megaBonus = this.treasureChest.open(this);
|
||||||
|
this.addScore(megaBonus);
|
||||||
|
|
||||||
|
// VICTOIRE ! Lancer l'animation de fin
|
||||||
|
this.time.delayedCall(2000, () => {
|
||||||
|
this.levelComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation de victoire - Niveau terminé !
|
||||||
|
*/
|
||||||
|
private levelComplete(): void {
|
||||||
|
console.log('🏆 NIVEAU TERMINÉ ! VICTOIRE !');
|
||||||
|
|
||||||
|
// Arrêter la physique
|
||||||
|
this.physics.pause();
|
||||||
|
|
||||||
|
// Flash doré géant
|
||||||
|
this.cameras.main.flash(1000, 255, 215, 0, true);
|
||||||
|
|
||||||
|
// Fond sombre semi-transparent
|
||||||
|
const overlay = this.add.rectangle(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
this.cameras.main.width,
|
||||||
|
this.cameras.main.height,
|
||||||
|
0x000000,
|
||||||
|
0.7
|
||||||
|
);
|
||||||
|
overlay.setScrollFactor(0);
|
||||||
|
overlay.setDepth(1500);
|
||||||
|
|
||||||
|
// Message VICTOIRE
|
||||||
|
const victoryText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2 - 150,
|
||||||
|
'🏆 NIVEAU TERMINÉ ! 🏆',
|
||||||
|
{
|
||||||
|
fontSize: '64px',
|
||||||
|
color: '#FFD700',
|
||||||
|
stroke: '#FF8C00',
|
||||||
|
strokeThickness: 10,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
victoryText.setOrigin(0.5);
|
||||||
|
victoryText.setScrollFactor(0);
|
||||||
|
victoryText.setDepth(2000);
|
||||||
|
|
||||||
|
// Animation pulsation
|
||||||
|
this.tweens.add({
|
||||||
|
targets: victoryText,
|
||||||
|
scaleX: 1.2,
|
||||||
|
scaleY: 1.2,
|
||||||
|
duration: 800,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Statistiques
|
||||||
|
const stats = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2 - 20,
|
||||||
|
`Score Final: ${this.score}\n\n` +
|
||||||
|
`Cadeaux collectés: ${this.giftsCollected}\n` +
|
||||||
|
`Vies restantes: ${this.lives}\n\n` +
|
||||||
|
`Félicitations !`,
|
||||||
|
{
|
||||||
|
fontSize: '32px',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 6,
|
||||||
|
align: 'center',
|
||||||
|
lineSpacing: 10,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
stats.setOrigin(0.5);
|
||||||
|
stats.setScrollFactor(0);
|
||||||
|
stats.setDepth(2000);
|
||||||
|
|
||||||
|
// Message retour menu
|
||||||
|
const returnText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height - 80,
|
||||||
|
'Retour au menu dans 7 secondes...',
|
||||||
|
{
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#CCCCCC',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
returnText.setOrigin(0.5);
|
||||||
|
returnText.setScrollFactor(0);
|
||||||
|
returnText.setDepth(2000);
|
||||||
|
|
||||||
|
// Particules de célébration
|
||||||
|
this.createVictoryParticles();
|
||||||
|
|
||||||
|
// Retour au menu après 7 secondes
|
||||||
|
this.time.delayedCall(7000, () => {
|
||||||
|
this.cleanup();
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée des particules de célébration pour la victoire
|
||||||
|
*/
|
||||||
|
private createVictoryParticles(): void {
|
||||||
|
const centerX = this.cameras.main.scrollX + this.cameras.main.width / 2;
|
||||||
|
|
||||||
|
// Créer 50 particules dorées qui explosent
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const angle = (i / 50) * Math.PI * 2;
|
||||||
|
const radius = 100 + Math.random() * 200;
|
||||||
|
|
||||||
|
const particle = this.add.circle(
|
||||||
|
centerX,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
5 + Math.random() * 10,
|
||||||
|
Math.random() > 0.5 ? 0xFFD700 : 0xFF8C00
|
||||||
|
);
|
||||||
|
particle.setScrollFactor(0);
|
||||||
|
particle.setDepth(1900);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: particle,
|
||||||
|
x: centerX + Math.cos(angle) * radius,
|
||||||
|
y: this.cameras.main.height / 2 + Math.sin(angle) * radius - 100,
|
||||||
|
alpha: 0,
|
||||||
|
scaleX: 0.2,
|
||||||
|
scaleY: 0.2,
|
||||||
|
duration: 2000 + Math.random() * 1000,
|
||||||
|
ease: 'Power2',
|
||||||
|
onComplete: () => {
|
||||||
|
particle.destroy();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fait perdre une vie au joueur
|
||||||
|
*/
|
||||||
|
private loseLife(): void {
|
||||||
|
// Flash rouge pour indiquer les dégâts
|
||||||
|
this.cameras.main.flash(200, 255, 0, 0, true);
|
||||||
|
|
||||||
|
// Décrémenter les vies
|
||||||
|
this.lives--;
|
||||||
|
this.livesText?.setText(`❤️ Vies: ${this.lives}`);
|
||||||
|
|
||||||
|
// Effet sonore (TODO: ajouter un vrai son)
|
||||||
|
console.log(`💔 Vie perdue ! Vies restantes: ${this.lives}`);
|
||||||
|
|
||||||
|
if (this.lives <= 0) {
|
||||||
|
// Game Over
|
||||||
|
this.gameOver();
|
||||||
|
} else {
|
||||||
|
// Respawn au dernier checkpoint
|
||||||
|
this.respawnPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réapparaît au dernier checkpoint avec invincibilité
|
||||||
|
*/
|
||||||
|
private respawnPlayer(): void {
|
||||||
|
if (!this.player) return;
|
||||||
|
|
||||||
|
// Téléporter au checkpoint
|
||||||
|
this.player.setPosition(this.lastCheckpointX, this.cameras.main.height - 150);
|
||||||
|
this.player.setVelocity(0, 0);
|
||||||
|
|
||||||
|
// Activer l'invincibilité temporaire
|
||||||
|
this.player.makeInvincible(this);
|
||||||
|
|
||||||
|
console.log(`🔄 Respawn au checkpoint x=${this.lastCheckpointX}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game Over - plus de vies
|
||||||
|
*/
|
||||||
|
private gameOver(): void {
|
||||||
|
console.log('💀 GAME OVER - Plus de vies!');
|
||||||
|
|
||||||
|
// Arrêter la physique
|
||||||
|
this.physics.pause();
|
||||||
|
|
||||||
|
// Message Game Over
|
||||||
|
const gameOverText = this.add.text(
|
||||||
|
this.cameras.main.scrollX + this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
`GAME OVER\n\nScore Final: ${this.score}\n\nRetour au menu dans 5s...`,
|
||||||
|
{
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#FF0000',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 8,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
align: 'center',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
gameOverText.setOrigin(0.5);
|
||||||
|
gameOverText.setScrollFactor(0);
|
||||||
|
gameOverText.setDepth(2000);
|
||||||
|
|
||||||
|
// Animation du texte
|
||||||
|
this.tweens.add({
|
||||||
|
targets: gameOverText,
|
||||||
|
scaleX: 1.1,
|
||||||
|
scaleY: 1.1,
|
||||||
|
duration: 500,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retour au menu après 5 secondes
|
||||||
|
this.time.delayedCall(5000, () => {
|
||||||
|
this.cleanup();
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute des points au score
|
||||||
|
*/
|
||||||
|
private addScore(points: number): void {
|
||||||
|
this.score += points;
|
||||||
|
if (this.score < 0) this.score = 0;
|
||||||
|
this.scoreText?.setText(`Score: ${this.score}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Termine le jeu
|
||||||
|
*/
|
||||||
|
private endGame(): void {
|
||||||
|
console.log(`Jeu terminé ! Score final: ${this.score}`);
|
||||||
|
|
||||||
|
// TODO: Créer une EndScene
|
||||||
|
this.add.text(
|
||||||
|
this.cameras.main.width / 2,
|
||||||
|
this.cameras.main.height / 2,
|
||||||
|
`TEMPS ÉCOULÉ!\nScore: ${this.score}`,
|
||||||
|
{
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#ffffff',
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
padding: { x: 20, y: 20 },
|
||||||
|
align: 'center',
|
||||||
|
}
|
||||||
|
).setOrigin(0.5).setScrollFactor(0).setDepth(1000);
|
||||||
|
|
||||||
|
this.time.delayedCall(3000, () => {
|
||||||
|
this.cleanup();
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyage avant de quitter la scène
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
if (this.gyroControl) {
|
||||||
|
this.gyroControl.destroy();
|
||||||
|
}
|
||||||
|
if (this.jumpButton) {
|
||||||
|
this.jumpButton.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/scenes/MenuScene.ts
Normal file
125
src/scenes/MenuScene.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import Phaser from 'phaser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scène de menu - Demande permission gyroscope et lance le jeu
|
||||||
|
*/
|
||||||
|
export class MenuScene extends Phaser.Scene {
|
||||||
|
private startButton?: Phaser.GameObjects.Text;
|
||||||
|
private gyroStatus?: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'MenuScene' });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
const width = this.cameras.main.width;
|
||||||
|
const height = this.cameras.main.height;
|
||||||
|
|
||||||
|
// Titre
|
||||||
|
const title = this.add.text(width / 2, height / 3, 'MARIO RUNNER', {
|
||||||
|
fontSize: '64px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
stroke: '#000000',
|
||||||
|
strokeThickness: 6,
|
||||||
|
});
|
||||||
|
title.setOrigin(0.5);
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
const instructions = this.add.text(
|
||||||
|
width / 2,
|
||||||
|
height / 2,
|
||||||
|
'Inclinez le téléphone pour avancer/reculer\nAppuyez sur le bouton pour sauter',
|
||||||
|
{
|
||||||
|
fontSize: '24px',
|
||||||
|
color: '#ffffff',
|
||||||
|
align: 'center',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
instructions.setOrigin(0.5);
|
||||||
|
|
||||||
|
// Bouton démarrer
|
||||||
|
this.startButton = this.add.text(width / 2, height / 1.5, 'DÉMARRER', {
|
||||||
|
fontSize: '48px',
|
||||||
|
color: '#4CAF50',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
padding: { x: 20, y: 10 },
|
||||||
|
});
|
||||||
|
this.startButton.setOrigin(0.5);
|
||||||
|
this.startButton.setInteractive({ useHandCursor: true });
|
||||||
|
|
||||||
|
// Status gyroscope
|
||||||
|
this.gyroStatus = this.add.text(width / 2, height - 150, '', {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#ffeb3b',
|
||||||
|
align: 'center',
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
padding: { x: 10, y: 10 },
|
||||||
|
wordWrap: { width: width - 40 },
|
||||||
|
});
|
||||||
|
this.gyroStatus.setOrigin(0.5);
|
||||||
|
|
||||||
|
// Click sur le bouton
|
||||||
|
this.startButton.on('pointerdown', () => {
|
||||||
|
this.requestGyroPermission();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Effet hover
|
||||||
|
this.startButton.on('pointerover', () => {
|
||||||
|
this.startButton?.setScale(1.1);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startButton.on('pointerout', () => {
|
||||||
|
this.startButton?.setScale(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demande la permission gyroscope (iOS) et lance le jeu
|
||||||
|
*/
|
||||||
|
private requestGyroPermission(): void {
|
||||||
|
let debugInfo = '🔍 Vérification...\n';
|
||||||
|
debugInfo += `DeviceOrientation: ${!!window.DeviceOrientationEvent}\n`;
|
||||||
|
debugInfo += `requestPermission: ${typeof (DeviceOrientationEvent as any).requestPermission}\n`;
|
||||||
|
debugInfo += `HTTPS: ${window.location.protocol === 'https:'}\n`;
|
||||||
|
debugInfo += `UserAgent: ${navigator.userAgent.substring(0, 50)}...`;
|
||||||
|
|
||||||
|
this.gyroStatus?.setText(debugInfo);
|
||||||
|
|
||||||
|
// iOS 13+ nécessite une permission explicite
|
||||||
|
if (typeof (DeviceOrientationEvent as any).requestPermission === 'function') {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.gyroStatus?.setText('📱 iOS détecté\nDemande permission...');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
(DeviceOrientationEvent as any).requestPermission()
|
||||||
|
.then((response: string) => {
|
||||||
|
if (response === 'granted') {
|
||||||
|
this.gyroStatus?.setText('✅ Permission accordée\nLancement du jeu...');
|
||||||
|
setTimeout(() => this.startGame(), 500);
|
||||||
|
} else {
|
||||||
|
this.gyroStatus?.setText(`❌ Permission: ${response}\nLancement quand même...`);
|
||||||
|
setTimeout(() => this.startGame(), 2000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
this.gyroStatus?.setText(`❌ Erreur:\n${error.message}\n${error.name}\nLancement quand même...`);
|
||||||
|
setTimeout(() => this.startGame(), 3000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Android ou navigateurs sans permission requise
|
||||||
|
setTimeout(() => {
|
||||||
|
this.gyroStatus?.setText('✅ Android/Desktop\nGyroscope disponible\nLancement...');
|
||||||
|
setTimeout(() => this.startGame(), 500);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lance la scène de jeu
|
||||||
|
*/
|
||||||
|
private startGame(): void {
|
||||||
|
this.scene.start('GameScene');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/utils/constants.ts
Normal file
34
src/utils/constants.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Constantes du jeu
|
||||||
|
|
||||||
|
// Dimensions
|
||||||
|
export const GAME_WIDTH = 1280;
|
||||||
|
export const GAME_HEIGHT = 720;
|
||||||
|
|
||||||
|
// Physique joueur
|
||||||
|
export const PLAYER_GRAVITY = 800;
|
||||||
|
export const PLAYER_JUMP_VELOCITY = -550; // Augmenté pour atteindre les plateformes
|
||||||
|
export const PLAYER_MAX_SPEED = 300;
|
||||||
|
export const PLAYER_ACCELERATION = 50;
|
||||||
|
export const PLAYER_MAX_JUMPS = 2; // Nombre de sauts autorisés (double saut)
|
||||||
|
|
||||||
|
// Gyroscope
|
||||||
|
export const GYRO_DEADZONE = 5; // Degrés de zone morte
|
||||||
|
export const GYRO_MAX_TILT = 30; // Tilt maximum pris en compte (degrés)
|
||||||
|
export const GYRO_SENSITIVITY = 10; // Multiplicateur de sensibilité
|
||||||
|
|
||||||
|
// Niveau
|
||||||
|
export const LEVEL_DURATION = 180; // 3 minutes en secondes
|
||||||
|
export const LEVEL_LENGTH = 10000; // Longueur du niveau en pixels (alternative au timer)
|
||||||
|
|
||||||
|
// Système de vies
|
||||||
|
export const PLAYER_STARTING_LIVES = 3; // Nombre de vies au départ
|
||||||
|
export const RESPAWN_INVINCIBILITY_TIME = 2000; // Temps d'invincibilité après respawn (ms)
|
||||||
|
|
||||||
|
// Coffre final
|
||||||
|
export const CHEST_REQUIRED_GIFTS = 5; // Nombre de cadeaux requis pour ouvrir le coffre
|
||||||
|
|
||||||
|
// Couleurs
|
||||||
|
export const COLOR_PRIMARY = 0x4CAF50;
|
||||||
|
export const COLOR_DANGER = 0xF44336;
|
||||||
|
export const COLOR_GIFT = 0xFFEB3B;
|
||||||
|
export const COLOR_SKY = 0x87CEEB;
|
||||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Paths */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
20
vite.config.ts
Normal file
20
vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
host: true, // Permet l'accès depuis le réseau local (pour tester sur mobile)
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
allowedHosts: [
|
||||||
|
'jeu.maison43.duckdns.org', // Domaine DuckDNS autorisé
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
publicDir: 'public',
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user