Lukeetah commited on
Commit
5b92433
·
verified ·
1 Parent(s): e1ed9de

Upload 9 files

Browse files
Files changed (1) hide show
  1. templates/index.html +533 -454
templates/index.html CHANGED
@@ -1,574 +1,653 @@
1
  <!DOCTYPE html>
2
  <html lang="es">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>CHORIZO: DEFINITIVE EDITION</title>
7
  <style>
8
- /* GTA SAN ANDREAS STYLE HUD */
9
- @import url('https://fonts.googleapis.com/css2?family=Pricedown&display=swap'); /* Fake Pricedown if avail, else fallback */
10
- @font-face {
11
- font-family: 'Pricedown';
12
- src: url('https://db.onlinewebfonts.com/t/048a9c37b420f86460334812a8069d2f.woff2') format('woff2');
 
 
 
13
  }
14
 
15
- body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial', sans-serif; }
16
- #game-container { position: relative; width: 100vw; height: 100vh; }
17
- canvas { display: block; width: 100%; height: 100%; }
 
 
18
 
19
- /* HUD ELEMENTS */
20
- #hud {
 
 
 
 
 
 
 
21
  position: absolute;
22
- top: 20px; right: 20px;
23
- width: 150px;
 
 
 
24
  pointer-events: none;
 
 
25
  }
26
 
27
- .hud-bar-container {
28
- background: rgba(0,0,0,0.5);
29
- border: 2px solid #000;
30
- margin-bottom: 5px;
31
- height: 15px;
32
- position: relative;
33
  }
34
-
35
- .hud-bar { height: 100%; width: 100%; }
36
- #health-bar { background: #b62fac; } /* Old school SA Health Pink/Red */
37
- #armor-bar { background: #cfcfcf; height: 10px; margin-bottom: 2px; width: 0%; display:none;}
38
 
39
- #money {
40
- font-family: 'Pricedown', 'Impact', sans-serif;
41
- color: #3d8f3d; /* Dark Green */
42
- font-size: 32px;
43
- text-align: right;
44
- text-shadow: 2px 2px 0 #000;
45
- margin-bottom: 5px;
46
  }
47
 
48
- #weapon-icon {
49
- width: 60px; height: 60px;
50
- background: rgba(0,0,0,0.3);
51
- border: 2px solid #000;
52
- border-radius: 50%;
53
- margin-left: auto;
54
- display: flex; align-items: center; justify-content: center;
55
- color: #fff; font-size: 24px;
56
  }
57
 
58
- #wanted-level {
 
 
 
 
 
 
 
59
  display: flex;
60
- justify-content: flex-end;
61
- margin-top: 5px;
62
  }
63
- .star { color: #555; font-size: 20px; margin-left: 2px; }
64
- .star.active { color: #fff; text-shadow: 0 0 5px #fff; }
65
 
66
- #zone-name {
 
 
 
 
 
 
 
 
 
67
  position: absolute;
68
- bottom: 30px; right: 30px;
69
- font-family: 'Pricedown', 'Impact', sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
70
  color: #fff;
71
- font-size: 36px;
72
- text-shadow: 2px 2px 0 #000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  text-transform: uppercase;
74
  }
75
 
76
- /* MISSION TEXT / SUBTITLES */
77
- #mission-text {
 
 
 
 
 
78
  position: absolute;
79
- bottom: 100px; width: 100%;
80
- text-align: center;
81
- font-family: 'Arial', sans-serif;
82
- font-weight: bold;
83
- font-size: 20px;
84
- color: #fff;
85
- text-shadow: 1px 1px 2px #000;
86
  pointer-events: none;
87
  display: none;
88
- padding: 0 20px;
89
- box-sizing: border-box;
90
  }
91
- .subtitle {
92
- background: rgba(0,0,0,0.6);
93
- padding: 5px 10px;
94
- border-radius: 4px;
95
- display: inline-block;
 
 
 
 
96
  }
97
 
98
- /* MENU */
99
- #menu {
100
- position: absolute; top:0; left:0; width:100%; height:100%;
101
- background: url('https://images.unsplash.com/photo-1620021156647-7521852e3995?q=80&w=1920&auto=format&fit=crop') no-repeat center center/cover;
102
- display: flex; flex-direction: column; align-items: center; justify-content: center;
103
- z-index: 100;
104
  }
105
- #menu::after {
106
- content:''; position:absolute; top:0; left:0; width:100%; height:100%;
107
- background: rgba(0,0,0,0.6); z-index: -1;
108
- }
109
- h1 {
110
- font-family: 'Pricedown', 'Impact', sans-serif;
111
- font-size: 100px; color: #fea500;
112
- text-shadow: 4px 4px 0 #000; margin: 0;
113
- letter-spacing: 2px;
114
- }
115
- .menu-btn {
116
- font-family: 'Pricedown', 'Impact', sans-serif;
117
- font-size: 40px; color: #fff;
118
- background: none; border: none;
119
- cursor: pointer; margin: 10px;
120
- text-shadow: 2px 2px 0 #000;
121
- transition: transform 0.2s, color 0.2s;
122
- }
123
- .menu-btn:hover { transform: scale(1.1); color: #fea500; }
124
-
125
- /* VINYETTE & SCANLINES for "Human Reality" / Gritty feel */
126
- #screensfx {
127
- position: absolute; top:0; left:0; width:100%; height:100%;
128
- background: radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,10,20,0.4) 100%);
129
- pointer-events: none; z-index: 50;
130
- }
131
- .scanline {
132
- width: 100%; height: 100px;
133
- background: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.05) 50%, rgba(0,0,0,0) 100%);
134
- opacity: 0.1;
135
- position: absolute; bottom: 100%;
136
- animation: scanline 10s linear infinite;
137
- pointer-events: none;
138
  }
139
- @keyframes scanline { 0% { bottom: 100%; } 100% { bottom: -100%; } }
140
 
141
- #controls-hint {
142
- position: absolute; top: 20px; left: 20px;
143
- color: rgba(255,255,255,0.5); font-family: monospace;
 
 
 
 
144
  }
145
  </style>
146
  </head>
147
- <body>
148
 
 
149
  <div id="game-container">
150
- <!-- 3D Canvas injected by Three.js -->
151
-
152
- <div id="screensfx"><div class="scanline"></div></div>
 
 
153
 
154
  <!-- MENU -->
155
  <div id="menu">
156
- <h1>CHORIZO</h1>
157
- <h2 style="font-family:'Pricedown'; color:#fff; text-shadow:1px 1px 0 #000; margin-top:-10px;">DEFINITIVE EDITION</h2>
158
- <button class="menu-btn" id="start-btn">JUGAR</button>
159
- <div style="color:#aaa; position:absolute; bottom:20px; font-size:12px;">ESTRELLO GAMES - BUENOS AIRES 2025</div>
 
160
  </div>
161
 
162
  <!-- HUD -->
163
- <div id="hud" style="display:none;">
164
- <div id="money">$00000000</div>
165
- <div class="hud-bar-container">
166
- <div id="health-bar" class="hud-bar"></div>
167
  </div>
168
- <div id="weapon-icon">👊</div>
169
- <div id="wanted-level">
170
- <span class="star">★</span><span class="star">★</span><span class="star">★</span><span class="star">★</span><span class="star">★</span>
171
  </div>
 
172
  </div>
173
-
174
- <div id="zone-name" style="display:none;">VILLA 31</div>
175
-
176
- <div id="mission-text">
177
- <div id="subtitle-box" class="subtitle"></div>
178
- </div>
179
-
180
- <div id="controls-hint">WASD: Mover | SHIFT: Correr | ESPACIO: Saltar | CLICK: Interactuar</div>
181
  </div>
182
 
183
- <!-- AUDIO SYSTEM -->
184
  <script>
185
  const AUDIO = {
186
  ctx: null,
187
- init: function() {
188
- try {
189
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
190
- this.ctx = new AudioContext();
191
- } catch(e) { console.warn('Web Audio API not supported'); }
192
- },
193
- playTone: function(freq, type, duration) {
194
- if(!this.ctx) return;
195
- const osc = this.ctx.createOscillator();
196
- const gain = this.ctx.createGain();
197
- osc.type = type;
198
- osc.frequency.value = freq;
199
- osc.connect(gain);
200
- gain.connect(this.ctx.destination);
201
- osc.start();
202
- gain.gain.exponentialRampToValueAtTime(0.00001, this.ctx.currentTime + duration);
203
- osc.stop(this.ctx.currentTime + duration);
204
- },
205
- playSelect: function() { this.playTone(400, 'square', 0.1); },
206
- playStart: function() { this.playTone(600, 'sawtooth', 0.5); }
207
- };
208
- </script>
209
-
210
- <!-- STORY DATA (Merged Renacer + Chorizo) -->
211
- <script>
212
- const STORY = {
213
- player: {
214
- name: "Martín",
215
- money: 250,
216
- respect: 10,
217
- hp: 100,
218
- wanted: 0
219
- },
220
- missions: [
221
- {
222
- id: "intro",
223
- title: "El Amanecer de los Que Menos Tienen",
224
- npc: "Don Carmelo",
225
- triggerZone: { x: 50, z: 50, r: 5 }, // Near start
226
- dialogue: [
227
- { speaker: "Don Carmelo", text: "Martín, pibe. Llegaste temprano. Eso me gusta." },
228
- { speaker: "Martín", text: "¿Tiene el trabajo del que hablamos, Don Carmelo?" },
229
- { speaker: "Don Carmelo", text: "Sí. Pero escuchame bien: esto no es repartir pizzas." },
230
- { speaker: "Don Carmelo", text: "El Diputado necesita limpiar unas paredes en Microcentro. 'Limpieza Ideológica' le dicen." },
231
- { speaker: "Don Carmelo", text: "Andá y sacá esos carteles zurdos. Que no quede ni uno." }
232
- ],
233
- objective: "Destruí 5 carteles en Microcentro",
234
- targetZone: "MICROCENTRO",
235
- reward: 500
236
- },
237
- {
238
- id: "dolar_blue",
239
- title: "Dólar Blue",
240
- npc: "El Financista",
241
- triggerZone: { x: -200, z: -200, r: 5 }, // Puerto Madero
242
- dialogue: [
243
- { speaker: "Financista", text: "Ah, vos sos el enviado de Carmelo. Tenés cara de pobre, perfecto." },
244
- { speaker: "Martín", text: "Dame el paquete y no rompas las bolas." },
245
- { speaker: "Financista", text: "Epa, qué carácter. Tomá. Son verdes. Llevalos a la cueva en La Boca sin que te agarre la cana." }
246
- ],
247
- objective: "Llevá el paquete a La Boca",
248
- targetZone: "LA BOCA",
249
- reward: 1200
250
  }
251
- ]
252
  };
253
-
254
- let currentMissionIndex = -1;
255
- let missionStage = 0; // 0: not started, 1: dialogue, 2: active, 3: completed
256
  </script>
257
 
258
- <!-- THREE.JS ENGINE -->
259
  <script type="module">
260
  import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
261
- // Simple first person controls or orbit controls? We'll write a custom 3rd person controller.
262
 
263
- // --- GAME VARIABLES ---
264
- let scene, camera, renderer;
265
- let player, playerMesh;
266
- let terrain;
267
- let buildings = [];
268
  let keyState = {};
269
  let gameActive = false;
270
-
 
 
 
 
 
271
  const ZONES = [
272
- { name: "VILLA 31", x: 0, z: 0, color: 0x8b4513, density: 0.8 },
273
- { name: "MICROCENTRO", x: 200, z: 200, color: 0x555555, density: 0.6 },
274
- { name: "PUERTO MADERO", x: -200, z: -200, color: 0xadd8e6, density: 0.3 }, // Glassy
275
- { name: "LA BOCA", x: 200, z: -200, color: 0xffd700, density: 0.5 }
276
  ];
277
 
278
- // --- INIT ---
279
  function init() {
280
- // Audio init
281
  document.getElementById('start-btn').addEventListener('click', () => {
282
  AUDIO.init();
283
- AUDIO.playStart();
284
  document.getElementById('menu').style.display = 'none';
285
  document.getElementById('hud').style.display = 'block';
286
- document.getElementById('zone-name').style.display = 'block';
287
- gameActive = true;
288
-
289
- // Lock pointer
290
  document.body.requestPointerLock();
 
291
  });
292
 
293
- // Scene
294
  scene = new THREE.Scene();
295
- scene.background = new THREE.Color(0x050510); // Dark night sky
296
- scene.fog = new THREE.FogExp2(0x050510, 0.0025); // Smoggy atmosphere
297
 
298
- // Renderer
299
- renderer = new THREE.WebGLRenderer({ antialias: true });
300
  renderer.setSize(window.innerWidth, window.innerHeight);
301
  renderer.shadowMap.enabled = true;
 
302
  document.getElementById('game-container').appendChild(renderer.domElement);
303
 
304
- // Camera
305
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
306
- camera.position.set(0, 5, -10);
307
-
308
- // Lights
309
- const ambientLight = new THREE.HemisphereLight(0x4040a0, 0x202040, 0.5); // Blue-ish night ambiance
310
- scene.add(ambientLight);
311
-
312
- const moonLight = new THREE.DirectionalLight(0xffffff, 0.8);
313
- moonLight.position.set(50, 100, 50);
314
- moonLight.castShadow = true;
315
- moonLight.shadow.camera.left = -100;
316
- moonLight.shadow.camera.right = 100;
317
- moonLight.shadow.camera.top = 100;
318
- moonLight.shadow.camera.bottom = -100;
319
- moonLight.shadow.mapSize.width = 2048;
320
- moonLight.shadow.mapSize.height = 2048;
321
- scene.add(moonLight);
322
-
323
- // --- WORLD GENERATION ---
324
- generateWorld();
325
-
326
- // --- PLAYER ---
327
  createPlayer();
328
-
329
- // Events
330
- window.addEventListener('resize', onWindowResize, false);
331
- document.addEventListener('keydown', (e) => keyState[e.code] = true);
332
- document.addEventListener('keyup', (e) => keyState[e.code] = false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  document.addEventListener('mousemove', onMouseMove);
334
 
335
  animate();
336
  }
337
 
338
- function createTexture(type) {
339
- const canvas = document.createElement('canvas');
340
- canvas.width = 256; canvas.height = 256;
341
- const ctx = canvas.getContext('2d');
342
-
343
- // Base
344
- ctx.fillStyle = type === 'asphalt' ? '#333' :
345
- type === 'concrete' ? '#666' :
346
- type === 'brick' ? '#843131' : '#223344'; // Glass
347
- ctx.fillRect(0,0,256,256);
348
-
349
- // Noise/Grit
350
- for(let i=0; i<5000; i++) {
351
- ctx.fillStyle = Math.random() > 0.5 ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.1)';
352
- ctx.fillRect(Math.random()*256, Math.random()*256, 2, 2);
353
- }
354
-
355
- // Details
356
- if(type === 'brick') {
357
- ctx.strokeStyle = 'rgba(0,0,0,0.3)';
358
- ctx.lineWidth = 2;
359
- for(let y=0; y<256; y+=32) {
360
- ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(256,y); ctx.stroke();
361
- for(let x=0; x<256; x+=64) {
362
- let off = (y/32)%2 === 0 ? 0 : 32;
363
- ctx.beginPath(); ctx.moveTo(x+off, y); ctx.lineTo(x+off, y+32); ctx.stroke();
364
- }
 
 
365
  }
366
- }
367
-
368
- if(type === 'window') {
369
- ctx.fillStyle = '#111';
370
- ctx.fillRect(0,0,256,256);
371
- // Lit windows
372
- for(let y=10; y<250; y+=40) {
373
- for(let x=10; x<250; x+=40) {
374
- if(Math.random() > 0.7) {
375
- ctx.fillStyle = Math.random() > 0.9 ? '#ffccaa' : '#ffffaa';
376
- ctx.fillRect(x,y,30,30);
377
- } else {
378
- ctx.fillStyle = '#222';
379
- ctx.fillRect(x,y,30,30);
380
- }
381
- }
382
  }
383
- }
384
 
385
- const tex = new THREE.CanvasTexture(canvas);
386
- tex.wrapS = THREE.RepeatWrapping;
387
- tex.wrapT = THREE.RepeatWrapping;
388
- return tex;
389
- }
390
 
391
- const TEX = {
392
- asphalt: createTexture('asphalt'),
393
- concrete: createTexture('concrete'),
394
- brick: createTexture('brick'),
395
- window: createTexture('window')
396
- };
397
- TEX.asphalt.repeat.set(100,100);
 
 
398
 
399
- function generateWorld() {
 
400
  // Ground
401
- const planeGeo = new THREE.PlaneGeometry(2000, 2000);
402
- const planeMat = new THREE.MeshStandardMaterial({ map: TEX.asphalt, roughness: 0.9 });
403
- const ground = new THREE.Mesh(planeGeo, planeMat);
404
- ground.rotation.x = -Math.PI / 2;
405
- ground.receiveShadow = true;
406
- scene.add(ground);
407
-
408
- // Buildings
409
- ZONES.forEach((zone, idx) => {
410
- const count = idx === 0 ? 150 : 50; // More slums
411
- const range = 300;
412
-
413
- for(let i=0; i<count; i++) {
414
- // Random pos inside zone
415
- const x = zone.x + (Math.random()-0.5) * range;
416
- const z = zone.z + (Math.random()-0.5) * range;
417
-
418
- // Don't spawn on top of each other (simple check omitted for speed, relying on density)
419
-
420
- // Size
421
- const w = 10 + Math.random() * 20;
422
- const d = 10 + Math.random() * 20;
423
- let h = 10 + Math.random() * 20;
424
-
425
- let material = new THREE.MeshStandardMaterial({ map: TEX.concrete });
426
-
427
- if(zone.name === "PUERTO MADERO") {
428
- h = 50 + Math.random() * 100; // Sky scrapers
429
- material = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.8 }); // Glassy
430
- } else if(zone.name === "LA BOCA") {
431
- material = new THREE.MeshStandardMaterial({ color: Math.random() > 0.5 ? 0xff0000 : (Math.random()>0.5 ? 0xffff00 : 0x0000ff), map: TEX.brick });
432
- } else if(zone.name === "VILLA 31") {
433
- h = 5 + Math.random() * 10;
434
- material = new THREE.MeshStandardMaterial({ map: TEX.brick, color: 0xaa8877 });
435
- } else { // Microcentro
436
- h = 30 + Math.random() * 50;
437
- material = new THREE.MeshStandardMaterial({ map: TEX.window });
438
- }
439
-
440
- const geo = new THREE.BoxGeometry(w, h, d);
441
- const mesh = new THREE.Mesh(geo, material);
442
- mesh.position.set(x, h/2, z);
443
- mesh.castShadow = true;
444
- mesh.receiveShadow = true;
445
- scene.add(mesh);
446
- buildings.push(mesh);
447
  }
448
  });
 
449
 
450
- // Street Lights (Neon)
451
- for(let i=0; i<50; i++) {
452
- const light = new THREE.PointLight(Math.random() > 0.5 ? 0xff00ff : 0x00ffff, 1, 50);
453
- light.position.set((Math.random()-0.5)*600, 10, (Math.random()-0.5)*600);
454
- scene.add(light);
455
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  }
457
 
