Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>San Andreas: Santa Maria Beach - Anycoder Edition</title> | |
| <!-- Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --ui-bg: rgba(0, 0, 0, 0.85); | |
| --ui-border: 2px solid rgba(255, 215, 0, 0.5); | |
| --accent: #ffcc00; | |
| --danger: #ff3333; | |
| --text-main: #ffffff; | |
| --font-main: 'Rajdhani', sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| } | |
| body, html { | |
| margin: 0; | |
| padding: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: #1a1a1a; | |
| overflow: hidden; | |
| font-family: var(--font-main); | |
| } | |
| /* Game Container */ | |
| #game-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| canvas { | |
| box-shadow: 0 0 50px rgba(0,0,0,0.8); | |
| background: #87CEEB; /* Sky fallback */ | |
| } | |
| /* UI Overlay */ | |
| #ui-layer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; /* Let clicks pass through to canvas */ | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 20px; | |
| } | |
| /* Top HUD */ | |
| .top-hud { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| } | |
| .stats-container { | |
| background: var(--ui-bg); | |
| border: var(--ui-border); | |
| padding: 15px; | |
| border-radius: 4px; | |
| width: 300px; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.5); | |
| } | |
| .stat-row { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| font-size: 1.1rem; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| } | |
| .stat-row:last-child { | |
| margin-bottom: 0; | |
| } | |
| .stat-label { | |
| width: 80px; | |
| } | |
| .stat-bar-bg { | |
| flex-grow: 1; | |
| height: 12px; | |
| background: #333; | |
| margin: 0 10px; | |
| border: 1px solid #555; | |
| position: relative; | |
| } | |
| .stat-bar-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #ffcc00, #ff9900); | |
| width: 100%; | |
| transition: width 0.2s; | |
| } | |
| .health-fill { background: linear-gradient(90deg, #ff3333, #aa0000); } | |
| .stamina-fill { background: linear-gradient(90deg, #00ccff, #0066cc); } | |
| .minimap-container { | |
| width: 180px; | |
| height: 180px; | |
| background: rgba(0,0,0,0.7); | |
| border: 2px solid var(--accent); | |
| border-radius: 50%; | |
| position: relative; | |
| overflow: hidden; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #minimap { | |
| width: 100%; | |
| height: 100%; | |
| image-rendering: pixelated; | |
| } | |
| .minimap-compass { | |
| position: absolute; | |
| top: 10px; | |
| right: 20px; | |
| color: var(--accent); | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| text-shadow: 1px 1px 0 #000; | |
| } | |
| /* Bottom HUD */ | |
| .bottom-hud { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-end; | |
| } | |
| .controls-info { | |
| background: var(--ui-bg); | |
| border: var(--ui-border); | |
| padding: 15px; | |
| border-radius: 4px; | |
| color: #ccc; | |
| font-size: 0.9rem; | |
| } | |
| .key { | |
| display: inline-block; | |
| background: #444; | |
| border: 1px solid #666; | |
| border-radius: 3px; | |
| padding: 2px 6px; | |
| margin: 0 2px; | |
| color: #fff; | |
| font-weight: bold; | |
| } | |
| .cash-display { | |
| background: var(--ui-bg); | |
| border: var(--ui-border); | |
| padding: 15px 25px; | |
| border-radius: 4px; | |
| color: var(--accent); | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| text-align: right; | |
| text-shadow: 2px 2px 0 #000; | |
| } | |
| /* Screens (Start, Pause) */ | |
| .screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.9); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| pointer-events: auto; | |
| backdrop-filter: blur(5px); | |
| } | |
| .hidden { | |
| display: none ; | |
| } | |
| h1 { | |
| font-size: 4rem; | |
| color: var(--accent); | |
| text-transform: uppercase; | |
| letter-spacing: 5px; | |
| margin-bottom: 10px; | |
| text-shadow: 0 0 20px rgba(255, 204, 0, 0.5); | |
| text-align: center; | |
| } | |
| p.subtitle { | |
| font-size: 1.2rem; | |
| color: #fff; | |
| margin-bottom: 40px; | |
| max-width: 600px; | |
| text-align: center; | |
| line-height: 1.5; | |
| } | |
| .btn { | |
| background: var(--accent); | |
| color: #000; | |
| border: none; | |
| padding: 15px 40px; | |
| font-size: 1.5rem; | |
| font-family: var(--font-main); | |
| font-weight: 700; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| transition: all 0.2s; | |
| clip-path: polygon(10% 0, 100% 0, 100% 70%, 90% 100%, 0 100%, 0 30%); | |
| } | |
| .btn:hover { | |
| background: #fff; | |
| transform: scale(1.05); | |
| box-shadow: 0 0 20px var(--accent); | |
| } | |
| .anycoder-footer { | |
| position: absolute; | |
| bottom: 10px; | |
| width: 100%; | |
| text-align: center; | |
| color: #666; | |
| font-size: 0.8rem; | |
| } | |
| .anycoder-link { | |
| color: #666; | |
| text-decoration: none; | |
| border-bottom: 1px dotted #666; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| /* Mobile Controls */ | |
| #mobile-controls { | |
| display: none; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: auto; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| } | |
| @media (max-width: 768px) { | |
| h1 { font-size: 2.5rem; } | |
| .stats-container { width: 200px; } | |
| .minimap-container { width: 120px; height: 120px; } | |
| .controls-info { display: none; } /* Hide keyboard hints on mobile */ | |
| #mobile-controls { display: block; } | |
| /* Virtual Joystick Areas */ | |
| #joystick-zone { | |
| position: absolute; | |
| bottom: 50px; | |
| left: 50px; | |
| width: 150px; | |
| height: 150px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 50%; | |
| border: 2px solid rgba(255,255,255,0.2); | |
| } | |
| #action-zone { | |
| position: absolute; | |
| bottom: 50px; | |
| right: 50px; | |
| width: 100px; | |
| height: 100px; | |
| background: rgba(255,0,0,0.1); | |
| border-radius: 50%; | |
| border: 2px solid rgba(255,0,0,0.2); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: rgba(255,0,0,0.5); | |
| font-size: 2rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- UI Layer --> | |
| <div id="ui-layer"> | |
| <div class="top-hud"> | |
| <div class="stats-container"> | |
| <div class="stat-row"> | |
| <span class="stat-label"><i class="fas fa-heart"></i> HP</span> | |
| <div class="stat-bar-bg"><div class="stat-bar-fill health-fill" id="hp-bar"></div></div> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label"><i class="fas fa-running"></i> STAM</span> | |
| <div class="stat-bar-bg"><div class="stat-bar-fill stamina-fill" id="stamina-bar"></div></div> | |
| </div> | |
| </div> | |
| <div class="minimap-container"> | |
| <canvas id="minimap"></canvas> | |
| <div class="minimap-compass">N</div> | |
| </div> | |
| </div> | |
| <div class="bottom-hud"> | |
| <div class="controls-info"> | |
| <div><span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> Move</div> | |
| <div><span class="key">SHIFT</span> Sprint</div> | |
| <div><span class="key">SPACE</span> Jump / Brake</div> | |
| <div><span class="key">F</span> Enter/Exit Vehicle</div> | |
| </div> | |
| <div class="cash-display"> | |
| $ <span id="cash-display">0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="start-screen" class="screen"> | |
| <h1>San Andreas<br><span style="font-size: 0.5em; color: #fff;">Santa Maria Beach</span></h1> | |
| <p class="subtitle"> | |
| Explore the sunny coast of Los Santos. | |
| <br>Find hidden crates for cash. | |
| <br>Watch out for the water! | |
| </p> | |
| <button class="btn" id="start-btn">Start Game</button> | |
| <div class="anycoder-footer"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">anycoder</a> | |
| </div> | |
| </div> | |
| <!-- Mobile Controls --> | |
| <div id="mobile-controls"> | |
| <div id="joystick-zone"></div> | |
| <div id="action-zone"><i class="fas fa-car"></i></div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * San Andreas Clone - Santa Maria Beach | |
| * A pure JS/Canvas implementation. | |
| */ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const minimapCanvas = document.getElementById('minimap'); | |
| const minimapCtx = minimapCanvas.getContext('2d'); | |
| // Game State | |
| const GAME = { | |
| width: 0, | |
| height: 0, | |
| scale: 1, // Pixels per meter (approx) | |
| running: false, | |
| camera: { x: 0, y: 0 }, | |
| time: 0, | |
| keys: {}, | |
| lastTime: 0 | |
| }; | |
| // Map Dimensions (Virtual World) | |
| const MAP_WIDTH = 4000; | |
| const MAP_HEIGHT = 4000; | |
| // Player Entity | |
| const player = { | |
| x: 500, y: 500, // Start at Santa Maria Beach | |
| vx: 0, vy: 0, | |
| angle: 0, | |
| speed: 0, | |
| maxSpeed: 4, | |
| acceleration: 0.2, | |
| friction: 0.92, | |
| radius: 15, | |
| state: 'walking', // walking, running, swimming | |
| hp: 100, | |
| stamina: 100, | |
| cash: 0, | |
| inVehicle: null, | |
| color: '#ffcc00', | |
| animFrame: 0 | |
| }; | |
| // Vehicle Database | |
| const VEHICLES = [ | |
| { type: 'sedan', color: '#ff3333', width: 60, height: 30, speed: 10, accel: 0.3, handling: 0.06 }, | |
| { type: 'truck', color: '#333333', width: 90, height: 40, speed: 6, accel: 0.15, handling: 0.03 }, | |
| { type: 'sport', color: '#00ccff', width: 55, height: 28, speed: 14, accel: 0.4, handling: 0.07 } | |
| ]; | |
| // World Objects | |
| const objects = []; | |
| const pickups = []; | |
| // Input Handling | |
| window.addEventListener('keydown', (e) => { | |
| GAME.keys[e.code] = true; | |
| if (e.code === 'KeyF') toggleVehicle(); | |
| }); | |
| window.addEventListener('keyup', (e) => GAME.keys[e.code] = false); | |
| // Resize Handling | |
| function resize() { | |
| GAME.width = window.innerWidth; | |
| GAME.height = window.innerHeight; | |
| canvas.width = GAME.width; | |
| canvas.height = GAME.height; | |
| minimapCanvas.width = 180; | |
| minimapCanvas.height = 180; | |
| // Adjust scale for responsiveness | |
| GAME.scale = Math.min(GAME.width / MAP_WIDTH, GAME.height / MAP_HEIGHT) * 0.9; | |
| } | |
| window.addEventListener('resize', resize); | |
| // --- World Generation --- | |
| function initWorld() { | |
| objects.length = 0; | |
| pickups.length = 0; | |
| // 1. Water (Santa Maria Beach) | |
| // Create a beach strip | |
| const beachY = 600; | |
| // 2. Buildings / Structures | |
| // Generate some random blocks | |
| for (let i = 0; i < 40; i++) { | |
| objects.push({ | |
| x: Math.random() * MAP_WIDTH, | |
| y: Math.random() * MAP_HEIGHT, | |
| w: 50 + Math.random() * 100, | |
| h: 50 + Math.random() * 100, | |
| type: 'building', | |
| color: `hsl(${Math.random() * 20 + 20}, 30%, ${Math.random() * 20 + 30}%)` // Beige/Tan | |
| }); | |
| } | |
| // 3. Palm Trees | |
| for (let i = 0; i < 100; i++) { | |
| objects.push({ | |
| x: Math.random() * MAP_WIDTH, | |
| y: Math.random() * MAP_HEIGHT, | |
| type: 'tree', | |
| color: '#8B4513' | |
| }); | |
| } | |
| // 4. Roads | |
| // Main highway | |
| for (let x = 0; x < MAP_WIDTH; x+= 100) { | |
| objects.push({ x: x, y: 800, w: 100, h: 20, type: 'road', color: '#444' }); | |
| } | |
| // 5. Pickups (Crates) | |
| for (let i = 0; i < 20; i++) { | |
| pickups.push({ | |
| x: Math.random() * (MAP_WIDTH - 100) + 50, | |
| y: Math.random() * (MAP_HEIGHT - 100) + 50, | |
| value: Math.floor(Math.random() * 500) + 100, | |
| collected: false | |
| }); | |
| } | |
| // 6. Spawn Vehicles | |
| spawnVehicle(VEHICLES[0], 800, 800); | |
| spawnVehicle(VEHICLES[1], 1200, 1200); | |
| spawnVehicle(VEHICLES[2], 1500, 600); | |
| } | |
| function spawnVehicle(template, x, y) { | |
| const v = { | |
| ...template, | |
| x: x, | |
| y: y, | |
| vx: 0, | |
| vy: 0, | |
| angle: Math.random() * Math.PI * 2, | |
| id: Math.random() | |
| }; | |
| objects.push({ ...v, type: 'vehicle' }); // Add to collision objects | |
| pickups.push(v); // Add to interaction list | |
| } | |
| // --- Game Logic --- | |
| function update(dt) { | |
| if (!GAME.running) return; | |
| GAME.time += dt; | |
| // Player Logic | |
| if (player.inVehicle) { | |
| updateVehicle(player.inVehicle); | |
| } else { | |
| updatePlayer(); | |
| } | |
| // Camera Follow | |
| GAME.camera.x = player.x - GAME.width / 2; | |
| GAME.camera.y = player.y - GAME.height / 2; | |
| // Clamp Camera | |
| GAME.camera.x = Math.max(0, Math.min(GAME.camera.x, MAP_WIDTH - GAME.width)); | |
| GAME.camera.y = Math.max(0, Math.min(GAME.camera.y, MAP_HEIGHT - GAME.height)); | |
| // Check Collisions | |
| checkCollisions(); | |
| } | |
| function updatePlayer() { | |
| // Input | |
| let inputX = 0; | |
| let inputY = 0; | |
| const isRunning = GAME.keys['ShiftLeft'] || GAME.keys['ShiftRight']; | |
| const maxSpd = isRunning ? player.maxSpeed * 1.6 : player.maxSpeed; | |
| const accel = player.acceleration * (isRunning ? 1.5 : 1); | |
| if (GAME.keys['KeyW']) inputY = -1; | |
| if (GAME.keys['KeyS']) inputY = 1; | |
| if (GAME.keys['KeyA']) inputX = -1; | |
| if (GAME.keys['KeyD']) inputX = 1; | |
| // Normalize | |
| if (inputX !== 0 || inputY !== 0) { | |
| const len = Math.sqrt(inputX*inputX + inputY*inputY); | |
| inputX /= len; | |
| inputY /= len; | |
| player.speed += accel; | |
| // Rotate towards movement | |
| const targetAngle = Math.atan2(inputY, inputX); | |
| // Simple rotation interpolation | |
| let diff = targetAngle - player.angle; | |
| while (diff < -Math.PI) diff += Math.PI * 2; | |
| while (diff > Math.PI) diff -= Math.PI * 2; | |
| player.angle += diff * 0.2; | |
| } else { | |
| player.speed *= player.friction; | |
| } | |
| // Cap speed | |
| player.speed = Math.max(0, Math.min(player.speed, maxSpd)); | |
| // Apply movement | |
| player.x += Math.cos(player.angle) * player.speed; | |
| player.y += Math.sin(player.angle) * player.speed; | |
| // Boundaries | |
| player.x = Math.max(player.radius, Math.min(MAP_WIDTH - player.radius, player.x)); | |
| player.y = Math.max(player.radius, Math.min(MAP_HEIGHT - player.radius, player.y)); | |
| // Stamina Regen | |
| if (player.stamina < 100) player.stamina += 0.5; | |
| if (player.stamina < 0) player.stamina = 0; | |
| if (isRunning && player.speed > 0.1) player.stamina -= 1; | |
| // Animation | |
| if (player.speed > 0.1) player.animFrame += dt * 0.2; | |
| } | |
| function updateVehicle(v) { | |
| // Input | |
| let inputX = 0; | |
| if (GAME.keys['KeyW']) inputX = 1; | |
| if (GAME.keys['KeyS']) inputX = -1; | |
| if (inputX !== 0) { | |
| // Accelerate | |
| v.vx += Math.cos(v.angle) * v.accel; | |
| v.vy += Math.sin(v.angle) * v.accel; | |
| // Turn | |
| v.angle += inputX * v.handling; | |
| } else { | |
| // Friction | |
| v.vx *= 0.98; | |
| v.vy *= 0.98; | |
| } | |
| // Cap Speed | |
| const currentSpeed = Math.sqrt(v.vx*v.vx + v.vy*v.vy); | |
| if (currentSpeed > v.speed) { | |
| const ratio = v.speed / currentSpeed; | |
| v.vx *= ratio; | |
| v.vy *= ratio; | |
| } | |
| // Move | |
| v.x += v.vx; | |
| v.y += v.vy; | |
| // Simple wall collision for vehicles (bounce back slightly) | |
| if (v.x < 0 || v.x > MAP_WIDTH || v.y < 0 || v.y > MAP_HEIGHT) { | |
| v.vx *= -0.5; | |
| v.vy *= -0.5; | |
| v.x = Math.max(0, Math.min(v.x, MAP_WIDTH)); | |
| v.y = Math.max(0, Math.min(v.y, MAP_HEIGHT)); | |
| } | |
| // Sync player to vehicle | |
| player.x = v.x; | |
| player.y = v.y; | |
| player.angle = v.angle; | |
| } | |
| function checkCollisions() { | |
| // 1. Player vs Objects (Simple AABB) | |
| objects.forEach(obj => { | |
| if (obj.type === 'building' || obj.type === 'road') { | |
| // Simple box collision | |
| if (player.x > obj.x - player.radius && player.x < obj.x + obj.w + player.radius && | |
| player.y > obj.y - player.radius && player.y < obj.y + obj.h + player.radius) { | |
| // Push out | |
| const overlapX = (player.x - obj.x) / (obj.w/2); | |
| const overlapY = (player.y - obj.y) / (obj.h/2); | |
| if (Math.abs(overlapX) < Math.abs(overlapY)) { | |
| player.x = overlapX > 0 ? obj.x + obj.w + player.radius : obj.x - player.radius; | |
| player.speed *= 0.5; | |
| } else { | |
| player.y = overlapY > 0 ? obj.y + obj.h + player.radius : obj.y - player.radius; | |
| player.speed *= 0.5; | |
| } | |
| } | |
| } else if (obj.type === 'tree') { | |
| // Tree collision is circular | |
| const dx = player.x - obj.x; | |
| const dy = player.y - obj.y; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| if (dist < player.radius + 15) { | |
| player.x -= dx * 0.1; | |
| player.y -= dy * 0.1; | |
| player.speed *= 0.5; | |
| } | |
| } | |
| }); | |
| // 2. Player vs Water | |
| const beachY = 600; | |
| if (player.y > beachY) { | |
| // Swimming logic | |
| player.state = 'swimming'; | |
| player.color = '#00ccff'; // Turn blue | |
| // Swim logic similar to walking but slower | |
| if (GAME.keys['KeyW']) player.y -= 0.1; | |
| if (GAME.keys['KeyS']) player.y += 0.1; | |
| if (GAME.keys['KeyA']) player.x -= 0.1; | |
| if (GAME.keys['KeyD']) player.x += 0.1; | |
| } else { | |
| player.state = 'walking'; | |
| player.color = '#ffcc00'; | |
| } | |
| // 3. Pickups | |
| pickups.forEach(p => { | |
| if (p.collected) return; | |
| if (p.type === 'vehicle' && player.inVehicle) return; // Already inside | |
| const dx = player.x - p.x; | |
| const dy = player.y - p.y; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| if (dist < player.radius + 30) { | |
| if (p.type === 'vehicle') { | |
| // Enter vehicle | |
| player.inVehicle = p; | |
| // Reset player pos to vehicle center | |
| player.x = p.x; | |
| player.y = p.y; | |
| showToast("Entered Vehicle"); | |
| } else { | |
| // Collect cash | |
| p.collected = true; | |
| player.cash += p.value; | |
| document.getElementById('cash-display').innerText = player.cash; | |
| showToast(`+$${p.value}`); | |
| } | |
| } | |
| }); | |
| } | |
| function toggleVehicle() { | |
| if (player.inVehicle) { | |
| // Exit | |
| player.inVehicle = null; | |
| // Place player slightly in front | |
| player.x += Math.cos(player.angle) * 20; | |
| player.y += Math.sin(player.angle) * 20; | |
| player.speed = 0; | |
| showToast("Exited Vehicle"); | |
| } else { | |
| // Check if near vehicle | |
| let nearest = null; | |
| let minDist = 50; | |
| pickups.forEach(p => { | |
| if (p.type === 'vehicle' && !p.collected) { | |
| const dx = player.x - p.x; | |
| const dy = player.y - p.y; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| if (dist < minDist) { | |
| nearest = p; | |
| } | |
| } | |
| }); | |
| if (nearest) { | |
| player.inVehicle = nearest; | |
| showToast("Entered Vehicle"); | |
| } else { | |
| showToast("No vehicle nearby"); | |
| } | |
| } | |
| } | |
| // --- Rendering --- | |
| function draw() { | |
| // Clear | |
| ctx.fillStyle = '#87CEEB'; // Sky | |
| ctx.fillRect(0, 0, GAME.width, GAME.height); | |
| ctx.save(); | |
| ctx.translate(-GAME.camera.x, -GAME.camera.y); | |
| // 1. Draw Water | |
| const beachY = 600; | |
| ctx.fillStyle = '#006994'; | |
| ctx.fillRect(0, beachY, MAP_WIDTH, MAP_HEIGHT - beachY); | |
| // Water shimmer | |
| ctx.strokeStyle = 'rgba(255,255,255,0.1)'; | |
| ctx.beginPath(); | |
| for(let i=0; i<10; i++) { | |
| let wx = (GAME.time * 50 + i * 200) % MAP_WIDTH; | |
| ctx.moveTo(wx, beachY + 50); | |
| ctx.lineTo(wx + 50, beachY + 100); | |
| } | |
| ctx.stroke(); | |
| // 2. Draw Buildings | |
| objects.forEach(obj => { | |
| if (obj.type === 'building') { | |
| // Shadow | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.fillRect(obj.x + 5, obj.y + 5, obj.w, obj.h); | |
| // Body | |
| ctx.fillStyle = obj.color; | |
| ctx.fillRect(obj.x, obj.y, obj.w, obj.h); | |
| // Roof | |
| ctx.fillStyle = '#8B4513'; | |
| ctx.fillRect(obj.x - 5, obj.y - 5, obj.w + 10, 10); | |
| } else if (obj.type === 'road') { | |
| ctx.fillStyle = obj.color; | |
| ctx.fillRect(obj.x, obj.y, obj.w, obj.h); | |
| // Dashed line | |
| ctx.fillStyle = '#fff'; | |
| for(let i=0; i<obj.w; i+=40) { | |
| ctx.fillRect(obj.x + i, obj.y + 9, 20, 2); | |
| } | |
| } | |
| }); | |
| // 3. Draw Trees | |
| objects.forEach(obj => { | |
| if (obj.type === 'tree') { | |
| // Trunk | |
| ctx.fillStyle = obj.color; | |
| ctx.fillRect(obj.x - 3, obj.y, 6, 20); | |
| // Leaves (Palm style) | |
| ctx.fillStyle = '#228B22'; | |
| ctx.beginPath(); | |
| ctx.arc(obj.x, obj.y - 10, 15, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| }); | |
| // 4. Draw Pickups (Vehicles) | |
| pickups.forEach(p => { | |
| if (p.type === 'vehicle' && !p.collected) { | |
| drawCar(ctx, p.x, p.y, p.angle, p.color, p.type); | |
| } else if (!p.collected) { | |
| // Cash Crate | |
| ctx.fillStyle = '#d4a017'; | |
| ctx.fillRect(p.x - 10, p.y - 10, 20, 20); | |
| ctx.strokeStyle = '#fff'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(p.x - 10, p.y - 10, 20, 20); | |
| ctx.fillStyle = '#000'; | |
| ctx.font = '10px Arial'; | |
| ctx.fillText('$', p.x - 3, p.y + 4); | |
| } | |
| }); | |
| // 5. Draw Player | |
| if (!player.inVehicle) { | |
| drawPlayer(ctx, player.x, player.y, player.angle, player.color); | |
| } | |
| ctx.restore(); | |
| // 6. Draw Minimap | |
| drawMinimap(); | |
| // 7. Update UI Bars | |
| document.getElementById('hp-bar').style.width = player.hp + '%'; | |
| document.getElementById('stamina-bar').style.width = player.stamina + '%'; | |
| } | |
| function drawPlayer(ctx, x, y, angle, color) { | |
| ctx.save(); | |
| ctx.translate(x, y); | |
| ctx.rotate(angle); | |
| // Shadow | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 15, 10, 5, 0, 0, Math.PI*2); | |
| ctx.fill(); | |
| // Body (Simple character) | |
| ctx.fillStyle = color; // Shirt color | |
| ctx.beginPath(); | |
| ctx.arc(0, -5, 8, 0, Math.PI * 2); // Head | |
| ctx.fill(); | |
| ctx.fillStyle = '#fff'; // Pants | |
| ctx.fillRect(-5, 5, 10, 10); | |
| ctx.fillStyle = color; // Shirt | |
| ctx.fillRect(-8, -5, 16, 10); // Torso | |
| // Legs (animated) | |
| const legOffset = Math.sin(player.animFrame * 5) * 5; | |
| ctx.fillStyle = '#333'; | |
| ctx.fillRect(-5, 10 + legOffset, 4, 8); | |
| ctx.fillRect(1, 10 - legOffset, 4, 8); | |
| ctx.restore(); | |
| } | |
| function drawCar(ctx, x, y, angle, color, type) { | |
| ctx.save(); | |
| ctx.translate(x, y); | |
| ctx.rotate(angle); | |
| // Shadow | |
| ctx.fillStyle = 'rgba(0,0,0,0.4)'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 15, 30, 15, 0, 0, Math.PI*2); | |
| ctx.fill(); | |
| // Car Body | |
| ctx.fillStyle = color; | |
| // Main chassis | |
| ctx.beginPath(); | |
| ctx.roundRect(-30, -15, 60, 30, 5); | |
| ctx.fill(); | |
| // Roof | |
| ctx.fillStyle = 'rgba(0,0,0,0.2)'; | |
| ctx.fillRect(-20, -15, 30, 20); | |
| // Windshield | |
| ctx.fillStyle = '#aaddff'; | |
| ctx.fillRect(10, -10, 10, 15); | |
| // Wheels | |
| ctx.fillStyle = '#111'; | |
| ctx.beginPath(); | |
| ctx.arc(-20, 10, 8, 0, Math.PI * 2); | |
| ctx.arc(20, 10, 8, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Headlights | |
| ctx.fillStyle = '#ffeb3b'; | |
| ctx.beginPath(); | |
| ctx.arc(30, -10, 3, 0, Math.PI * 2); | |
| ctx.arc(30, 10, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawMinimap() { | |
| const w = minimapCanvas.width; | |
| const h = minimapCanvas.height; | |
| const scale = w / MAP_WIDTH; | |
| minimapCtx.clearRect(0, 0, w, h); | |
| // Draw Buildings | |
| minimapCtx.fillStyle = '#555'; | |
| objects.forEach(obj => { | |
| if (obj.type === 'building') { | |
| minimapCtx.fillRect(obj.x * scale, obj.y * scale, obj.w * scale, obj.h * scale); | |
| } | |
| }); | |
| // Draw Water | |
| minimapCtx.fillStyle = '#006994'; | |
| minimapCtx.fillRect(0, 600 * scale, w, h - 600 * scale); | |
| // Draw Vehicles | |
| minimapCtx.fillStyle = '#fff'; | |
| pickups.forEach(p => { | |
| if (p.type === 'vehicle' && !p.collected) { | |
| minimapCtx.beginPath(); | |
| minimapCtx.arc(p.x * scale, p.y * scale, 3, 0, Math.PI*2); | |
| minimapCtx.fill(); | |
| } | |
| }); | |
| // Draw Player | |
| minimapCtx.fillStyle = '#ffcc00'; | |
| minimapCtx.beginPath(); | |
| minimapCtx.arc(player.x * scale, player.y * scale, 4, 0, Math.PI*2); | |
| minimapCtx.fill(); | |
| // Player Direction | |
| minimapCtx.strokeStyle = '#ffcc00'; | |
| minimapCtx.lineWidth = 1; | |
| minimapCtx.beginPath(); | |
| minimapCtx.moveTo(player.x * scale, player.y * scale); | |
| minimapCtx.lineTo((player.x + Math.cos(player.angle)*10) * scale, (player.y + Math.sin(player.angle)*10) * scale); | |
| minimapCtx.stroke(); | |
| } | |
| function showToast(msg) { | |
| // Simple visual feedback | |
| const div = document.createElement('div'); | |
| div.innerText = msg; | |
| div.style.position = 'absolute'; | |
| div.style.top = '50%'; | |
| div.style.left = '50%'; | |
| div.style.transform = 'translate(-50%, -50%)'; | |
| div.style.background = 'rgba(0,0,0,0.7)'; | |
| div.style.color = '#fff'; | |
| div.style.padding = '10px 20px'; | |
| div.style.borderRadius = '5px'; | |
| div.style.pointerEvents = 'none'; | |
| div.style.transition = 'opacity 1s'; | |
| div.style.zIndex = '100'; | |
| document.body.appendChild(div); | |
| setTimeout(() => { | |
| div.style.opacity = '0'; | |
| setTimeout(() => div.remove(), 1000); | |
| }, 1000); | |
| } | |
| // --- Game Loop --- | |
| function loop(timestamp) { | |
| const dt = (timestamp - GAME.lastTime) / 1000; | |
| GAME.lastTime = timestamp; | |
| update(dt); | |
| draw(); | |
| requestAnimationFrame(loop); | |
| } | |
| // --- Initialization --- | |
| document.getElementById('start-btn').addEventListener('click', () => { | |
| document.getElementById('start-screen').classList.add('hidden'); | |
| resize(); | |
| initWorld(); | |
| GAME.running = true; | |
| GAME.lastTime = performance.now(); | |
| requestAnimationFrame(loop); | |
| }); | |
| // Mobile Controls Logic | |
| const joystickZone = document.getElementById('joystick-zone'); | |
| const actionZone = document.getElementById('action-zone'); | |
| let touchId = null; | |
| let touchX = 0; | |
| let touchY = 0; | |
| joystickZone.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| const touch = e.changedTouches[0]; | |
| touchId = touch.identifier; | |
| touchX = touch.clientX; | |
| touchY = touch.clientY; | |
| }, {passive: false}); | |
| joystickZone.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| for (let i=0; i<e.changedTouches.length; i++) { | |
| if (e.changedTouches[i].identifier === touchId) { | |
| const dx = e.changedTouches[i].clientX - touchX; | |
| const dy = e.changedTouches[i].clientY - touchY; | |
| // Normalize input -1 to 1 | |
| const len = Math.sqrt(dx*dx + dy*dy); | |
| const maxDist = 50; | |
| const clampedLen = Math.min(len, maxDist); | |
| const normX = (dx / maxDist); | |
| const normY = (dy / maxDist); | |
| // Set Keys | |
| GAME.keys['KeyW'] = normY < -0.3; | |
| GAME.keys['KeyS'] = normY > 0.3; | |
| GAME.keys['KeyA'] = normX < -0.3; | |
| GAME.keys['KeyD'] = normX > 0.3; | |
| } | |
| } | |
| }, {passive: false}); | |
| joystickZone.addEventListener('touchend', (e) => { | |
| e.preventDefault(); | |
| GAME.keys['KeyW'] = false; | |
| GAME.keys['KeyS'] = false; | |
| GAME.keys['KeyA'] = false; | |
| GAME.keys['KeyD'] = false; | |
| }); | |
| actionZone.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| toggleVehicle(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |