talas-group/11_RECHERCHE_&_LAB/mockup_jeu_ux/game.js
senke 1db6d066c0 nettoyage repo : réorganisation fichiers en vrac, ajout body solidworks + studio mic ref
- Body SolidWorks v1 → 02_PRODUITS_PHYSIQUES/Microphone/Conception/
- Studio Mic KiCAD (DIYPerks) → 02_PRODUITS_PHYSIQUES/R&D_References/DIY/
- cleanup_ports.sh → 04_INFRA_DEPLOIEMENT/
- mockup_jeu_ux → 11_RECHERCHE_&_LAB/
- Printables → 12_DOCUMENTATION/Imprimables/
- Screenshots, ideas, one.html → _BROUILLON/
- all-talas (23Go) → 13_ARCHIVES/
- Supprimé all-talas.zip (20Go doublon), lock files LibreOffice
- Nettoyé .gitignore
- Remote → Forgejo (10.0.20.105:3000)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:31:26 +02:00

383 lines
No EOL
12 KiB
JavaScript

/**
* game.js
* Moteur 2D Top-Down V2 - Architecture Orientée Objet
* Tilemap, Caméra, Collisions & Interactions (Raycasting)
*/
// --- 1. Configuration Globale ---
const TILE_SIZE = 64;
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
// --- 2. Gestion des Entrées (Input) ---
const keys = {
w: false, a: false, s: false, d: false,
ArrowUp: false, ArrowLeft: false, ArrowDown: false, ArrowRight: false
};
// Écouteurs globaux pour le clavier (ZQSD / WASD / Flèches)
window.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.key)) keys[e.key] = true;
if (e.code === 'KeyW' || e.code === 'KeyZ') keys.w = true; // Z pour AZERTY, W pour QWERTY
if (e.code === 'KeyA' || e.code === 'KeyQ') keys.a = true; // Q pour AZERTY, A pour QWERTY
if (e.code === 'KeyS') keys.s = true;
if (e.code === 'KeyD') keys.d = true;
});
window.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.key)) keys[e.key] = false;
if (e.code === 'KeyW' || e.code === 'KeyZ') keys.w = false;
if (e.code === 'KeyA' || e.code === 'KeyQ') keys.a = false;
if (e.code === 'KeyS') keys.s = false;
if (e.code === 'KeyD') keys.d = false;
});
// --- 3. Classes de l'Architecture ---
/**
* Gère la position de la vue (Caméra) dans le monde
*/
class Camera {
constructor(width, height, mapWidth, mapHeight) {
this.x = 0;
this.y = 0;
this.width = width;
this.height = height;
this.mapWidth = mapWidth;
this.mapHeight = mapHeight;
}
update(target) {
// La caméra cible le centre de l'entité (le joueur)
this.x = target.x + target.width / 2 - this.width / 2;
this.y = target.y + target.height / 2 - this.height / 2;
// Culling de la caméra : on l'empêche de sortir des limites de la Tilemap
this.x = Math.max(0, Math.min(this.x, this.mapWidth - this.width));
this.y = Math.max(0, Math.min(this.y, this.mapHeight - this.height));
}
// Convertit des coordonnées écran (clic souris) en coordonnées monde absolues
screenToWorld(screenX, screenY) {
return {
x: screenX + this.x,
y: screenY + this.y
};
}
}
/**
* Gère la carte, le tableau 2D (grille) et le rendu des tuiles
*/
class TileMap {
constructor() {
this.tileSize = TILE_SIZE;
// Création d'une carte de 1600x1216 pixels (25x19 tuiles)
this.cols = 25;
this.rows = 19;
this.width = this.cols * this.tileSize;
this.height = this.rows * this.tileSize;
this.data = this.generateMap();
// Palette de couleurs pour la V2 (en attendant les sprites)
this.colors = {
0: '#6ab04c', // Herbe (Vert clair)
1: '#eb4d4b', // Chemin de terre (Marron/Rouge)
2: '#535c68' // Mur / Obstacle (Gris)
};
}
generateMap() {
const map = [];
for (let y = 0; y < this.rows; y++) {
const row = [];
for (let x = 0; x < this.cols; x++) {
// Créer des murs sur toutes les bordures extérieures
if (x === 0 || x === this.cols - 1 || y === 0 || y === this.rows - 1) {
row.push(2);
} else {
// Remplissage aléatoire simple du centre
const rand = Math.random();
if (rand < 0.15) row.push(2); // 15% de murs/obstacles
else if (rand < 0.35) row.push(1); // 20% de chemins
else row.push(0); // 65% d'herbe
}
}
map.push(row);
}
// S'assurer de libérer une "clairière" au centre pour que le joueur y apparaisse sans être bloqué
const midY = Math.floor(this.rows / 2);
const midX = Math.floor(this.cols / 2);
for(let dy = -2; dy <= 2; dy++) {
for(let dx = -2; dx <= 2; dx++) {
map[midY + dy][midX + dx] = 0; // Herbe au centre
}
}
return map;
}
// Rend uniquement les tuiles visibles par la caméra (Optimisation: Culling)
draw(ctx, camera) {
const startCol = Math.floor(camera.x / this.tileSize);
const endCol = startCol + (camera.width / this.tileSize) + 1;
const startRow = Math.floor(camera.y / this.tileSize);
const endRow = startRow + (camera.height / this.tileSize) + 1;
for (let y = startRow; y < endRow; y++) {
for (let x = startCol; x < endCol; x++) {
// Vérifier si la tuile existe bien dans le tableau pour éviter les erreurs
if (y >= 0 && y < this.rows && x >= 0 && x < this.cols) {
const tileType = this.data[y][x];
ctx.fillStyle = this.colors[tileType];
// Calcul de la position de dessin en soustrayant la position de la caméra
const drawX = Math.round(x * this.tileSize - camera.x);
const drawY = Math.round(y * this.tileSize - camera.y);
ctx.fillRect(drawX, drawY, this.tileSize, this.tileSize);
// Ligne optionnelle pour afficher la grille
ctx.strokeStyle = 'rgba(0,0,0,0.05)';
ctx.strokeRect(drawX, drawY, this.tileSize, this.tileSize);
}
}
}
}
// Indique si une position X,Y spécifique du monde est solide (infranchissable)
isSolid(x, y) {
const col = Math.floor(x / this.tileSize);
const row = Math.floor(y / this.tileSize);
// Sortir de la map compte comme un mur
if (col < 0 || col >= this.cols || row < 0 || row >= this.rows) {
return true;
}
// La tuile n°2 est notre identifiant pour "Mur"
return this.data[row][col] === 2;
}
}
/**
* L'avatar contrôlé par l'utilisateur
*/
class Player {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 32; // Plus petit que la tuile (64)
this.height = 32;
this.color = '#f9ca24'; // Jaune vif
this.speed = 5;
}
update(map) {
let dx = 0;
let dy = 0;
// Déterminer le vecteur de direction désiré
if (keys.w || keys.ArrowUp) dy -= this.speed;
if (keys.s || keys.ArrowDown) dy += this.speed;
if (keys.a || keys.ArrowLeft) dx -= this.speed;
if (keys.d || keys.ArrowRight) dx += this.speed;
// Normalisation de la vitesse en diagonale
if (dx !== 0 && dy !== 0) {
const length = Math.sqrt(dx * dx + dy * dy);
dx = (dx / length) * this.speed;
dy = (dy / length) * this.speed;
}
// Système de Collision séparé par axe (AABB)
// Axe X
if (dx !== 0) {
const newX = this.x + dx;
if (!this.checkCollision(newX, this.y, map)) {
this.x = newX;
}
}
// Axe Y
if (dy !== 0) {
const newY = this.y + dy;
if (!this.checkCollision(this.x, newY, map)) {
this.y = newY;
}
}
}
// Vérifie si la boîte de collision (Bounding Box) aux futures coordonnées heurte un mur
checkCollision(newX, newY, map) {
const left = newX;
const right = newX + this.width - 1; // -1 pour ne pas compter le pixel adjacent en collision
const top = newY;
const bottom = newY + this.height - 1;
// Si n'importe lequel des 4 coins touche un mur, il y a collision
return map.isSolid(left, top) ||
map.isSolid(right, top) ||
map.isSolid(left, bottom) ||
map.isSolid(right, bottom);
}
draw(ctx, camera) {
ctx.fillStyle = this.color;
// La position dessinée dépend de l'offset de la caméra
const drawX = Math.round(this.x - camera.x);
const drawY = Math.round(this.y - camera.y);
ctx.fillRect(drawX, drawY, this.width, this.height);
}
}
/**
* Entité interactive cliquable sur la carte
*/
class PointOfInterest {
constructor(x, y, radius = 24) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = '#0984e3'; // Bleu
}
draw(ctx, camera) {
const drawX = Math.round(this.x - camera.x);
const drawY = Math.round(this.y - camera.y);
ctx.beginPath();
ctx.arc(drawX, drawY, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.strokeStyle = '#74b9ff';
ctx.lineWidth = 4;
ctx.stroke();
}
// Vérifie si un point absolu du monde (px, py) intersecte le cercle (Raycasting basique)
contains(px, py) {
const dx = this.x - px;
const dy = this.y - py;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= this.radius;
}
}
/**
* Classe Chef d'Orchestre du jeu
*/
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.map = new TileMap();
// Apparition du joueur au centre du monde
const startX = (this.map.width / 2) - 16;
const startY = (this.map.height / 2) - 16;
this.player = new Player(startX, startY);
this.camera = new Camera(CANVAS_WIDTH, CANVAS_HEIGHT, this.map.width, this.map.height);
// Génération de quelques POIs autour du centre
this.pois = [
new PointOfInterest(startX + 200, startY + 50),
new PointOfInterest(startX - 250, startY + 150),
new PointOfInterest(startX + 60, startY - 200)
];
this.isPaused = false;
this.initUI();
this.initMouseInput();
// Lancer la boucle de jeu
requestAnimationFrame(() => this.loop());
}
initUI() {
this.modal = document.getElementById('ui-modal');
const closeBtn = document.getElementById('close-modal-btn');
// Fermer la modale
closeBtn.addEventListener('click', () => {
this.modal.classList.add('hidden');
this.isPaused = false; // Relancer les updates du jeu
});
}
initMouseInput() {
this.canvas.addEventListener('mousedown', (e) => {
if (this.isPaused) return;
// 1. Obtenir les coordonnées locales du clic sur le canvas
const rect = this.canvas.getBoundingClientRect();
// Facteur de mise à l'échelle si le canvas est redimensionné en CSS
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const mouseX = (e.clientX - rect.left) * scaleX;
const mouseY = (e.clientY - rect.top) * scaleY;
// 2. Convertir les coordonnées écran en coordonnées monde
const worldPos = this.camera.screenToWorld(mouseX, mouseY);
// 3. Vérifier l'intersection avec tous les Points d'Intérêts
for (const poi of this.pois) {
if (poi.contains(worldPos.x, worldPos.y)) {
this.triggerEvent(poi);
break;
}
}
});
}
triggerEvent(poi) {
this.isPaused = true;
this.modal.classList.remove('hidden');
// Réinitialiser les touches pour empêcher le joueur de "glisser" à la fermeture
for (let key in keys) {
keys[key] = false;
}
}
update() {
if (this.isPaused) return;
this.player.update(this.map);
this.camera.update(this.player);
}
draw() {
// Fond de sécurité
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 1. Dessiner la Tilemap
this.map.draw(this.ctx, this.camera);
// 2. Dessiner les objets interactifs
for (const poi of this.pois) {
poi.draw(this.ctx, this.camera);
}
// 3. Dessiner le Joueur
this.player.draw(this.ctx, this.camera);
}
loop() {
this.update();
this.draw();
requestAnimationFrame(() => this.loop());
}
}
// --- Démarrage ---
window.onload = () => {
new Game();
};