458
- function createPlayer() {
459
- // Simple capsule placeholder
460
- const geo = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
461
- const mat = new THREE.MeshStandardMaterial({ color: 0xffa500 });
462
- playerMesh = new THREE.Mesh(geo, mat);
463
- playerMesh.position.y = 1;
464
- playerMesh.castShadow = true;
465
- scene.add(playerMesh);
466
-
467
- player = {
468
- mesh: playerMesh,
469
- speed: 0,
470
- rot: 0,
471
- velocity: new THREE.Vector3()
472
- };
473
  }
474
 
475
- // Camera Logic
476
- let camDist = 10;
477
- let camPitch = 0.5;
478
- let camYaw = 0;
479
-
480
- function updateCamera() {
481
- // Camera follows player
482
- const targetX = player.mesh.position.x - Math.sin(camYaw) * camDist;
483
- const targetZ = player.mesh.position.z - Math.cos(camYaw) * camDist;
484
- const targetY = player.mesh.position.y + camDist * 0.5; // + Pitch logic if needed
485
-
486
- // Smooth lerp
487
- camera.position.x += (targetX - camera.position.x) * 0.1;
488
- camera.position.z += (targetZ - camera.position.z) * 0.1;
489
- camera.position.y += (targetY - camera.position.y) * 0.1;
490
-
491
- camera.lookAt(player.mesh.position.x, player.mesh.position.y + 1, player.mesh.position.z);
492
- }
493
-
494
- function onMouseMove(e) {
495
- if(!gameActive) return;
496
- // Simple mouse look
497
- camYaw -= e.movementX * 0.005;
498
- // camPitch -= e.movementY * 0.005;
499
- }
500
-
501
- function updatePlayerPhysics() {
502
- // Movement independent of camera yaw (WASD = directions relative to camera or world? -> GTA is relative to camera)
503
- // For now, simpler: W moves forward in player's facing direction. Mouse rotates camera.
504
- // Actually nice GTA controls: W moves forward relative to CAMERA facing.
505
-
506
- const moveSpeed = keyState['ShiftLeft'] ? 0.3 : 0.15;
507
- let dx = 0;
508
- let dz = 0;
509
-
510
- if(keyState['KeyW']) { dx -= Math.sin(camYaw); dz -= Math.cos(camYaw); }
511
- if(keyState['KeyS']) { dx += Math.sin(camYaw); dz += Math.cos(camYaw); }
512
- if(keyState['KeyA']) { dx -= Math.sin(camYaw + Math.PI/2); dz -= Math.cos(camYaw + Math.PI/2); }
513
- if(keyState['KeyD']) { dx += Math.sin(camYaw + Math.PI/2); dz += Math.cos(camYaw + Math.PI/2); }
514
-
515
- if(dx !== 0 || dz !== 0) {
516
- player.mesh.position.x += dx * moveSpeed;
517
- player.mesh.position.z += dz * moveSpeed;
518
-
519
- // Rotate player mesh to face movement
520
- player.mesh.rotation.y = Math.atan2(dx, dz) + Math.PI; // Face away from camera when moving forward? No, face movement vector.
521
- player.rot = Math.atan2(dx, dz);
522
  }
 
523
 
524
- // Simple collision with ground (y=1)
525
- player.mesh.position.y = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
- // Zone Check
528
- checkZone();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  }
530
 
531
- function checkZone() {
532
- let currentZone = "DESCONOCIDO";
533
- let minDist = 9999;
534
- ZONES.forEach(z => {
535
- const dist = Math.sqrt((player.mesh.position.x - z.x)**2 + (player.mesh.position.z - z.z)**2);
536
- if(dist < 150) { // Zone radius roughly
537
- if(dist < minDist) {
538
- minDist = dist;
539
- currentZone = z.name;
540
- }
 
 
 
 
541
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  });
543
- document.getElementById('zone-name').textContent = currentZone;
 
 
 
 
 
 
 
 
 
544
  }
545
 
546
- function updateGameLogic() {
547
- // MISSION TRIGGERS
548
- // Check distance to active mission start
549
- // ... (Basic stub for now)
550
- }
551
 
552
  function animate() {
553
  requestAnimationFrame(animate);
554
- if(gameActive) {
555
- updatePlayerPhysics();
556
- updateCamera();
557
- updateGameLogic();
 
558
  renderer.render(scene, camera);
559
  }
560
  }
561
 
562
- // Handle resize
563
- function onWindowResize() {
564
- camera.aspect = window.innerWidth / window.innerHeight;
565
- camera.updateProjectionMatrix();
566
- renderer.setSize(window.innerWidth, window.innerHeight);
567
- }
568
-
569
- // Init on load
570
  init();
571
-
572
  </script>
573
  </body>
 
574
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="es">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
7
+ <title>CHORIZO: VICE LOPEZ STORIES</title>
8
  <style>
9
+ /* FONTS & UI */
10
+ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@800&family=Roboto+Mono:wght@500&display=swap');
11
+
12
+ body {
13
+ margin: 0;
14
+ overflow: hidden;
15
+ background: #000;
16
+ font-family: 'Montserrat', sans-serif;
17
  }
18
 
19
+ #game-container {
20
+ position: relative;
21
+ width: 100vw;
22
+ height: 100vh;
23
+ }
24
 
25
+ canvas {
26
+ display: block;
27
+ width: 100%;
28
+ height: 100%;
29
+ outline: none;
30
+ }
31
+
32
+ /* ADS / REALISM UI */
33
+ #crosshair {
34
  position: absolute;
35
+ top: 50%;
36
+ left: 50%;
37
+ width: 20px;
38
+ height: 20px;
39
+ transform: translate(-50%, -50%);
40
  pointer-events: none;
41
+ display: none;
42
+ /* Show when aiming */
43
  }
44
 
45
+ .ch-line {
46
+ position: absolute;
47
+ background: #fff;
 
 
 
48
  }
 
 
 
 
49
 
50
+ .ch-h {
51
+ width: 100%;
52
+ height: 2px;
53
+ top: 9px;
 
 
 
54
  }
55
 
56
+ .ch-v {
57
+ height: 100%;
58
+ width: 2px;
59
+ left: 9px;
 
 
 
 
60
  }
61
 
62
+ #ammo-box {
63
+ position: absolute;
64
+ top: 60px;
65
+ right: 40px;
66
+ color: #ccc;
67
+ font-family: 'Roboto Mono', monospace;
68
+ font-size: 24px;
69
+ text-shadow: 1px 1px 0 #000;
70
  display: flex;
71
+ align-items: center;
 
72
  }
 
 
73
 
