anycoder-b8781a74 / index.html
salmanarshad's picture
Upload folder using huggingface_hub
43f993f verified
<!DOCTYPE html>
<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 !important;
}
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>