Mousco commited on
Commit
2cce7d7
·
verified ·
1 Parent(s): 1440318

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +805 -19
index.html CHANGED
@@ -1,19 +1,805 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Vice City Web - PSP Edition</title>
7
+ <style>
8
+ /* IMPORTATION DE POLICE */
9
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Press+Start+2P&display=swap');
10
+
11
+ /* RESET & BASE */
12
+ * { box-sizing: border-box; touch-action: none; user-select: none; -webkit-user-select: none; }
13
+ body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #000; font-family: 'Orbitron', sans-serif; color: white; }
14
+
15
+ /* LIEN ANYCODER */
16
+ .anycoder-link {
17
+ position: absolute;
18
+ top: 10px;
19
+ left: 50%;
20
+ transform: translateX(-50%);
21
+ z-index: 1000;
22
+ color: #0ff;
23
+ text-decoration: none;
24
+ font-size: 12px;
25
+ text-shadow: 0 0 5px #0ff;
26
+ font-family: sans-serif;
27
+ opacity: 0.8;
28
+ }
29
+
30
+ /* CANEVAS DE JEU */
31
+ #gameCanvas { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
32
+
33
+ /* INTERFACE UTILISATEUR (HUD) */
34
+ #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; }
35
+
36
+ .hud-top { display: flex; justify-content: space-between; align-items: flex-start; }
37
+ .player-stats { text-shadow: 2px 2px 0 #000; }
38
+ .health-bar-container { width: 150px; height: 15px; background: #333; border: 2px solid white; margin-bottom: 5px; }
39
+ .health-bar { width: 100%; height: 100%; background: #f00; transition: width 0.2s; }
40
+ .money { color: #0f0; font-size: 20px; font-weight: bold; text-shadow: 2px 2px 0 #000; }
41
+ .wanted-level { color: #FFD700; font-size: 20px; letter-spacing: 5px; text-shadow: 2px 2px 0 #000; }
42
+
43
+ .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; }
44
+ .radio-display.active { display: block; animation: radioPulse 2s infinite; }
45
+
46
+ @keyframes radioPulse { 0% { opacity: 0.7; } 50% { opacity: 1; text-shadow: 0 0 10px #0ff; } 100% { opacity: 0.7; } }
47
+
48
+ /* NOTIFICATIONS DE MISSION */
49
+ #mission-text {
50
+ position: absolute;
51
+ bottom: 20%;
52
+ left: 50%;
53
+ transform: translateX(-50%);
54
+ background: rgba(0,0,0,0.8);
55
+ border-left: 5px solid #f0f;
56
+ padding: 15px;
57
+ max-width: 80%;
58
+ display: none;
59
+ color: white;
60
+ font-size: 14px;
61
+ }
62
+
63
+ /* MENUS (PAUSE, MAP) */
64
+ #pause-menu, #map-screen {
65
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
66
+ background: rgba(0,0,0,0.85);
67
+ display: none; flex-direction: column; align-items: center; justify-content: center;
68
+ z-index: 500; pointer-events: auto;
69
+ }
70
+ #map-screen { background: #001133; }
71
+ .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; }
72
+ .menu-item { color: white; font-size: 18px; margin: 10px 0; cursor: pointer; border: 1px solid transparent; padding: 5px; }
73
+ .menu-item:hover { border: 1px solid #f0f; color: #f0f; }
74
+
75
+ /* CONTRÔLES MOBILES (STYLE PSP) */
76
+ #mobile-controls {
77
+ display: none; /* Caché sur PC par défaut, activé via JS si tactile */
78
+ position: absolute; bottom: 0; left: 0; width: 100%; height: 100%;
79
+ pointer-events: none; z-index: 200;
80
+ }
81
+
82
+ /* D-PAD */
83
+ .dpad-container { position: absolute; bottom: 40px; left: 30px; width: 140px; height: 140px; pointer-events: auto; }
84
+ .dpad-btn { position: absolute; background: rgba(50,50,50,0.8); border: 2px solid #888; border-radius: 5px; }
85
+ .dpad-btn:active { background: #f0f; border-color: white; }
86
+ .dpad-up { top: 0; left: 45px; width: 50px; height: 45px; border-radius: 5px 5px 0 0; }
87
+ .dpad-down { bottom: 0; left: 45px; width: 50px; height: 45px; border-radius: 0 0 5px 5px; }
88
+ .dpad-left { top: 45px; left: 0; width: 45px; height: 50px; border-radius: 5px 0 0 5px; }
89
+ .dpad-right { top: 45px; right: 0; width: 45px; height: 50px; border-radius: 0 5px 5px 0; }
90
+ .dpad-center { top: 45px; left: 45px; width: 50px; height: 50px; background: #333; }
91
+
92
+ /* BOUTONS ACTION */
93
+ .actions-container { position: absolute; bottom: 30px; right: 30px; width: 180px; height: 140px; pointer-events: auto; }
94
+ .action-btn {
95
+ position: absolute; width: 50px; height: 50px;
96
+ border-radius: 50%; border: 2px solid white;
97
+ display: flex; align-items: center; justify-content: center;
98
+ font-weight: bold; font-size: 18px; color: white; text-shadow: 1px 1px 0 #000;
99
+ box-shadow: 0 4px 0 rgba(0,0,0,0.5);
100
+ }
101
+ .action-btn:active { transform: translateY(4px); box-shadow: none; background: #ccc; }
102
+
103
+ .btn-x { bottom: 0; right: 40px; background: rgba(100,100,255,0.8); } /* Croix - Bleu */
104
+ .btn-o { bottom: 40px; right: 0; background: rgba(255,100,100,0.8); } /* Rond - Rouge */
105
+ .btn-tri { bottom: 80px; right: 40px; background: rgba(100,255,100,0.8); } /* Triangle - Vert */
106
+ .btn-sq { bottom: 40px; right: 80px; background: rgba(255,100,255,0.8); } /* Carré - Rose */
107
+
108
+ /* SHOULDER BUTTONS */
109
+ .btn-l { top: 20px; left: 20px; width: 60px; height: 30px; border-radius: 10px; background: #555; font-size: 12px; pointer-events: auto; }
110
+ .btn-r { top: 20px; right: 20px; width: 60px; height: 30px; border-radius: 10px; background: #555; font-size: 12px; pointer-events: auto; }
111
+
112
+ /* START / SELECT */
113
+ .sys-btns { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; pointer-events: auto; }
114
+ .btn-start, .btn-select { font-size: 10px; color: #aaa; text-transform: uppercase; text-align: center; }
115
+ .rect-btn { width: 40px; height: 10px; background: #666; border-radius: 5px; margin: 0 auto 5px auto; }
116
+
117
+ /* MEDIA QUERIES */
118
+ @media (hover: none) and (pointer: coarse) {
119
+ #mobile-controls { display: block; }
120
+ }
121
+ </style>
122
+ </head>
123
+ <body>
124
+
125
+ <!-- LIEN OBLIGATOIRE -->
126
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
127
+
128
+ <!-- CANEVAS -->
129
+ <canvas id="gameCanvas"></canvas>
130
+
131
+ <!-- INTERFACE HUD -->
132
+ <div id="ui-layer">
133
+ <div class="hud-top">
134
+ <div class="player-stats">
135
+ <div class="health-bar-container"><div class="health-bar" id="healthBar"></div></div>
136
+ <div class="money">$<span id="moneyDisplay">0</span></div>
137
+ </div>
138
+ <div class="wanted-level" id="wantedStars">★★★★★</div>
139
+ </div>
140
+ <div class="radio-display" id="radioDisplay">
141
+ <div id="radioStation">RADIO: OFF</div>
142
+ <div id="radioSong" style="font-size: 10px; color: #ccc;"></div>
143
+ </div>
144
+ <div id="mission-text"></div>
145
+ </div>
146
+
147
+ <!-- MENU PAUSE -->
148
+ <div id="pause-menu">
149
+ <div class="menu-title">PAUSE</div>
150
+ <div class="menu-item" onclick="game.togglePause()">Reprendre</div>
151
+ <div class="menu-item" onclick="game.resetGame()">Nouvelle Partie</div>
152
+ </div>
153
+
154
+ <!-- ECRAN CARTE -->
155
+ <div id="map-screen">
156
+ <div class="menu-title">CARTE RADAR</div>
157
+ <div style="color: #aaa; font-size: 12px; text-align: center; max-width: 300px;">
158
+ Position: <span id="mapCoords">0, 0</span><br>
159
+ Zone: <span id="mapZone">Centre-Ville</span>
160
+ </div>
161
+ <div class="menu-item" style="margin-top: 50px;" onclick="game.toggleMap()">Fermer (Select)</div>
162
+ </div>
163
+
164
+ <!-- CONTRÔLES MOBILES -->
165
+ <div id="mobile-controls">
166
+ <div class="btn-l" id="btnL">L</div>
167
+ <div class="btn-r" id="btnR">R</div>
168
+
169
+ <div class="dpad-container">
170
+ <div class="dpad-btn dpad-up" data-key="ArrowUp"></div>
171
+ <div class="dpad-btn dpad-down" data-key="ArrowDown"></div>
172
+ <div class="dpad-btn dpad-left" data-key="ArrowLeft"></div>
173
+ <div class="dpad-btn dpad-right" data-key="ArrowRight"></div>
174
+ <div class="dpad-btn dpad-center"></div>
175
+ </div>
176
+
177
+ <div class="actions-container">
178
+ <div class="action-btn btn-tri" data-key="Triangle">△</div>
179
+ <div class="action-btn btn-sq" data-key="Square">□</div>
180
+ <div class="action-btn btn-x" data-key="Cross">✕</div>
181
+ <div class="action-btn btn-o" data-key="Circle">○</div>
182
+ </div>
183
+
184
+ <div class="sys-btns">
185
+ <div class="btn-select" id="btnSelect"><div class="rect-btn"></div>Select</div>
186
+ <div class="btn-start" id="btnStart"><div class="rect-btn" style="width: 50px;"></div>Start</div>
187
+ </div>
188
+ </div>
189
+
190
+ <script>
191
+ /**
192
+ * MOTEUR DE JEU - VICE CITY WEB
193
+ * Architecture: Boucle de jeu unique, Gestion d'état, Rendu Canvas 2D
194
+ */
195
+
196
+ // --- CONSTANTES & CONFIGURATION ---
197
+ const CONFIG = {
198
+ TILE_SIZE: 100,
199
+ WORLD_WIDTH: 4000,
200
+ WORLD_HEIGHT: 4000,
201
+ PLAYER_SPEED: 4,
202
+ PLAYER_RUN_SPEED: 7,
203
+ CAR_SPEED: 12,
204
+ CAR_ROTATION_SPEED: 0.06,
205
+ COLORS: {
206
+ ROAD: '#333',
207
+ ROAD_MARKING: '#FFD700',
208
+ GRASS: '#2d5a27',
209
+ SAND: '#e6c288',
210
+ WATER: '#006994',
211
+ BUILDING_SIDE: '#556',
212
+ BUILDING_ROOF: '#ff00ff', // Style Vice City
213
+ BUILDING_ROOF_2: '#00ffff'
214
+ }
215
+ };
216
+
217
+ const RADIO_STATIONS = [
218
+ { name: "Vice Wave FM", song: "Electro Tropical - 1986" },
219
+ { name: "Rock Classics", song: "Turn Up The Radio" },
220
+ { name: "Fever 105", song: "Boogie Wonderland" },
221
+ { name: "Espantoso", song: "Mambo No. 5" }
222
+ ];
223
+
224
+ // --- GESTION DES ENTRÉES (CLAVIER & TACTILE) ---
225
+ class InputHandler {
226
+ constructor() {
227
+ this.keys = {};
228
+ this.pressed = {}; // Pour les impulsions (une seule fois par appui)
229
+
230
+ // Mapping clavier PC
231
+ this.keyMap = {
232
+ 'w': 'ArrowUp', 'W': 'ArrowUp', 'ArrowUp': 'ArrowUp',
233
+ 's': 'ArrowDown', 'S': 'ArrowDown', 'ArrowDown': 'ArrowDown',
234
+ 'a': 'ArrowLeft', 'A': 'ArrowLeft', 'ArrowLeft': 'ArrowLeft',
235
+ 'd': 'ArrowRight', 'D': 'ArrowRight', 'ArrowRight': 'ArrowRight',
236
+ ' ': 'Triangle', 'Triangle': 'Triangle',
237
+ 'f': 'Square', 'F': 'Square', 'Square': 'Square',
238
+ 'e': 'Circle', 'E': 'Circle', 'Circle': 'Circle',
239
+ 'Shift': 'Cross', 'Cross': 'Cross',
240
+ 'p': 'Start', 'P': 'Start', 'Start': 'Start',
241
+ 'm': 'Select', 'M': 'Select', 'Select': 'Select',
242
+ 'q': 'L', 'Q': 'L', 'L': 'L',
243
+ 'r': 'R', 'R': 'R', 'R': 'R'
244
+ };
245
+
246
+ window.addEventListener('keydown', (e) => this.onKey(e, true));
247
+ window.addEventListener('keyup', (e) => this.onKey(e, false));
248
+ this.setupTouchControls();
249
+ }
250
+
251
+ onKey(e, isDown) {
252
+ const mapped = this.keyMap[e.key];
253
+ if (mapped) {
254
+ this.keys[mapped] = isDown;
255
+ if (isDown) this.pressed[mapped] = true;
256
+ }
257
+ }
258
+
259
+ setupTouchControls() {
260
+ const bindTouch = (selector, key) => {
261
+ const el = document.querySelector(selector);
262
+ if (!el) return;
263
+ el.addEventListener('touchstart', (e) => { e.preventDefault(); this.keys[key] = true; this.pressed[key] = true; });
264
+ el.addEventListener('touchend', (e) => { e.preventDefault(); this.keys[key] = false; });
265
+ };
266
+
267
+ bindTouch('.dpad-up', 'ArrowUp');
268
+ bindTouch('.dpad-down', 'ArrowDown');
269
+ bindTouch('.dpad-left', 'ArrowLeft');
270
+ bindTouch('.dpad-right', 'ArrowRight');
271
+ bindTouch('.btn-x', 'Cross');
272
+ bindTouch('.btn-o', 'Circle');
273
+ bindTouch('.btn-tri', 'Triangle');
274
+ bindTouch('.btn-sq', 'Square');
275
+ bindTouch('.btn-l', 'L');
276
+ bindTouch('.btn-r', 'R');
277
+ bindTouch('#btnStart', 'Start');
278
+ bindTouch('#btnSelect', 'Select');
279
+ }
280
+
281
+ isDown(key) { return !!this.keys[key]; }
282
+ isPressed(key) {
283
+ if (this.pressed[key]) {
284
+ this.pressed[key] = false;
285
+ return true;
286
+ }
287
+ return false;
288
+ }
289
+ resetPressed() { this.pressed = {}; }
290
+ }
291
+
292
+ // --- CLASSES DU JEU ---
293
+
294
+ class Camera {
295
+ constructor() {
296
+ this.x = 0;
297
+ this.y = 0;
298
+ this.zoom = 1;
299
+ }
300
+ follow(target, width, height) {
301
+ // Interpolation fluide (Lerp)
302
+ const targetX = target.x - width / 2;
303
+ const targetY = target.y - height / 2;
304
+ this.x += (targetX - this.x) * 0.1;
305
+ this.y += (targetY - this.y) * 0.1;
306
+
307
+ // Limites du monde
308
+ this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH - width));
309
+ this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT - height));
310
+ }
311
+ }
312
+
313
+ class Entity {
314
+ constructor(x, y, color) {
315
+ this.x = x;
316
+ this.y = y;
317
+ this.width = 20;
318
+ this.height = 20;
319
+ this.color = color;
320
+ this.angle = 0;
321
+ this.speed = 0;
322
+ this.vx = 0;
323
+ this.vy = 0;
324
+ this.markedForDeletion = false;
325
+ }
326
+ update() {
327
+ this.x += this.vx;
328
+ this.y += this.vy;
329
+ }
330
+ draw(ctx) {
331
+ ctx.save();
332
+ ctx.translate(this.x, this.y);
333
+ ctx.rotate(this.angle);
334
+ ctx.fillStyle = this.color;
335
+ ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
336
+ ctx.restore();
337
+ }
338
+ getBounds() {
339
+ return { x: this.x - this.width/2, y: this.y - this.height/2, w: this.width, h: this.height };
340
+ }
341
+ collidesWith(other) {
342
+ const a = this.getBounds();
343
+ const b = other.getBounds();
344
+ 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;
345
+ }
346
+ }
347
+
348
+ class Vehicle extends Entity {
349
+ constructor(x, y, type = 'car') {
350
+ super(x, y, type === 'sport' ? '#ff0055' : '#00aa00');
351
+ this.type = type;
352
+ this.width = 40;
353
+ this.height = 20;
354
+ this.maxSpeed = type === 'sport' ? CONFIG.CAR_SPEED * 1.5 : CONFIG.CAR_SPEED;
355
+ this.acceleration = 0.2;
356
+ this.friction = 0.96;
357
+ this.turnSpeed = CONFIG.CAR_ROTATION_SPEED;
358
+ this.driver = null;
359
+ this.radioIndex = 0;
360
+ }
361
+ update(input) {
362
+ if (this.driver) {
363
+ // Contrôles du véhicule
364
+ if (input.isDown('Cross')) { // X
365
+ this.speed += this.acceleration;
366
+ } else if (input.isDown('Square')) { // Frein/Reculer
367
+ this.speed -= this.acceleration;
368
+ } else {
369
+ this.speed *= this.friction;
370
+ }
371
+
372
+ // Limite de vitesse
373
+ this.speed = Math.max(-this.maxSpeed / 2, Math.min(this.speed, this.maxSpeed));
374
+
375
+ // Direction (seulement si en mouvement)
376
+ if (Math.abs(this.speed) > 0.1) {
377
+ const dir = this.speed > 0 ? 1 : -1;
378
+ if (input.isDown('ArrowLeft')) this.angle -= this.turnSpeed * dir;
379
+ if (input.isDown('ArrowRight')) this.angle += this.turnSpeed * dir;
380
+ }
381
+
382
+ this.vx = Math.cos(this.angle) * this.speed;
383
+ this.vy = Math.sin(this.angle) * this.speed;
384
+
385
+ // Sync driver position
386
+ this.driver.x = this.x;
387
+ this.driver.y = this.y;
388
+ this.driver.angle = this.angle;
389
+ } else {
390
+ this.speed *= 0.9; // Arrêt progressif si vide
391
+ this.vx = Math.cos(this.angle) * this.speed;
392
+ this.vy = Math.sin(this.angle) * this.speed;
393
+ }
394
+
395
+ // Collisions bords du monde
396
+ this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH));
397
+ this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT));
398
+
399
+ super.update();
400
+ }
401
+
402
+ draw(ctx) {
403
+ ctx.save();
404
+ ctx.translate(this.x, this.y);
405
+ ctx.rotate(this.angle);
406
+
407
+ // Corps de la voiture
408
+ ctx.fillStyle = this.color;
409
+ ctx.shadowBlur = 10;
410
+ ctx.shadowColor = this.color;
411
+ ctx.fillRect(-20, -10, 40, 20);
412
+
413
+ // Pare-brise
414
+ ctx.fillStyle = '#000';
415
+ ctx.fillRect(0, -8, 10, 16);
416
+
417
+ // Phares
418
+ ctx.fillStyle = '#ffeda0';
419
+ ctx.fillRect(18, -9, 4, 4);
420
+ ctx.fillRect(18, 5, 4, 4);
421
+
422
+ ctx.restore();
423
+ }
424
+ }
425
+
426
+ class NPC extends Entity {
427
+ constructor(x, y) {
428
+ super(x, y, `hsl(${Math.random()*360}, 70%, 50%)`);
429
+ this.state = 'idle'; // idle, walk, flee
430
+ this.timer = 0;
431
+ this.walkSpeed = 1 + Math.random();
432
+ }
433
+ update(player) {
434
+ const dist = Math.hypot(player.x - this.x, player.y - this.y);
435
+
436
+ if (this.state === 'idle') {
437
+ if (Math.random() < 0.01) this.state = 'walk';
438
+ this.vx = 0; this.vy = 0;
439
+ } else if (this.state === 'walk') {
440
+ this.timer++;
441
+ if (this.timer > 100) { this.state = 'idle'; this.timer = 0; }
442
+ this.vx = Math.cos(this.angle) * this.walkSpeed;
443
+ this.vy = Math.sin(this.angle) * this.walkSpeed;
444
+ } else if (this.state === 'flee') {
445
+ this.angle = Math.atan2(this.y - player.y, this.x - player.x);
446
+ this.vx = Math.cos(this.angle) * (this.walkSpeed * 2);
447
+ this.vy = Math.sin(this.angle) * (this.walkSpeed * 2);
448
+ if (dist > 300) this.state = 'idle';
449
+ }
450
+
451
+ // Collision Player (Fuir si attaqué)
452
+ if (player.isAttacking && dist < 50) {
453
+ this.state = 'flee';
454
+ // Blesser PNJ ?
455
+ }
456
+
457
+ super.update();
458
+ // Limites simples
459
+ this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH));
460
+ this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT));
461
+ }
462
+ draw(ctx) {
463
+ ctx.save();
464
+ ctx.translate(this.x, this.y);
465
+ ctx.rotate(this.angle);
466
+ ctx.fillStyle = this.color;
467
+ ctx.beginPath();
468
+ ctx.arc(0, 0, 8, 0, Math.PI * 2);
469
+ ctx.fill();
470
+ // Direction
471
+ ctx.fillStyle = '#000';
472
+ ctx.fillRect(0, -2, 10, 4);
473
+ ctx.restore();
474
+ }
475
+ }
476
+
477
+ class Cop extends Entity {
478
+ constructor(x, y) {
479
+ super(x, y, '#000088');
480
+ this.width = 24;
481
+ this.height = 24;
482
+ this.sirenTimer = 0;
483
+ }
484
+ update(target) {
485
+ const angle = Math.atan2(target.y - this.y, target.x - this.x);
486
+ this.angle = angle;
487
+ const speed = 3.5; // Un peu plus rapide que le joueur
488
+ this.vx = Math.cos(angle) * speed;
489
+ this.vy = Math.sin(angle) * speed;
490
+ super.update();
491
+ }
492
+ draw(ctx) {
493
+ this.sirenTimer++;
494
+ ctx.save();
495
+ ctx.translate(this.x, this.y);
496
+ ctx.rotate(this.angle);
497
+ ctx.fillStyle = this.color;
498
+ ctx.fillRect(-12, -12, 24, 24);
499
+ // Gyro
500
+ ctx.fillStyle = this.sirenTimer % 20 < 10 ? 'red' : 'blue';
501
+ ctx.fillRect(-5, -15, 10, 5);
502
+ ctx.restore();
503
+ }
504
+ }
505
+
506
+ class Player extends Entity {
507
+ constructor(x, y) {
508
+ super(x, y, '#fff');
509
+ this.width = 16;
510
+ this.height = 16;
511
+ this.health = 100;
512
+ this.money = 0;
513
+ this.isDriving = false;
514
+ this.currentVehicle = null;
515
+ this.isJumping = false;
516
+ this.z = 0; // Hauteur de saut
517
+ this.isAttacking = false;
518
+ this.attackTimer = 0;
519
+ }
520
+ update(input, vehicles) {
521
+ this.isAttacking = false;
522
+
523
+ // Interaction véhicule
524
+ if (input.isPressed('Circle')) { // O
525
+ if (this.isDriving) {
526
+ // Sortir
527
+ this.isDriving = false;
528
+ this.x = this.currentVehicle.x + 40;
529
+ this.y = this.currentVehicle.y;
530
+ this.currentVehicle.driver = null;
531
+ this.currentVehicle = null;
532
+ game.updateRadio(false);
533
+ } else {
534
+ // Entrer
535
+ vehicles.forEach(v => {
536
+ if (!v.driver && Math.hypot(v.x - this.x, v.y - this.y) < 50) {
537
+ this.isDriving = true;
538
+ this.currentVehicle = v;
539
+ v.driver = this;
540
+ game.updateRadio(true);
541
+ }
542
+ });
543
+ }
544
+ }
545
+
546
+ if (this.isDriving && this.currentVehicle) {
547
+ // Logique véhicule gérée par le véhicule, mais on met à jour la position joueur
548
+ this.currentVehicle.update(input);
549
+ this.x = this.currentVehicle.x;
550
+ this.y = this.currentVehicle.y;
551
+ this.angle = this.currentVehicle.angle;
552
+ } else {
553
+ // Logique piéton
554
+ let speed = input.isDown('Cross') ? CONFIG.PLAYER_RUN_SPEED : CONFIG.PLAYER_SPEED;
555
+ let dx = 0;
556
+ let dy = 0;
557
+
558
+ if (input.isDown('ArrowUp')) dy = -1;
559
+ if (input.isDown('ArrowDown')) dy = 1;
560
+ if (input.isDown('ArrowLeft')) dx = -1;
561
+ if (input.isDown('ArrowRight')) dx = 1;
562
+
563
+ if (dx !== 0 || dy !== 0) {
564
+ this.angle = Math.atan2(dy, dx);
565
+ this.x += dx * speed;
566
+ this.y += dy * speed;
567
+ }
568
+
569
+ // Saut
570
+ if (input.isPressed('Triangle') && !this.isJumping) {
571
+ this.isJumping = true;
572
+ this.vz = 5;
573
+ }
574
+
575
+ // Gravité saut
576
+ if (this.isJumping) {
577
+ this.z += this.vz;
578
+ this.vz -= 0.5;
579
+ if (this.z <= 0) {
580
+ this.z = 0;
581
+ this.isJumping = false;
582
+ }
583
+ }
584
+
585
+ // Attaque
586
+ if (input.isPressed('Square')) {
587
+ this.isAttacking = true;
588
+ this.attackTimer = 10;
589
+ }
590
+ if (this.attackTimer > 0) this.attackTimer--;
591
+ }
592
+
593
+ // Limites
594
+ this.x = Math.max(0, Math.min(this.x, CONFIG.WORLD_WIDTH));
595
+ this.y = Math.max(0, Math.min(this.y, CONFIG.WORLD_HEIGHT));
596
+ }
597
+
598
+ draw(ctx) {
599
+ if (this.isDriving) return; // Dessiné par le véhicule
600
+
601
+ ctx.save();
602
+ ctx.translate(this.x, this.y);
603
+
604
+ // Ombre
605
+ ctx.fillStyle = 'rgba(0,0,0,0.3)';
606
+ ctx.beginPath(); ctx.ellipse(0, 0, 8, 4, 0, 0, Math.PI*2); ctx.fill();
607
+
608
+ // Saut (Offset Y)
609
+ ctx.translate(0, -this.z);
610
+ ctx.rotate(this.angle);
611
+
612
+ // Corps
613
+ ctx.fillStyle = '#eee'; // Chemise hawaïenne stylée
614
+ ctx.fillRect(-8, -8, 16, 16);
615
+
616
+ // Tête
617
+ ctx.fillStyle = '#dcb';
618
+ ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI*2); ctx.fill();
619
+
620
+ // Arme / Attaque
621
+ if (this.attackTimer > 0) {
622
+ ctx.fillStyle = '#555';
623
+ ctx.fillRect(8, -2, 15, 4); // Poing américain
624
+ }
625
+
626
+ ctx.restore();
627
+ }
628
+ }
629
+
630
+ // --- MOTEUR PRINCIPAL ---
631
+
632
+ class Game {
633
+ constructor() {
634
+ this.canvas = document.getElementById('gameCanvas');
635
+ this.ctx = this.canvas.getContext('2d');
636
+ this.input = new InputHandler();
637
+ this.camera = new Camera();
638
+
639
+ this.resize();
640
+ window.addEventListener('resize', () => this.resize());
641
+
642
+ this.state = 'PLAYING'; // PLAYING, PAUSED, MAP
643
+ this.dayTime = 0; // 0 à 1
644
+ this.daySpeed = 0.0002;
645
+
646
+ this.initWorld();
647
+ this.loop = this.loop.bind(this);
648
+ requestAnimationFrame(this.loop);
649
+ }
650
+
651
+ resize() {
652
+ this.canvas.width = window.innerWidth;
653
+ this.canvas.height = window.innerHeight;
654
+ }
655
+
656
+ initWorld() {
657
+ this.player = new Player(2000, 2000);
658
+ this.vehicles = [];
659
+ this.npcs = [];
660
+ this.cops = [];
661
+ this.buildings = [];
662
+ this.missions = [];
663
+ this.wantedLevel = 0;
664
+ this.wantedTimer = 0;
665
+
666
+ // Génération procédurale simple
667
+ // Routes
668
+ this.roads = [];
669
+ for(let i=0; i<CONFIG.WORLD_WIDTH; i+=400) {
670
+ this.roads.push({x: i, y: 0, w: 80, h: CONFIG.WORLD_HEIGHT, vertical: true});
671
+ }
672
+ for(let i=0; i<CONFIG.WORLD_HEIGHT; i+=400) {
673
+ this.roads.push({x: 0, y: i, w: CONFIG.WORLD_WIDTH, h: 80, vertical: false});
674
+ }
675
+
676
+ // Bâtiments (Entre les routes)
677
+ for(let x=100; x<CONFIG.WORLD_WIDTH; x+=400) {
678
+ for(let y=100; y<CONFIG.WORLD_HEIGHT; y+=400) {
679
+ if (Math.random() > 0.2) {
680
+ const w = 200 + Math.random() * 100;
681
+ const h = 200 + Math.random() * 100;
682
+ const color = Math.random() > 0.5 ? CONFIG.COLORS.BUILDING_ROOF : CONFIG.COLORS.BUILDING_ROOF_2;
683
+ this.buildings.push({x: x, y: y, w: w, h: h, color: color});
684
+ }
685
+ }
686
+ }
687
+
688
+ // Véhicules
689
+ for(let i=0; i<20; i++) {
690
+ this.vehicles.push(new Vehicle(Math.random() * CONFIG.WORLD_WIDTH, Math.random() * CONFIG.WORLD_HEIGHT, Math.random() > 0.8 ? 'sport' : 'car'));
691
+ }
692
+
693
+ // PNJ
694
+ for(let i=0; i<50; i++) {
695
+ this.npcs.push(new NPC(Math.random() * CONFIG.WORLD_WIDTH, Math.random() * CONFIG.WORLD_HEIGHT));
696
+ }
697
+
698
+ // Mission initiale
699
+ this.startMission(0);
700
+ }
701
+
702
+ startMission(index) {
703
+ this.missionIndex = index;
704
+ this.missionStep = 0;
705
+ this.showMissionText("MISSION: Trouvez une voiture (Bouton O)");
706
+ }
707
+
708
+ showMissionText(text) {
709
+ const el = document.getElementById('mission-text');
710
+ el.innerText = text;
711
+ el.style.display = 'block';
712
+ setTimeout(() => el.style.display = 'none', 4000);
713
+ }
714
+
715
+ updateMissions() {
716
+ if (this.missionIndex === 0) {
717
+ if (this.player.isDriving) {
718
+ this.showMissionText("OBJECTIF: Allez à la plage (Sud-Est)");
719
+ this.missionIndex = 1;
720
+ }
721
+ } else if (this.missionIndex === 1) {
722
+ // Plage approx zone sud-est
723
+ if (this.player.x > CONFIG.WORLD_WIDTH - 1000 && this.player.y > CONFIG.WORLD_HEIGHT - 1000) {
724
+ this.player.money += 100;
725
+ this.updateHUD();
726
+ this.showMissionText("SUCCÈS! +$100. Retournez en ville.");
727
+ this.missionIndex = 2;
728
+ }
729
+ }
730
+ }
731
+
732
+ updatePolice() {
733
+ // Gestion étoiles
734
+ if (this.wantedLevel > 0) {
735
+ this.wantedTimer++;
736
+ if (this.wantedTimer > 600) { // 10 sec sans crime
737
+ this.wantedLevel--;
738
+ this.wantedTimer = 0;
739
+ }
740
+ }
741
+
742
+ // Spawn police
743
+ if (this.wantedLevel > this.cops.length) {
744
+ const angle = Math.random() * Math.PI * 2;
745
+ const dist = 600;
746
+ this.cops.push(new Cop(this.player.x + Math.cos(angle)*dist, this.player.y + Math.sin(angle)*dist));
747
+ } else if (this.wantedLevel < this.cops.length) {
748
+ this.cops.pop();
749
+ }
750
+
751
+ // Update cops
752
+ this.cops.forEach(cop => cop.update(this.player));
753
+
754
+ // Collision flic -> joueur
755
+ this.cops.forEach(cop => {
756
+ if (cop.collidesWith(this.player)) {
757
+ this.player.health -= 0.5;
758
+ this.updateHUD();
759
+ if (this.player.health <= 0) this.gameOver();
760
+ }
761
+ });
762
+ }
763
+
764
+ addWantedLevel() {
765
+ if (this.wantedLevel < 5) {
766
+ this.wantedLevel++;
767
+ this.wantedTimer = 0;
768
+ this.updateHUD();
769
+ }
770
+ }
771
+
772
+ updateRadio(active) {
773
+ const display = document.getElementById('radioDisplay');
774
+ if (active) {
775
+ display.classList.add('active');
776
+ const station = RADIO_STATIONS[Math.floor(Math.random() * RADIO_STATIONS.length)];
777
+ document.getElementById('radioStation').innerText = "RADIO: " + station.name;
778
+ document.getElementById('radioSong').innerText = station.song;
779
+ } else {
780
+ display.classList.remove('active');
781
+ }
782
+ }
783
+
784
+ togglePause() {
785
+ this.state = this.state === 'PAUSED' ? 'PLAYING' : 'PAUSED';
786
+ document.getElementById('pause-menu').style.display = this.state === 'PAUSED' ? 'flex' : 'none';
787
+ }
788
+
789
+ toggleMap() {
790
+ this.state = this.state === 'MAP' ? 'PLAYING' : 'MAP';
791
+ document.getElementById('map-screen').style.display = this.state === 'MAP' ? 'flex' : 'none';
792
+ if (this.state === 'MAP') this.updateMap();
793
+ }
794
+
795
+ updateMap() {
796
+ document.getElementById('mapCoords').innerText = `${Math.floor(this.player.x)}, ${Math.floor(this.player.y)}`;
797
+ let zone = "Centre-Ville";
798
+ if (this.player.y > CONFIG.WORLD_HEIGHT - 800) zone = "Plage Vice";
799
+ if (this.player.x > CONFIG.WORLD_WIDTH - 800) zone = "Port";
800
+ document.getElementById('mapZone').innerText = zone;
801
+ }
802
+
803
+ updateHUD() {
804
+ document.getElementById('healthBar').style.width = this.player.health + '%';
805
+ document.getElementById('moneyDisplay').innerText =