74
+ .bullet-icon {
75
+ width: 15px;
76
+ height: 25px;
77
+ background: #DAA520;
78
+ margin-right: 5px;
79
+ border: 1px solid #000;
80
+ }
81
+
82
+ /* MENU GTA STYLE */
83
+ #menu {
84
  position: absolute;
85
+ top: 0;
86
+ left: 0;
87
+ width: 100%;
88
+ height: 100%;
89
+ background: linear-gradient(135deg, #FF8C00 0%, #000 70%);
90
+ display: flex;
91
+ flex-direction: column;
92
+ align-items: center;
93
+ justify-content: center;
94
+ z-index: 100;
95
+ }
96
+
97
+ h1.gta-title {
98
+ font-size: 100px;
99
  color: #fff;
100
+ line-height: 0.8;
101
+ text-shadow: 4px 4px 0 #000;
102
+ margin: 0;
103
+ letter-spacing: -2px;
104
+ font-style: italic;
105
+ }
106
+
107
+ h2.gta-sub {
108
+ font-size: 40px;
109
+ color: #000;
110
+ background: #DAA520;
111
+ padding: 0 10px;
112
+ margin-top: -10px;
113
+ font-style: italic;
114
+ transform: rotate(-2deg);
115
+ }
116
+
117
+ .btn {
118
+ background: #000;
119
+ color: #DAA520;
120
+ border: 2px solid #DAA520;
121
+ padding: 15px 50px;
122
+ font-size: 24px;
123
+ font-weight: 800;
124
+ cursor: pointer;
125
+ margin-top: 50px;
126
+ transition: all 0.2s;
127
  text-transform: uppercase;
128
  }
129
 
130
+ .btn:hover {
131
+ background: #DAA520;
132
+ color: #000;
133
+ }
134
+
135
+ /* HUD */
136
+ #hud {
137
  position: absolute;
138
+ top: 0;
139
+ left: 0;
140
+ width: 100%;
141
+ height: 100%;
 
 
 
142
  pointer-events: none;
143
  display: none;
 
 
144
  }
145
+
146
+ #money-box {
147
+ position: absolute;
148
+ top: 30px;
149
+ right: 40px;
150
+ color: #85bb65;
151
+ font-family: 'Roboto Mono', monospace;
152
+ font-size: 32px;
153
+ text-shadow: 2px 2px 0 #000;
154
  }
155
 
156
+ #zone-box {
157
+ position: absolute;
158
+ bottom: 40px;
159
+ right: 40px;
160
+ text-align: right;
 
161
  }
162
+
163
+ #zone-name {
164
+ color: #fff;
165
+ font-size: 40px;
166
+ text-transform: uppercase;
167
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
168
+ }
169
+
170
+ #zone-sub {
171
+ color: #DAA520;
172
+ font-size: 18px;
173
+ font-weight: bold;
174
+ }
175
+
176
+ #interact-prompt {
177
+ position: absolute;
178
+ bottom: 20%;
179
+ left: 50%;
180
+ transform: translateX(-50%);
181
+ color: #000;
182
+ background: #DAA520;
183
+ padding: 10px 20px;
184
+ font-weight: bold;
185
+ font-size: 16px;
186
+ border: 2px solid #fff;
187
+ display: none;
 
 
 
 
 
 
 
188
  }
 
189
 
190
+ #controls {
191
+ position: absolute;
192
+ bottom: 10px;
193
+ left: 10px;
194
+ color: #555;
195
+ font-size: 12px;
196
+ font-family: monospace;
197
  }
198
  </style>
199
  </head>
 
200
 
201
+ <body>
202
  <div id="game-container">
203
+ <!-- CROSSHAIR -->
204
+ <div id="crosshair">
205
+ <div class="ch-line ch-h"></div>
206
+ <div class="ch-line ch-v"></div>
207
+ </div>
208
 
209
  <!-- MENU -->
210
  <div id="menu">
211
+ <h1 class="gta-title">CHORIZO</h1>
212
+ <h2 class="gta-sub">VICENTE LOPEZ STORIES</h2>
213
+ <button class="btn" id="start-btn">JUGAR</button>
214
+ <div id="controls">WASD: Mover | SHIFT: Correr | F: Auto | E: Hablar | CLICK DER: Apuntar | CLICK IZQ:
215
+ Disparar</div>
216
  </div>
217
 
218
  <!-- HUD -->
219
+ <div id="hud">
220
+ <div id="money-box">$00002500</div>
221
+ <div id="ammo-box">
222
+ <div class="bullet-icon"></div> 17/90
223
  </div>
224
+ <div id="zone-box">
225
+ <div id="zone-name">OLIVOS</div>
226
+ <div id="zone-sub">PARTIDO DE VICENTE LOPEZ</div>
227
  </div>
228
+ <div id="interact-prompt">[E] ACCION</div>
229
  </div>
 
 
 
 
 
 
 
 
230
  </div>
231
 
232
+ <!-- AUDIO -->
233
  <script>
