Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Vice City Web - PSP Edition</title> | |
| <style> | |
| /* IMPORTATION DE POLICE */ | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Press+Start+2P&display=swap'); | |
| /* RESET & BASE */ | |
| * { box-sizing: border-box; touch-action: none; user-select: none; -webkit-user-select: none; } | |
| body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #000; font-family: 'Orbitron', sans-serif; color: white; } | |
| /* LIEN ANYCODER */ | |
| .anycoder-link { | |
| position: absolute; | |
| top: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 1000; | |
| color: #0ff; | |
| text-decoration: none; | |
| font-size: 12px; | |
| text-shadow: 0 0 5px #0ff; | |
| font-family: sans-serif; | |
| opacity: 0.8; | |
| } | |
| /* CANEVAS DE JEU */ | |
| #gameCanvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; } | |
| /* INTERFACE UTILISATEUR (HUD) */ | |
| #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; padding: 10px; } | |
| .hud-top { display: flex; justify-content: space-between; align-items: flex-start; } | |
| .player-stats { text-shadow: 2px 2px 0 #000; } | |
| .health-bar-container { width: 150px; height: 15px; background: #333; border: 2px solid white; margin-bottom: 5px; } | |
| .health-bar { width: 100%; height: 100%; background: #f00; transition: width 0.2s; } | |
| .money { color: #0f0; font-size: 20px; font-weight: bold; text-shadow: 2px 2px 0 #000; } | |
| .wanted-level { color: #FFD700; font-size: 20px; letter-spacing: 5px; text-shadow: 2px 2px 0 #000; } | |
| .radio-display { background: rgba(0,0,0,0.7); padding: 5px; border: 1px solid #0ff; border-radius: 5px; color: #0ff; font-size: 12px; text-align: right; display: none; } | |
| .radio-display.active { display: block; animation: radioPulse 2s infinite; } | |
| @keyframes radioPulse { 0% { opacity: 0.7; } 50% { opacity: 1; text-shadow: 0 0 10px #0ff; } 100% { opacity: 0.7; } } | |
| /* NOTIFICATIONS DE MISSION */ | |
| #mission-text { | |
| position: absolute; | |
| bottom: 20%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0,0,0,0.8); | |
| border-left: 5px solid #f0f; | |
| padding: 15px; | |
| max-width: 80%; | |
| display: none; | |
| color: white; | |
| font-size: 14px; | |
| } | |
| /* MENUS (PAUSE, MAP) */ | |
| #pause-menu, #map-screen { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.85); | |
| display: none; flex-direction: column; align-items: center; justify-content: center; | |
| z-index: 500; pointer-events: auto; | |
| } | |
| #map-screen { background: #001133; } | |
| .menu-title { font-family: 'Press Start 2P', cursive; font-size: 30px; color: #f0f; margin-bottom: 20px; text-shadow: 4px 4px 0 #0ff; text-align: center; } | |
| .menu-item { color: white; font-size: 18px; margin: 10px 0; cursor: pointer; border: 1px solid transparent; padding: 5px; } | |
| .menu-item:hover { border: 1px solid #f0f; color: #f0f; } | |
| /* CONTRÔLES MOBILES (STYLE PSP) */ | |
| #mobile-controls { | |
| display: none; /* Caché sur PC par défaut, activé via JS si tactile */ | |
| position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; | |
| pointer-events: none; z-index: 200; | |
| } | |
| /* D-PAD */ | |
| .dpad-container { position: absolute; bottom: 40px; left: 30px; width: 140px; height: 140px; pointer-events: auto; } | |
| .dpad-btn { position: absolute; background: rgba(50,50,50,0.8); border: 2px solid #888; border-radius: 5px; } | |
| .dpad-btn:active { background: #f0f; border-color: white; } | |
| .dpad-up { top: 0; left: 45px; width: 50px; height: 45px; border-radius: 5px 5px 0 0; } | |
| .dpad-down { bottom: 0; left: 45px; width: 50px; height: 45px; border-radius: 0 0 5px 5px; } | |
| .dpad-left { top: 45px; left: 0; width: 45px; height: 50px; border-radius: 5px 0 0 5px; } | |
| .dpad-right { top: 45px; right: 0; width: 45px; height: 50px; border-radius: 0 5px 5px 0; } | |
| .dpad-center { top: 45px; left: 45px; width: 50px; height: 50px; background: #333; } | |
| /* BOUTONS ACTION */ | |
| .actions-container { position: absolute; bottom: 30px; right: 30px; width: 180px; height: 140px; pointer-events: auto; } | |
| .action-btn { | |
| position: absolute; width: 50px; height: 50px; | |
| border-radius: 50%; border: 2px solid white; | |
| display: flex; align-items: center; justify-content: center; | |
| font-weight: bold; font-size: 18px; color: white; text-shadow: 1px 1px 0 #000; | |
| box-shadow: 0 4px 0 rgba(0,0,0,0.5); | |
| } | |
| .action-btn:active { transform: translateY(4px); box-shadow: none; background: #ccc; } | |
| .btn-x { bottom: 0; right: 40px; background: rgba(100,100,255,0.8); } /* Croix - Bleu */ | |
| .btn-o { bottom: 40px; right: 0; background: rgba(255,100,100,0.8); } /* Rond - Rouge */ | |
| .btn-tri { bottom: 80px; right: 40px; background: rgba(100,255,100,0.8); } /* Triangle - Vert */ | |
| .btn-sq { bottom: 40px; right: 80px; background: rgba(255,100,255,0.8); } /* Carré - Rose */ | |
| /* SHOULDER BUTTONS */ | |
| .btn-l { top: 20px; left: 20px; width: 60px; height: 30px; border-radius: 10px; background: #555; font-size: 12px; pointer-events: auto; } | |
| .btn-r { top: 20px; right: 20px; width: 60px; height: 30px; border-radius: 10px; background: #555; font-size: 12px; pointer-events: auto; } | |
| /* START / SELECT */ | |
| .sys-btns { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; pointer-events: auto; } | |
| .btn-start, .btn-select { font-size: 10px; color: #aaa; text-transform: uppercase; text-align: center; } | |
| .rect-btn { width: 40px; height: 10px; background: #666; border-radius: 5px; margin: 0 auto 5px auto; } | |
| /* MEDIA QUERIES */ | |
| @media (hover: none) and (pointer: coarse) { | |
| #mobile-controls { display: block; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- LIEN OBLIGATOIRE --> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| <!-- CANEVAS --> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- INTERFACE HUD --> | |
| <div id="ui-layer"> | |
| <div class="hud-top"> | |
| <div class="player-stats"> | |
| <div class="health-bar-container"><div class="health-bar" id="healthBar"></div></div> | |
| <div class="money">$<span id="moneyDisplay">0</span></div> | |
| </div> | |
| <div class="wanted-level" id="wantedStars">★★★★★</div> | |
| </div> | |
| <div class="radio-display" id="radioDisplay"> | |
| <div id="radioStation">RADIO: OFF</div> | |
| <div id="radioSong" style="font-size: 10px; color: #ccc;"></div> | |
| </div> | |
| <div id="mission-text"></div> | |
| </div> | |
| <!-- MENU PAUSE --> | |
| <div id="pause-menu"> | |
| <div class="menu-title">PAUSE</div> | |
| <div class="menu-item" onclick="game.togglePause()">Reprendre</div> | |
| <div class="menu-item" onclick="game.resetGame()">Nouvelle Partie</div> | |
| </div> | |
| <!-- ECRAN CARTE --> | |
| <div id="map-screen"> | |
| <div class="menu-title">CARTE RADAR</div> | |
| <div style="color: #aaa; font-size: 12px; text-align: center; max-width: 300px;"> | |
| Position: <span id="mapCoords">0, 0</span><br> | |
| Zone: <span id="mapZone">Centre-Ville</span> | |
| </div> | |
| <div class="menu-item" style="margin-top: 50px;" onclick="game.toggleMap()">Fermer (Select)</div> | |
| </div> | |
| <!-- CONTRÔLES MOBILES --> | |
| <div id="mobile-controls"> | |
| <div class="btn-l" id="btnL">L</div> | |
| <div class="btn-r" id="btnR">R</div> | |
| <div class="dpad-container"> | |
| <div class="dpad-btn dpad-up" data-key="ArrowUp"></div> | |
| <div class="dpad-btn dpad-down" data-key="ArrowDown"></div> | |
| <div class="dpad-btn dpad-left" data-key="ArrowLeft"></div> | |
| <div class="dpad-btn dpad-right" data-key="ArrowRight"></div> | |
| <div class="dpad-btn dpad-center"></div> | |
| </div> | |
| <div class="actions-container"> | |
| <div class="action-btn btn-tri" data-key="Triangle">△</div> | |
| <div class="action-btn btn-sq" data-key="Square">□</div> | |
| <div class="action-btn btn-x" data-key="Cross">✕</div> | |
| <div class="action-btn btn-o" data-key="Circle">○</div> | |
| </div> | |
| <div class="sys-btns"> | |
| <div class="btn-select" id="btnSelect"><div class="rect-btn"></div>Select</div> | |
| <div class="btn-start" id="btnStart"><div class="rect-btn" style="width: 50px;"></div>Start</div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * MOTEUR DE JEU - VICE CITY WEB | |
| * Architecture: Boucle de jeu unique, Gestion d'état, Rendu Canvas 2D | |
| */ | |
| // --- CONSTANTES & CONFIGURATION --- | |
| const CONFIG = { | |
| TILE_SIZE: 100, | |
| WORLD_WIDTH: 4000, | |
| WORLD_HEIGHT: 4000, | |
| PLAYER_SPEED: 4, | |
| PLAYER_RUN_SPEED: 7, | |
| CAR_SPEED: 12, | |
| CAR_ROTATION_SPEED: 0.06, | |
| COLORS: { | |
| ROAD: '#333', | |
| ROAD_MARKING: '#FFD700', | |
| GRASS: '#2d5a27', | |
| SAND: '#e6c288', | |
| WATER: '#006994', | |
| BUILDING_SIDE: '#556', | |
| BUILDING_ROOF: '#ff00ff', // Style Vice City | |
| BUILDING_ROOF_2: '#00ffff' | |
| } | |
| }; | |
| const RADIO_STATIONS = [ | |
| { name: "Vice Wave FM", song: "Electro Tropical - 1986" }, | |
| { name: "Rock Classics", song: "Turn Up The Radio" }, | |
| { name: "Fever 105", song: "Boogie Wonderland" }, | |
| { name: "Espantoso", song: "Mambo No. 5" } | |
| ]; | |
| // --- GESTION DES ENTRÉES (CLAVIER & TACTILE) --- | |
| class InputHandler { | |
| constructor() { | |
| this.keys = {}; | |
| this.pressed = {}; // Pour les impulsions (une seule fois par appui) | |
| // Mapping clavier PC | |
| this.keyMap = { | |
| 'w': 'ArrowUp', 'W': 'ArrowUp', 'ArrowUp': 'ArrowUp', | |
| 's': 'ArrowDown', 'S': 'ArrowDown', 'ArrowDown': 'ArrowDown', | |
| 'a': 'ArrowLeft', 'A': 'ArrowLeft', 'ArrowLeft': 'ArrowLeft', | |
| 'd': 'ArrowRight', 'D': 'ArrowRight', 'ArrowRight': 'ArrowRight', | |
| ' ': 'Triangle', 'Triangle': 'Triangle', | |
| 'f': 'Square', 'F': 'Square', 'Square': 'Square', | |
| 'e': 'Circle', 'E': 'Circle', 'Circle': 'Circle', | |
| 'Shift': 'Cross', 'Cross': 'Cross', | |
| 'p': 'Start', 'P': 'Start', 'Start': 'Start', | |
| 'm': 'Select', 'M': 'Select', 'Select': 'Select', | |
| 'q': 'L', 'Q': 'L', 'L': 'L', | |
| 'r': 'R', 'R': 'R', 'R': 'R' | |
| }; | |
| window.addEventListener('keydown', (e) => this.onKey(e, true)); | |
| window.addEventListener('keyup', (e) => this.onKey(e, false)); | |
| this.setupTouchControls(); | |
| } | |
| onKey(e, isDown) { | |
| const mapped = this.keyMap[e.key]; | |
| if (mapped) { | |
| this.keys[mapped] = isDown; | |
| if (isDown) this.pressed[mapped] = true; | |
| } | |
| } | |
| setupTouchControls() { | |
| const bindTouch = (selector, key) => { | |
| const el = document.querySelector(selector); | |
| if (!el) return; | |
| el.addEventListener('touchstart', (e) => { e.preventDefault(); this.keys[key] = true; this.pressed[key] = true; }); | |
| el.addEventListener('touchend', (e) => { e.preventDefault(); this.keys[key] = false; }); | |
| }; | |
| bindTouch('.dpad-up', 'ArrowUp'); | |
| bindTouch('.dpad-down', 'ArrowDown'); | |
| bindTouch('.dpad-left', 'ArrowLeft'); | |
| bindTouch('.dpad-right', 'ArrowRight'); | |
| bindTouch('.btn-x', 'Cross'); | |
| bindTouch('.btn-o', 'Circle'); | |
| bindTouch('.btn-tri', 'Triangle'); | |
| bindTouch('.btn-sq', 'Square'); | |
| bindTouch('.btn-l', 'L'); | |
| bindTouch('.btn-r', 'R'); | |
| bindTouch('#btnStart', 'Start'); | |
| bindTouch('#btnSelect', 'Select'); | |
| } | |
| isDown(key) { return !!this.keys[key]; } | |
| isPressed(key) { | |
| if (this.pressed[key]) { | |
| this.pressed[key] = false; | |
| return true; | |
| } | |
| return false; | |
| } | |
| resetPressed() { this.pressed = {}; } | |
| } | |
| // --- CLASSES DU JEU --- | |
| class Camera { | |
| constructor() { | |
| this.x = 0; | |
| this.y = 0; | |
| this.zoom = 1; | |
| } | |
| follow(target, width, height) { | |
| // Interpolation fluide (Lerp) | |
| const targetX = target.x - width / 2; | |
| const targetY = target.y - height / 2; | |
| this.x += (targetX - this.x) * 0.1; | |
| this.y += (targetY - this.y) * 0.1; | |
| // Limites du monde | |
| this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH - width)); | |
| this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT - height)); | |
| } | |
| } | |
| class Entity { | |
| constructor(x, y, color) { | |
| this.x = x; | |
| this.y = y; | |
| this.width = 20; | |
| this.height = 20; | |
| this.color = color; | |
| this.angle = 0; | |
| this.speed = 0; | |
| this.vx = 0; | |
| this.vy = 0; | |
| this.markedForDeletion = false; | |
| } | |
| update() { | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height); | |
| ctx.restore(); | |
| } | |
| getBounds() { | |
| return { x: this.x - this.width/2, y: this.y - this.height/2, w: this.width, h: this.height }; | |
| } | |
| collidesWith(other) { | |
| const a = this.getBounds(); | |
| const b = other.getBounds(); | |
| return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; | |
| } | |
| } | |
| class Vehicle extends Entity { | |
| constructor(x, y, type = 'car') { | |
| super(x, y, type === 'sport' ? '#ff0055' : '#00aa00'); | |
| this.type = type; | |
| this.width = 40; | |
| this.height = 20; | |
| this.maxSpeed = type === 'sport' ? CONFIG.CAR_SPEED * 1.5 : CONFIG.CAR_SPEED; | |
| this.acceleration = 0.2; | |
| this.friction = 0.96; | |
| this.turnSpeed = CONFIG.CAR_ROTATION_SPEED; | |
| this.driver = null; | |
| this.radioIndex = 0; | |
| } | |
| update(input) { | |
| if (this.driver) { | |
| // Contrôles du véhicule | |
| if (input.isDown('Cross')) { // X | |
| this.speed += this.acceleration; | |
| } else if (input.isDown('Square')) { // Frein/Reculer | |
| this.speed -= this.acceleration; | |
| } else { | |
| this.speed *= this.friction; | |
| } | |
| // Limite de vitesse | |
| this.speed = Math.max(-this.maxSpeed / 2, Math.min(this.speed, this.maxSpeed)); | |
| // Direction (seulement si en mouvement) | |
| if (Math.abs(this.speed) > 0.1) { | |
| const dir = this.speed > 0 ? 1 : -1; | |
| if (input.isDown('ArrowLeft')) this.angle -= this.turnSpeed * dir; | |
| if (input.isDown('ArrowRight')) this.angle += this.turnSpeed * dir; | |
| } | |
| this.vx = Math.cos(this.angle) * this.speed; | |
| this.vy = Math.sin(this.angle) * this.speed; | |
| // Sync driver position | |
| this.driver.x = this.x; | |
| this.driver.y = this.y; | |
| this.driver.angle = this.angle; | |
| } else { | |
| this.speed *= 0.9; // Arrêt progressif si vide | |
| this.vx = Math.cos(this.angle) * this.speed; | |
| this.vy = Math.sin(this.angle) * this.speed; | |
| } | |
| // Collisions bords du monde | |
| this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH)); | |
| this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT)); | |
| super.update(); | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| // Corps de la voiture | |
| ctx.fillStyle = this.color; | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = this.color; | |
| ctx.fillRect(-20, -10, 40, 20); | |
| // Pare-brise | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(0, -8, 10, 16); | |
| // Phares | |
| ctx.fillStyle = '#ffeda0'; | |
| ctx.fillRect(18, -9, 4, 4); | |
| ctx.fillRect(18, 5, 4, 4); | |
| ctx.restore(); | |
| } | |
| } | |
| class NPC extends Entity { | |
| constructor(x, y) { | |
| super(x, y, `hsl(${Math.random()*360}, 70%, 50%)`); | |
| this.state = 'idle'; // idle, walk, flee | |
| this.timer = 0; | |
| this.walkSpeed = 1 + Math.random(); | |
| } | |
| update(player) { | |
| const dist = Math.hypot(player.x - this.x, player.y - this.y); | |
| if (this.state === 'idle') { | |
| if (Math.random() < 0.01) this.state = 'walk'; | |
| this.vx = 0; this.vy = 0; | |
| } else if (this.state === 'walk') { | |
| this.timer++; | |
| if (this.timer > 100) { this.state = 'idle'; this.timer = 0; } | |
| this.vx = Math.cos(this.angle) * this.walkSpeed; | |
| this.vy = Math.sin(this.angle) * this.walkSpeed; | |
| } else if (this.state === 'flee') { | |
| this.angle = Math.atan2(this.y - player.y, this.x - player.x); | |
| this.vx = Math.cos(this.angle) * (this.walkSpeed * 2); | |
| this.vy = Math.sin(this.angle) * (this.walkSpeed * 2); | |
| if (dist > 300) this.state = 'idle'; | |
| } | |
| // Collision Player (Fuir si attaqué) | |
| if (player.isAttacking && dist < 50) { | |
| this.state = 'flee'; | |
| // Blesser PNJ ? | |
| } | |
| super.update(); | |
| // Limites simples | |
| this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH)); | |
| this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT)); | |
| } | |
| draw(ctx) { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 8, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Direction | |
| ctx.fillStyle = '#000'; | |
| ctx.fillRect(0, -2, 10, 4); | |
| ctx.restore(); | |
| } | |
| } | |
| class Cop extends Entity { | |
| constructor(x, y) { | |
| super(x, y, '#000088'); | |
| this.width = 24; | |
| this.height = 24; | |
| this.sirenTimer = 0; | |
| } | |
| update(target) { | |
| const angle = Math.atan2(target.y - this.y, target.x - this.x); | |
| this.angle = angle; | |
| const speed = 3.5; // Un peu plus rapide que le joueur | |
| this.vx = Math.cos(angle) * speed; | |
| this.vy = Math.sin(angle) * speed; | |
| super.update(); | |
| } | |
| draw(ctx) { | |
| this.sirenTimer++; | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(-12, -12, 24, 24); | |
| // Gyro | |
| ctx.fillStyle = this.sirenTimer % 20 < 10 ? 'red' : 'blue'; | |
| ctx.fillRect(-5, -15, 10, 5); | |
| ctx.restore(); | |
| } | |
| } | |
| class Player extends Entity { | |
| constructor(x, y) { | |
| super(x, y, '#fff'); | |
| this.width = 16; | |
| this.height = 16; | |
| this.health = 100; | |
| this.money = 0; | |
| this.isDriving = false; | |
| this.currentVehicle = null; | |
| this.isJumping = false; | |
| this.z = 0; // Hauteur de saut | |
| this.isAttacking = false; | |
| this.attackTimer = 0; | |
| } | |
| update(input, vehicles) { | |
| this.isAttacking = false; | |
| // Interaction véhicule | |
| if (input.isPressed('Circle')) { // O | |
| if (this.isDriving) { | |
| // Sortir | |
| this.isDriving = false; | |
| this.x = this.currentVehicle.x + 40; | |
| this.y = this.currentVehicle.y; | |
| this.currentVehicle.driver = null; | |
| this.currentVehicle = null; | |
| game.updateRadio(false); | |
| } else { | |
| // Entrer | |
| vehicles.forEach(v => { | |
| if (!v.driver && Math.hypot(v.x - this.x, v.y - this.y) < 50) { | |
| this.isDriving = true; | |
| this.currentVehicle = v; | |
| v.driver = this; | |
| game.updateRadio(true); | |
| } | |
| }); | |
| } | |
| } | |
| if (this.isDriving && this.currentVehicle) { | |
| // Logique véhicule gérée par le véhicule, mais on met à jour la position joueur | |
| this.currentVehicle.update(input); | |
| this.x = this.currentVehicle.x; | |
| this.y = this.currentVehicle.y; | |
| this.angle = this.currentVehicle.angle; | |
| } else { | |
| // Logique piéton | |
| let speed = input.isDown('Cross') ? CONFIG.PLAYER_RUN_SPEED : CONFIG.PLAYER_SPEED; | |
| let dx = 0; | |
| let dy = 0; | |
| if (input.isDown('ArrowUp')) dy = -1; | |
| if (input.isDown('ArrowDown')) dy = 1; | |
| if (input.isDown('ArrowLeft')) dx = -1; | |
| if (input.isDown('ArrowRight')) dx = 1; | |
| if (dx !== 0 || dy !== 0) { | |
| this.angle = Math.atan2(dy, dx); | |
| this.x += dx * speed; | |
| this.y += dy * speed; | |
| } | |
| // Saut | |
| if (input.isPressed('Triangle') && !this.isJumping) { | |
| this.isJumping = true; | |
| this.vz = 5; | |
| } | |
| // Gravité saut | |
| if (this.isJumping) { | |
| this.z += this.vz; | |
| this.vz -= 0.5; | |
| if (this.z <= 0) { | |
| this.z = 0; | |
| this.isJumping = false; | |
| } | |
| } | |
| // Attaque | |
| if (input.isPressed('Square')) { | |
| this.isAttacking = true; | |
| this.attackTimer = 10; | |
| } | |
| if (this.attackTimer > 0) this.attackTimer--; | |
| } | |
| // Limites | |
| this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH)); | |
| this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT)); | |
| } | |
| draw(ctx) { | |
| if (this.isDriving) return; // Dessiné par le véhicule | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| // Ombre | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.beginPath(); ctx.ellipse(0, 0, 8, 4, 0, 0, Math.PI*2); ctx.fill(); | |
| // Saut (Offset Y) | |
| ctx.translate(0, -this.z); | |
| ctx.rotate(this.angle); | |
| // Corps | |
| ctx.fillStyle = '#eee'; // Chemise hawaïenne stylée | |
| ctx.fillRect(-8, -8, 16, 16); | |
| // Tête | |
| ctx.fillStyle = '#dcb'; | |
| ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI*2); ctx.fill(); | |
| // Arme / Attaque | |
| if (this.attackTimer > 0) { | |
| ctx.fillStyle = '#555'; | |
| ctx.fillRect(8, -2, 15, 4); // Poing américain | |
| } | |
| ctx.restore(); | |
| } | |
| } | |
| // --- MOTEUR PRINCIPAL --- | |
| class Game { | |
| constructor() { | |
| this.canvas = document.getElementById('gameCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.input = new InputHandler(); | |
| this.camera = new Camera(); | |
| this.resize(); | |
| window.addEventListener('resize', () => this.resize()); | |
| this.state = 'PLAYING'; // PLAYING, PAUSED, MAP | |
| this.dayTime = 0; // 0 à 1 | |
| this.daySpeed = 0.0002; | |
| this.initWorld(); | |
| this.loop = this.loop.bind(this); | |
| requestAnimationFrame(this.loop); | |
| } | |
| resize() { | |
| this.canvas.width = window.innerWidth; | |
| this.canvas.height = window.innerHeight; | |
| } | |
| initWorld() { | |
| this.player = new Player(2000, 2000); | |
| this.vehicles = []; | |
| this.npcs = []; | |
| this.cops = []; | |
| this.buildings = []; | |
| this.missions = []; | |
| this.wantedLevel = 0; | |
| this.wantedTimer = 0; | |
| // Génération procédurale simple | |
| // Routes | |
| this.roads = []; | |
| for(let i=0; i<CONFIG.WORLD_WIDTH; i+=400) { | |
| this.roads.push({x: i, y: 0, w: 80, h: CONFIG.WORLD_HEIGHT, vertical: true}); | |
| } | |
| for(let i=0; i<CONFIG.WORLD_HEIGHT; i+=400) { | |
| this.roads.push({x: 0, y: i, w: CONFIG.WORLD_WIDTH, h: 80, vertical: false}); | |
| } | |
| // Bâtiments (Entre les routes) | |
| for(let x=100; x<CONFIG.WORLD_WIDTH; x+=400) { | |
| for(let y=100; y<CONFIG.WORLD_HEIGHT; y+=400) { | |
| if (Math.random() > 0.2) { | |
| const w = 200 + Math.random() * 100; | |
| const h = 200 + Math.random() * 100; | |
| const color = Math.random() > 0.5 ? CONFIG.COLORS.BUILDING_ROOF : CONFIG.COLORS.BUILDING_ROOF_2; | |
| this.buildings.push({x: x, y: y, w: w, h: h, color: color}); | |
| } | |
| } | |
| } | |
| // Véhicules | |
| for(let i=0; i<20; i++) { | |
| this.vehicles.push(new Vehicle(Math.random() * CONFIG.WORLD_WIDTH, Math.random() * CONFIG.WORLD_HEIGHT, Math.random() > 0.8 ? 'sport' : 'car')); | |
| } | |
| // PNJ | |
| for(let i=0; i<50; i++) { | |
| this.npcs.push(new NPC(Math.random() * CONFIG.WORLD_WIDTH, Math.random() * CONFIG.WORLD_HEIGHT)); | |
| } | |
| // Mission initiale | |
| this.startMission(0); | |
| } | |
| startMission(index) { | |
| this.missionIndex = index; | |
| this.missionStep = 0; | |
| this.showMissionText("MISSION: Trouvez une voiture (Bouton O)"); | |
| } | |
| showMissionText(text) { | |
| const el = document.getElementById('mission-text'); | |
| el.innerText = text; | |
| el.style.display = 'block'; | |
| setTimeout(() => el.style.display = 'none', 4000); | |
| } | |
| updateMissions() { | |
| if (this.missionIndex === 0) { | |
| if (this.player.isDriving) { | |
| this.showMissionText("OBJECTIF: Allez à la plage (Sud-Est)"); | |
| this.missionIndex = 1; | |
| } | |
| } else if (this.missionIndex === 1) { | |
| // Plage approx zone sud-est | |
| if (this.player.x > CONFIG.WORLD_WIDTH - 1000 && this.player.y > CONFIG.WORLD_HEIGHT - 1000) { | |
| this.player.money += 100; | |
| this.updateHUD(); | |
| this.showMissionText("SUCCÈS! +$100. Retournez en ville."); | |
| this.missionIndex = 2; | |
| } | |
| } | |
| } | |
| updatePolice() { | |
| // Gestion étoiles | |
| if (this.wantedLevel > 0) { | |
| this.wantedTimer++; | |
| if (this.wantedTimer > 600) { // 10 sec sans crime | |
| this.wantedLevel--; | |
| this.wantedTimer = 0; | |
| } | |
| } | |
| // Spawn police | |
| if (this.wantedLevel > this.cops.length) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const dist = 600; | |
| this.cops.push(new Cop(this.player.x + Math.cos(angle)*dist, this.player.y + Math.sin(angle)*dist)); | |
| } else if (this.wantedLevel < this.cops.length) { | |
| this.cops.pop(); | |
| } | |
| // Update cops | |
| this.cops.forEach(cop => cop.update(this.player)); | |
| // Collision flic -> joueur | |
| this.cops.forEach(cop => { | |
| if (cop.collidesWith(this.player)) { | |
| this.player.health -= 0.5; | |
| this.updateHUD(); | |
| if (this.player.health <= 0) this.gameOver(); | |
| } | |
| }); | |
| } | |
| addWantedLevel() { | |
| if (this.wantedLevel < 5) { | |
| this.wantedLevel++; | |
| this.wantedTimer = 0; | |
| this.updateHUD(); | |
| } | |
| } | |
| updateRadio(active) { | |
| const display = document.getElementById('radioDisplay'); | |
| if (active) { | |
| display.classList.add('active'); | |
| const station = RADIO_STATIONS[Math.floor(Math.random() * RADIO_STATIONS.length)]; | |
| document.getElementById('radioStation').innerText = "RADIO: " + station.name; | |
| document.getElementById('radioSong').innerText = station.song; | |
| } else { | |
| display.classList.remove('active'); | |
| } | |
| } | |
| togglePause() { | |
| this.state = this.state === 'PAUSED' ? 'PLAYING' : 'PAUSED'; | |
| document.getElementById('pause-menu').style.display = this.state === 'PAUSED' ? 'flex' : 'none'; | |
| } | |
| toggleMap() { | |
| this.state = this.state === 'MAP' ? 'PLAYING' : 'MAP'; | |
| document.getElementById('map-screen').style.display = this.state === 'MAP' ? 'flex' : 'none'; | |
| if (this.state === 'MAP') this.updateMap(); | |
| } | |
| updateMap() { | |
| document.getElementById('mapCoords').innerText = `${Math.floor(this.player.x)}, ${Math.floor(this.player.y)}`; | |
| let zone = "Centre-Ville"; | |
| if (this.player.y > CONFIG.WORLD_HEIGHT - 800) zone = "Plage Vice"; | |
| if (this.player.x > CONFIG.WORLD_WIDTH - 800) zone = "Port"; | |
| document.getElementById('mapZone').innerText = zone; | |
| } | |
| updateHUD() { | |
| document.getElementById('healthBar').style.width = this.player.health + '%'; | |
| document.getElementById('moneyDisplay').innerText = |