234
  const AUDIO = {
235
  ctx: null,
236
+ init: () => { try { window.AudioContext = window.AudioContext || window.webkitAudioContext; AUDIO.ctx = new AudioContext(); } catch (e) { } },
237
+ play: (type) => {
238
+ if (!AUDIO.ctx) return;
239
+ const o = AUDIO.ctx.createOscillator(), g = AUDIO.ctx.createGain();
240
+ o.connect(g); g.connect(AUDIO.ctx.destination);
241
+ if (type === 'shoot') {
242
+ o.frequency.setValueAtTime(150, AUDIO.ctx.currentTime);
243
+ o.frequency.exponentialRampToValueAtTime(0.01, AUDIO.ctx.currentTime + 0.1);
244
+ g.gain.setValueAtTime(0.5, AUDIO.ctx.currentTime);
245
+ o.type = 'sawtooth'; o.start(); o.stop(AUDIO.ctx.currentTime + 0.1);
246
+ }
247
+ if (type === 'car') {
248
+ o.frequency.setValueAtTime(50, AUDIO.ctx.currentTime);
249
+ o.frequency.linearRampToValueAtTime(30, AUDIO.ctx.currentTime + 0.3);
250
+ g.gain.setValueAtTime(0.1, AUDIO.ctx.currentTime);
251
+ o.type = 'square'; o.start(); o.stop(AUDIO.ctx.currentTime + 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  }
253
+ }
254
  };
 
 
 
255
  </script>
256
 
257
+ <!-- ENGINE -->
258
  <script type="module">
259
  import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
 
260
 
261
+ let scene, camera, renderer, clock;
262
+ let player, playerGroup, playerParts = {};
263
+ let cars = [], npcs = [], buildings = [], collisionBoxes = [];
 
 
264
  let keyState = {};
265
  let gameActive = false;
266
+
267
+ // GAMEPLAY STATE
268
+ let inCar = null; // Car object if driving
269
+ let isAiming = false;
270
+ let lastShot = 0;
271
+
272
  const ZONES = [
273
+ { name: "OLIVOS", x: 0, z: 0, type: "residential" },
274
+ { name: "LA LUCILA", x: 400, z: 0, type: "posh" },
275
+ { name: "VICENTE LOPEZ", x: -400, z: 0, type: "center" },
276
+ { name: "RIO DE LA PLATA", x: 0, z: -400, type: "water" }
277
  ];
278
 
 
279
  function init() {
 
280
  document.getElementById('start-btn').addEventListener('click', () => {
281
  AUDIO.init();
 
282
  document.getElementById('menu').style.display = 'none';
283
  document.getElementById('hud').style.display = 'block';
 
 
 
 
284
  document.body.requestPointerLock();
285
+ gameActive = true;
286
  });
287
 
288
+ // SCENE
289
  scene = new THREE.Scene();
290
+ scene.background = new THREE.Color(0x87CEEB); // Blue Sky (Daytime Realism)
291
+ scene.fog = new THREE.Fog(0x87CEEB, 50, 400); // Atmospheric haze
292
 
293
+ camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
294
+ renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
295
  renderer.setSize(window.innerWidth, window.innerHeight);
296
  renderer.shadowMap.enabled = true;
297
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
298
  document.getElementById('game-container').appendChild(renderer.domElement);
299
 
300
+ // LIGHTING (Daylight)
301
+ const ambi = new THREE.AmbientLight(0xffffff, 0.6);
302
+ scene.add(ambi);
303
+ const sun = new THREE.DirectionalLight(0xffffee, 1.0);
304
+ sun.position.set(100, 200, 50);
305
+ sun.castShadow = true;
306
+ sun.shadow.mapSize.set(2048, 2048);
307
+ sun.shadow.camera.left = -300; sun.shadow.camera.right = 300;
308
+ sun.shadow.camera.top = 300; sun.shadow.camera.bottom = -300;
309
+ scene.add(sun);
310
+
311
+ // ASSETS
312
+ genTextures();
313
+ buildWorld();
 
 
 
 
 
 
 
 
 
314
  createPlayer();
315
+ spawnVehicles(10);
316
+ spawnNPCs(50);
317
+
318
+ // EVENTS
319
+ clock = new THREE.Clock();
320
+ window.addEventListener('resize', onResize);
321
+ document.addEventListener('keydown', e => {
322
+ keyState[e.code] = true;
323
+ if (e.code === 'KeyF') handleEnterExitCar();
324
+ if (e.code === 'KeyE') handleInteract();
325
+ });
326
+ document.addEventListener('keyup', e => keyState[e.code] = false);
327
+ document.addEventListener('mousedown', e => {
328
+ if (e.button === 2) { isAiming = true; document.getElementById('crosshair').style.display = 'block'; }
329
+ if (e.button === 0 && isAiming) handleShoot();
330
+ });
331
+ document.addEventListener('mouseup', e => {
332
+ if (e.button === 2) { isAiming = false; document.getElementById('crosshair').style.display = 'none'; }
333
+ });
334
  document.addEventListener('mousemove', onMouseMove);
335
 
336
  animate();
337
  }
338
 
339
+ // --- REALISTIC TEXTURES (Ads & Shops) ---
340
+ const TEX = {};
341
+ function genTextures() {
342
+ const create = (type) => {
343
+ const c = document.createElement('canvas'); c.width = 512; c.height = 512;
344
+ const x = c.getContext('2d');
345
+
346
+ // BASE
347
+ x.fillStyle = type === 'road' ? '#333' : type === 'grass' ? '#3a5a3a' : '#ccc';
348
+ x.fillRect(0, 0, 512, 512);
349
+
350
+ if (type === 'shop') {
351
+ // Facade Upper
352
+ x.fillStyle = Math.random() > 0.5 ? '#b8b8b8' : '#e0d0c0';
353
+ x.fillRect(0, 0, 512, 350);
354
+ // Windows
355
+ x.fillStyle = '#223';
356
+ for (let i = 0; i < 4; i++) for (let j = 0; j < 3; j++) x.fillRect(30 + i * 120, 30 + j * 100, 80, 80);
357
+
358
+ // SHOP FRONT (Lower)
359
+ const hue = Math.floor(Math.random() * 360);
360
+ x.fillStyle = `hsl(${hue}, 60%, 40%)`;
361
+ x.fillRect(0, 350, 512, 162);
362
+ // Logo/Text
363
+ x.fillStyle = '#fff';
364
+ x.font = 'bold 50px Arial';
365
+ x.textAlign = 'center';
366
+ const names = ["KIOSCO", "FARMACITY", "DIA%", "CARREFOUR", "PIZZERIA", "HAVANNA", "RAPIPAGO"];
367
+ x.fillText(names[Math.floor(Math.random() * names.length)], 256, 420);
368
  }
369
+
370
+ if (type === 'ad') {
371
+ x.fillStyle = '#fff'; x.fillRect(0, 0, 512, 512);
372
+ const ads = [
373
+ { bg: '#f00', txt: "TOME COCA" },
374
+ { bg: '#00f', txt: "VOTE X" },
375
+ { bg: '#fa0', txt: "MERCADOLIBRE" },
376
+ { bg: '#222', txt: "NETFLIX" }
377
+ ];
378
+ const ad = ads[Math.floor(Math.random() * ads.length)];
379
+ x.fillStyle = ad.bg;
380
+ x.fillRect(10, 10, 492, 492);
381
+ x.fillStyle = '#fff'; x.font = 'bold 60px Arial'; x.textAlign = 'center';
382
+ x.fillText(ad.txt, 256, 256);
 
 
383
  }
 
384
 
385
+ if (type === 'road') {
386
+ x.strokeStyle = '#fff'; x.lineWidth = 5; x.setLineDash([40, 40]);
387
+ x.beginPath(); x.moveTo(256, 0); x.lineTo(256, 512); x.stroke();
388
+ }
 
389
 
390
+ const t = new THREE.CanvasTexture(c);
391
+ t.wrapS = t.wrapT = THREE.RepeatWrapping;
392
+ return t;
393
+ };
394
+ TEX.shop = create('shop');
395
+ TEX.ad = create('ad');
396
+ TEX.road = create('road');
397
+ TEX.grass = create('grass');
398
+ }
399
 
400
+ // --- WORLD ---
401
+ function buildWorld() {
402
  // Ground
403
+ const g = new THREE.Mesh(new THREE.PlaneGeometry(2000, 2000), new THREE.MeshStandardMaterial({ map: TEX.road }));
404
+ g.rotation.x = -Math.PI / 2; g.receiveShadow = true;
405
+ TEX.road.repeat.set(80, 80);
406
+ scene.add(g);
407
+
408
+ // River (Rio de la Plata)
409
+ const w = new THREE.Mesh(new THREE.PlaneGeometry(2000, 500), new THREE.MeshStandardMaterial({ color: 0x4466aa, roughness: 0.1, metalness: 0.5 }));
410
+ w.rotation.x = -Math.PI / 2;
411
+ w.position.set(0, 0.1, -600);
412
+ scene.add(w);
413
+
414
+ // Buildings (Shops & Apts)
415
+ const geo = new THREE.BoxGeometry(1, 1, 1);
416
+ ZONES.forEach(z => {
417
+ if (z.type === 'water') return;
418
+ for (let i = 0; i < 60; i++) {
419
+ const w = 15 + Math.random() * 15;
420
+ const h = 10 + Math.random() * 40;
421
+ const d = 15 + Math.random() * 15;
422
+ const x = z.x + (Math.random() - 0.5) * 300;
423
+ const zpos = z.z + (Math.random() - 0.5) * 300;
424
+
425
+ // Material: Shop front or Ad?
426
+ const mat = new THREE.MeshStandardMaterial({
427
+ map: Math.random() > 0.3 ? TEX.shop : TEX.ad
428
+ });
429
+
430
+ const b = new THREE.Mesh(geo, mat);
431
+ b.position.set(x, h / 2, zpos);
432
+ b.scale.set(w, h, d);
433
+ b.castShadow = true; b.receiveShadow = true;
434
+ scene.add(b);
435
+ collisionBoxes.push(new THREE.Box3().setFromObject(b));
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  }
437
  });
438
+ }
439
 
440
+ // --- VEHICLES (The "Golcito") ---
441
+ function createCar(x, z) {
442
+ const grp = new THREE.Group();
443
+
444
+ // Body (VW Gol shape approx)
445
+ const bodyMat = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff, roughness: 0.2, metalness: 0.6 });
446
+ const chassis = new THREE.Mesh(new THREE.BoxGeometry(2, 0.8, 4), bodyMat);
447
+ chassis.position.y = 0.8;
448
+ grp.add(chassis);
449
+
450
+ // Cabin
451
+ const cabin = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.6, 2.5), new THREE.MeshStandardMaterial({ color: 0x333 })); // Glass
452
+ cabin.position.set(0, 1.4, -0.2);
453
+ grp.add(cabin);
454
+
455
+ // Wheels
456
+ const wGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 16);
457
+ const wMat = new THREE.MeshStandardMaterial({ color: 0x111 });
458
+ const w1 = new THREE.Mesh(wGeo, wMat); w1.rotation.z = Math.PI / 2; w1.position.set(-1, 0.4, 1.2);
459
+ const w2 = new THREE.Mesh(wGeo, wMat); w2.rotation.z = Math.PI / 2; w2.position.set(1, 0.4, 1.2);
460
+ const w3 = new THREE.Mesh(wGeo, wMat); w3.rotation.z = Math.PI / 2; w3.position.set(-1, 0.4, -1.2);
461
+ const w4 = new THREE.Mesh(wGeo, wMat); w4.rotation.z = Math.PI / 2; w4.position.set(1, 0.4, -1.2);
462
+ grp.add(w1, w2, w3, w4);
463
+
464
+ grp.position.set(x, 0, z);
465
+ scene.add(grp);
466
+
467
+ const car = { mesh: grp, speed: 0, steer: 0, velocity: new THREE.Vector3() };
468
+ cars.push(car);
469
+ collisionBoxes.push(new THREE.Box3().setFromObject(grp)); // Can crash into cars
470
  }
471
 
472
+ function spawnVehicles(n) {
473
+ for (let i = 0; i < n; i++) {
474
+ const z = ZONES[Math.floor(Math.random() * 3)];
475
+ createCar(z.x + (Math.random() - 0.5) * 200, z.z + (Math.random() - 0.5) * 200);
476
+ }
 
 
 
 
 
 
 
 
 
 
477
  }
478
 
479
+ function handleEnterExitCar() {
480
+ if (inCar) {
481
+ // Exit
482
+ playerGroup.position.copy(inCar.mesh.position).add(new THREE.Vector3(2, 0, 0));
483
+ playerGroup.visible = true;
484
+ inCar = null;
485
+ camera.position.y = 2; // Reset cam height
486
+ } else {
487
+ // Determine closest car
488
+ let closest = null, minD = 4.0;
489
+ cars.forEach(c => {
490
+ const d = playerGroup.position.distanceTo(c.mesh.position);
491
+ if (d < minD) { minD = d; closest = c; }
492
+ });
493
+
494
+ if (closest) {
495
+ inCar = closest;
496
+ playerGroup.visible = false; // "Inside" car
497
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  }
499
+ }
500
 
501
+ // --- COMBAT ---
502
+ function handleShoot() {
503
+ if (inCar) return; // No drive-by yet
504
+ const now = clock.elapsedTime;
505
+ if (now - lastShot < 0.2) return; // Fire rate
506
+ lastShot = now;
507
+
508
+ AUDIO.play('shoot');
509
+
510
+ // Raycast
511
+ const ray = new THREE.Raycaster();
512
+ ray.setFromCamera(new THREE.Vector2(0, 0), camera);
513
+
514
+ // Check Hit NPC
515
+ const hits = ray.intersectObjects(scene.children, true);
516
+ for (let hit of hits) {
517
+ // Find NPC root
518
+ let obj = hit.object;
519
+ while (obj.parent && obj.parent !== scene) obj = obj.parent;
520
+
521
+ // If it's an NPC group
522
+ const npc = npcs.find(n => n.mesh === obj);
523
+ if (npc) {
524
+ // KILL NPC
525
+ scene.remove(npc.mesh);
526
+ npcs = npcs.filter(n => n !== npc);
527
+ // Add "Ragdoll" or Effect? Just disappear for now (optimization)
528
+ // Blood effect?
529
+ const blood = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.1, 0.5), new THREE.MeshBasicMaterial({ color: 0xaa0000 }));
530
+ blood.position.copy(hit.point); blood.position.y = 0.1;
531
+ scene.add(blood);
532
+ break; // One kill per shot
533
+ }
534
+ }
535
+ }
536
 
537
+ // --- PLAYER & NPC (Minified from previous) ---
538
+ function createPlayer() {
539
+ const g = new THREE.Group();
540
+ const mat = new THREE.MeshStandardMaterial({ color: 0x87CEEB });
541
+ const b = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.8, 0.3), mat); b.position.y = 1;
542
+ const h = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.4, 0.4), new THREE.MeshStandardMaterial({ color: 0xdcb })); h.position.y = 1.6;
543
+ g.add(b, h); g.castShadow = true; scene.add(g);
544
+ playerGroup = g; player = { mesh: g };
545
+ }
546
+ function spawnNPCs(n) {
547
+ const geo = new THREE.BoxGeometry(0.5, 1.7, 0.3);
548
+ for (let i = 0; i < n; i++) {
549
+ const z = ZONES[Math.floor(Math.random() * 3)];
550
+ const m = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff }));
551
+ m.position.set(z.x + (Math.random() - 0.5) * 200, 0.85, z.z + (Math.random() - 0.5) * 200);
552
+ scene.add(m); npcs.push({ mesh: m, dir: new THREE.Vector3(Math.random() - 0.5, 0, Math.random() - 0.5).normalize() });
553
+ }
554
  }
555
 
556
+ // --- UPDATE LOOP ---
557
+ let camYaw = 0, camPitch = 0.3, camDist = 5;
558
+ function onMouseMove(e) { if (gameActive && !inCar) { camYaw -= e.movementX * 0.003; camPitch -= e.movementY * 0.003; } }
559
+
560
+ function updatePhysics(dt) {
561
+ if (inCar) {
562
+ // CAR PHYSICS
563
+ let gas = 0, turn = 0;
564
+ if (keyState['KeyW']) gas = 1; if (keyState['KeyS']) gas = -1;
565
+ if (keyState['KeyA']) turn = 1; if (keyState['KeyD']) turn = -1;
566
+
567
+ if (gas !== 0) {
568
+ inCar.speed += gas * 10 * dt;
569
+ AUDIO.play('car');
570
  }
571
+ inCar.speed *= 0.98; // Friction
572
+ inCar.mesh.rotation.y += turn * inCar.speed * 0.05 * dt;
573
+
574
+ const dir = new THREE.Vector3(0, 0, 1).applyAxisAngle(new THREE.Vector3(0, 1, 0), inCar.mesh.rotation.y);
575
+ inCar.mesh.position.addScaledVector(dir, inCar.speed * dt);
576
+
577
+ // Cam follow car
578
+ const cx = inCar.mesh.position.x - Math.sin(inCar.mesh.rotation.y) * 10;
579
+ const cz = inCar.mesh.position.z - Math.cos(inCar.mesh.rotation.y) * 10;
580
+ camera.position.lerp(new THREE.Vector3(cx, 5, cz), 0.1);
581
+ camera.lookAt(inCar.mesh.position);
582
+
583
+ // Teleport player group to car (for radar/zone check)
584
+ playerGroup.position.copy(inCar.mesh.position);
585
+
586
+ } else {
587
+ // ON FOOT
588
+ let f = 0, s = 0;
589
+ if (keyState['KeyW']) f = 1; if (keyState['KeyS']) s = -1;
590
+ if (keyState['KeyD']) s = 1; if (keyState['KeyA']) s = -1;
591
+
592
+ const spd = keyState['ShiftLeft'] ? 10 : 5;
593
+ if (f || s) {
594
+ const cd = new THREE.Vector3(); camera.getWorldDirection(cd); cd.y = 0; cd.normalize();
595
+ const cr = new THREE.Vector3(-cd.z, 0, cd.x);
596
+ const mv = new THREE.Vector3().addScaledVector(cd, f).addScaledVector(cr, s).normalize().multiplyScalar(spd * dt);
597
+
598
+ // Simple collision
599
+ const next = playerGroup.position.clone().add(mv);
600
+ const pbox = new THREE.Box3().setFromCenterAndSize(next.clone().add(new THREE.Vector3(0, 1, 0)), new THREE.Vector3(0.5, 2, 0.5));
601
+ let hit = false;
602
+ for (let b of collisionBoxes) if (pbox.intersectsBox(b)) hit = true;
603
+ if (!hit) playerGroup.position.add(mv);
604
+
605
+ // Rot
606
+ playerGroup.rotation.y = Math.atan2(mv.x, mv.z);
607
+ }
608
+
609
+ // Camera
610
+ if (isAiming) { camDist = 2; camPitch = 0.2; } else { camDist = 5; } // Zoom aim
611
+ const cx = playerGroup.position.x - Math.sin(camYaw) * camDist;
612
+ const cz = playerGroup.position.z - Math.cos(camYaw) * camDist;
613
+ const cy = playerGroup.position.y + 2 + Math.sin(camPitch) * 2;
614
+ camera.position.lerp(new THREE.Vector3(cx, cy, cz), 0.2);
615
+ camera.lookAt(playerGroup.position.clone().add(new THREE.Vector3(0, 1.5, 0)));
616
+ }
617
+
618
+ // HUD
619
+ let zn = "DESCONOCIDO"; let min = 9999;
620
+ ZONES.forEach(z => {
621
+ const d = new THREE.Vector2(playerGroup.position.x - z.x, playerGroup.position.z - z.z).length();
622
+ if (d < min) { min = d; zn = z.name; }
623
  });
624
+ if (min < 250) document.getElementById('zone-name').innerText = zn;
625
+
626
+ // Interaction Prompts
627
+ if (!inCar) {
628
+ // Check Cars
629
+ let closeCar = false;
630
+ for (let c of cars) if (playerGroup.position.distanceTo(c.mesh.position) < 3.0) closeCar = true;
631
+ document.getElementById('interact-prompt').innerText = closeCar ? "[F] MANEJAR" : "[E] HABLAR";
632
+ document.getElementById('interact-prompt').style.display = closeCar ? 'block' : 'none';
633
+ }
634
  }
635
 
636
+ function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
 
 
 
 
637
 
638
  function animate() {
639
  requestAnimationFrame(animate);
640
+ if (gameActive) {
641
+ const dt = Math.min(clock.getDelta(), 0.1);
642
+ updatePhysics(dt);
643
+ // NPC simple move
644
+ npcs.forEach(n => n.mesh.position.addScaledVector(n.dir, dt));
645
  renderer.render(scene, camera);
646
  }
647
  }
648
 
 
 
 
 
 
 
 
 
649
  init();
 
650
  </script>
651
  </body>
652
+
653
  </html>