
One shot GTA Clone game
Prompt
Create a basic GTA-style top-down game clone using HTML, CSS, and JavaScript. The player should control a character with arrow keys or WASD, move around a city map, and interact with vehicles and pedestrians. Include simple collision detection, sprite animations, and a minimap UI
A system prompt was added to support web rendering
# GTA-Style Top-Down Game Clone
I'll create a top-down game with player movement, vehicles, pedestrians, collision detection, and a minimap. Here's the complete implementation:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Top-Down City Game</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #111;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
color: white;
}
#game-container {
position: relative;
width: 800px;
height: 600px;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.5);
border: 2px solid #0af;
}
#game-canvas {
background: #222;
width: 100%;
height: 100%;
}
#minimap {
position: absolute;
top: 15px;
right: 15px;
width: 150px;
height: 150px;
background: rgba(0, 0, 0, 0.7);
border: 2px solid #0af;
box-shadow: 0 0 10px rgba(0, 150, 255, 0.5);
}
#game-ui {
position: absolute;
bottom: 15px;
left: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 5px;
border: 1px solid #0af;
}
#controls {
position: absolute;
top: 15px;
left: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
border: 1px solid #0af;
font-size: 12px;
}
#controls h3 {
margin-bottom: 5px;
color: #0af;
}
#controls ul {
list-style: none;
padding-left: 10px;
}
#controls li {
margin: 3px 0;
}
#title {
position: absolute;
top: 15px;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
color: #0af;
text-shadow: 0 0 10px rgba(0, 150, 255, 0.8);
}
.vehicle {
position: absolute;
width: 30px;
height: 15px;
background: #f00;
border-radius: 3px;
}
.pedestrian {
position: absolute;
width: 8px;
height: 8px;
background: #0f0;
border-radius: 50%;
}
#instructions {
position: absolute;
bottom: 15px;
right: 15px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
border: 1px solid #0af;
font-size: 12px;
max-width: 200px;
}
</style>
</head>
<body>
<div id="game-container">
<div id="title">CITY ROAMER</div>
<canvas id="game-canvas" width="800" height="600"></canvas>
<canvas id="minimap" width="150" height="150"></canvas>
<div id="game-ui">
<div>Speed: <span id="speed">0</span> mph</div>
<div>Status: <span id="status">Roaming</span></div>
</div>
<div id="controls">
<h3>Controls</h3>
<ul>
<li>Arrow Keys / WASD: Move</li>
<li>Shift: Sprint</li>
<li>Space: Interact</li>
</ul>
</div>
<div id="instructions">
<p>Explore the city, avoid collisions, and interact with vehicles and pedestrians!</p>
</div>
</div>
<script>
// Game variables
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const minimap = document.getElementById('minimap');
const minimapCtx = minimap.getContext('2d');
const speedElement = document.getElementById('speed');
const statusElement = document.getElementById('status');
// Player properties
const player = {
x: 400,
y: 300,
width: 20,
height: 20,
speed: 0,
maxSpeed: 5,
acceleration: 0.2,
deceleration: 0.1,
color: '#0af',
direction: 0, // 0: up, 1: right, 2: down, 3: left
isMoving: false
};
// Map properties
const map = {
width: 2000,
height: 1500,
tileSize: 50,
tiles: []
};
// Camera properties
const camera = {
x: 0,
y: 0,
width: canvas.width,
height: canvas.height
};
// Vehicles array
const vehicles = [
{ x: 300, y: 200, width: 30, height: 15, color: '#f00', speed: 1, direction: 1 },
{ x: 500, y: 400, width: 30, height: 15, color: '#ff0', speed: 1.5, direction: 2 },
{ x: 700, y: 300, width: 30, height: 15, color: '#0f0', speed: 1.2, direction: 3 },
{ x: 200, y: 500, width: 30, height: 15, color: '#00f', speed: 0.8, direction: 0 },
{ x: 600, y: 100, width: 30, height: 15, color: '#f0f', speed: 1.3, direction: 1 }
];
// Pedestrians array
const pedestrians = [];
for (let i = 0; i < 20; i++) {
pedestrians.push({
x: Math.random() * map.width,
y: Math.random() * map.height,
width: 8,
height: 8,
color: '#0f0',
speed: 0.5 + Math.random() * 0.5,
direction: Math.floor(Math.random() * 4)
});
}
// Buildings array
const buildings = [
{ x: 100, y: 100, width: 150, height: 100, color: '#555' },
{ x: 300, y: 150, width: 120, height: 180, color: '#666' },
{ x: 500, y: 100, width: 100, height: 120, color: '#777' },
{ x: 700, y: 200, width: 180, height: 150, color: '#555' },
{ x: 200, y: 400, width: 100, height: 200, color: '#666' },
{ x: 400, y: 500, width: 150, height: 100, color: '#777' },
{ x: 600, y: 450, width: 120, height: 150, color: '#555' }
];
// Roads array
const roads = [
{ x: 0, y: 250, width: map.width, height: 50, color: '#333' },
{ x: 0, y: 450, width: map.width, height: 50, color: '#333' },
{ x: 250, y: 0, width: 50, height: map.height, color: '#333' },
{ x: 450, y: 0, width: 50, height: map.height, color: '#333' },
{ x: 650, y: 0, width: 50, height: map.height, color: '#333' }
];
// Key state tracking
const keys = {
up: false,
down: false,
left: false,
right: false,
shift: false,
space: false
};
// Initialize game
function init() {
// Generate map tiles for minimap
for (let y = 0; y < map.height; y += map.tileSize) {
for (let x = 0; x < map.width; x += map.tileSize) {
map.tiles.push({
x: x,
y: y,
width: map.tileSize,
height: map.tileSize,
color: (x + y) % 100 === 0 ? '#333' : '#222'
});
}
}
// Event listeners for keyboard
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') keys.up = true;
if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') keys.down = true;
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = true;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = true;
if (e.key === 'Shift') keys.shift = true;
if (e.key === ' ') keys.space = true;
});
window.addEventListener('keyup', (e) => {
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') keys.up = false;
if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') keys.down = false;
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') keys.left = false;
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') keys.right = false;
if (e.key === 'Shift') keys.shift = false;
if (e.key === ' ') keys.space = false;
});
// Start game loop
gameLoop();
}
// Update game state
function update() {
// Update player movement
player.isMoving = false;
if (keys.up) {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed * (keys.shift ? 2 : 1));
player.direction = 0;
player.isMoving = true;
}
if (keys.down) {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed * (keys.shift ? 2 : 1));
player.direction = 2;
player.isMoving = true;
}
if (keys.left) {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed * (keys.shift ? 2 : 1));
player.direction = 3;
player.isMoving = true;
}
if (keys.right) {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed * (keys.shift ? 2 : 1));
player.direction = 1;
player.isMoving = true;
}
// Apply friction
if (!player.isMoving) {
player.speed = Math.max(player.speed - player.deceleration, 0);
}
// Update player position based on direction and speed
if (player.isMoving || player.speed > 0) {
switch (player.direction) {
case 0: // up
player.y -= player.speed;
break;
case 1: // right
player.x += player.speed;
break;
case 2: // down
player.y += player.speed;
break;
case 3: // left
player.x -= player.speed;
break;
}
// Boundary check for player
player.x = Math.max(player.width/2, Math.min(map.width - player.width/2, player.x));
player.y = Math.max(player.height/2, Math.min(map.height - player.height/2, player.y));
}
// Update vehicles
vehicles.forEach(vehicle => {
switch (vehicle.direction) {
case 0: // up
vehicle.y -= vehicle.speed;
if (vehicle.y < -vehicle.height) vehicle.y = map.height;
break;
case 1: // right
vehicle.x += vehicle.speed;
if (vehicle.x > map.width) vehicle.x = -vehicle.width;
break;
case 2: // down
vehicle.y += vehicle.speed;
if (vehicle.y > map.height) vehicle.y = -vehicle.height;
break;
case 3: // left
vehicle.x -= vehicle.speed;
if (vehicle.x < -vehicle.width) vehicle.x = map.width;
break;
}
});
// Update pedestrians
pedestrians.forEach(pedestrian => {
switch (pedestrian.direction) {
case 0: // up
pedestrian.y -= pedestrian.speed;
break;
case 1: // right
pedestrian.x += pedestrian.speed;
break;
case 2: // down
pedestrian.y += pedestrian.speed;
break;
case 3: // left
pedestrian.x -= pedestrian.speed;
break;
}
// Randomly change direction
if (Math.random() < 0.02) {
pedestrian.direction = Math.floor(Math.random() * 4);
}
// Boundary checking for pedestrians
if (pedestrian.x < 0) pedestrian.x = map.width;
if (pedestrian.x > map.width) pedestrian.x = 0;
if (pedestrian.y < 0) pedestrian.y = map.height;
if (pedestrian.y > map.height) pedestrian.y = 0;
});
// Update camera to follow player
camera.x = player.x - camera.width / 2;
camera.y = player.y - camera.height / 2;
// Boundary checking for camera
camera.x = Math.max(0, Math.min(map.width - camera.width, camera.x));
camera.y = Math.max(0, Math.min(map.height - camera.height, camera.y));
// Update UI
speedElement.textContent = Math.floor(player.speed * 20);
statusElement.textContent = player.isMoving ? (keys.shift ? "Sprinting" : "Moving") : "Idle";
}
// Render game
function render() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw roads
roads.forEach(road => {
ctx.fillStyle = road.color;
ctx.fillRect(road.x - camera.x, road.y - camera.y, road.width, road.height);
// Draw road lines
if (road.width > road.height) { // Horizontal road
ctx.fillStyle = '#fff';
for (let x = road.x; x < road.x + road.width; x += 40) {
ctx.fillRect(x - camera.x, road.y + road.height/2 - 2 - camera.y, 20, 4);
}
} else { // Vertical road
ctx.fillStyle = '#fff';
for (let y = road.y; y < road.y + road.height; y += 40) {
ctx.fillRect(road.x + road.width/2 - 2 - camera.x, y - camera.y, 4, 20);
}
}
});
// Draw buildings
buildings.forEach(building => {
ctx.fillStyle = building.color;
ctx.fillRect(building.x - camera.x, building.y - camera.y, building.width, building.height);
// Draw windows
ctx.fillStyle = '#337';
for (let y = building.y + 10; y < building.y + building.height - 10; y += 20) {
for (let x = building.x + 10; x < building.x + building.width - 10; x += 20) {
ctx.fillRect(x - camera.x, y - camera.y, 8, 12);
}
}
});
// Draw vehicles
vehicles.forEach(vehicle => {
ctx.fillStyle = vehicle.color;
ctx.fillRect(vehicle.x - camera.x, vehicle.y - camera.y, vehicle.width, vehicle.height);
// Draw wheels
ctx.fillStyle = '#222';
ctx.fillRect(vehicle.x - camera.x, vehicle.y - camera.y, 4, 2);
ctx.fillRect(vehicle.x - camera.x, vehicle.y + vehicle.height - 2 - camera.y, 4, 2);
ctx.fillRect(vehicle.x + vehicle.width - 4 - camera.x, vehicle.y - camera.y, 4, 2);
ctx.fillRect(vehicle.x + vehicle.width - 4 - camera.x, vehicle.y + vehicle.height - 2 - camera.y, 4, 2);
});
// Draw pedestrians
pedestrians.forEach(pedestrian => {
ctx.fillStyle = pedestrian.color;
ctx.beginPath();
ctx.arc(pedestrian.x - camera.x, pedestrian.y - camera.y, pedestrian.width/2, 0, Math.PI * 2);
ctx.fill();
});
// Draw player
ctx.fillStyle = player.color;
ctx.fillRect(player.x - player.width/2 - camera.x, player.y - player.height/2 - camera.y, player.width, player.height);
// Draw player direction indicator
ctx.fillStyle = '#ff0';
const indicatorSize = 5;
let indicatorX = player.x - camera.x;
let indicatorY = player.y - camera.y;
switch (player.direction) {
case 0: // up
indicatorY = player.y - player.height/2 - indicatorSize - camera.y;
break;
case 1: // right
indicatorX = player.x + player.width/2 + camera.x;
break;
case 2: // down
indicatorY = player.y + player.height/2 + indicatorSize - camera.y;
break;
case 3: // left
indicatorX = player.x - player.width/2 - indicatorSize - camera.x;
break;
}
ctx.fillRect(indicatorX - indicatorSize/2, indicatorY - indicatorSize/2, indicatorSize, indicatorSize);
// Draw minimap
renderMinimap();
}
// Render minimap
function renderMinimap() {
// Clear minimap
minimapCtx.clearRect(0, 0, minimap.width, minimap.height);
// Draw map background
const scaleX = minimap.width / map.width;
const scaleY = minimap.height / map.height;
// Draw tiles on minimap
map.tiles.forEach(tile => {
minimapCtx.fillStyle = tile.color;
minimapCtx.fillRect(
tile.x * scaleX,
tile.y * scaleY,
tile.width * scaleX,
tile.height * scaleY
);
});
// Draw roads on minimap
roads.forEach(road => {
minimapCtx.fillStyle = road.color;
minimapCtx.fillRect(
road.x * scaleX,
road.y * scaleY,
road.width * scaleX,
road.height * scaleY
);
});
// Draw buildings on minimap
buildings.forEach(building => {
minimapCtx.fillStyle = building.color;
minimapCtx.fillRect(
building.x * scaleX,
building.y * scaleY,
building.width * scaleX,
building.height * scaleY
);
});
// Draw vehicles on minimap
vehicles.forEach(vehicle => {
minimapCtx.fillStyle = vehicle.color;
minimapCtx.fillRect(
vehicle.x * scaleX,
vehicle.y * scaleY,
vehicle.width * scaleX,
vehicle.height * scaleY
);
});
// Draw pedestrians on minimap
pedestrians.forEach(pedestrian => {
minimapCtx.fillStyle = pedestrian.color;
minimapCtx.beginPath();
minimapCtx.arc(
pedestrian.x * scaleX,
pedestrian.y * scaleY,
pedestrian.width/2 * scaleX,
0,
Math.PI * 2
);
minimapCtx.fill();
});
// Draw player on minimap
minimapCtx.fillStyle = '#fff';
minimapCtx.fillRect(
(player.x - player.width/2) * scaleX,
(player.y - player.height/2) * scaleY,
player.width * scaleX,
player.height * scaleY
);
// Draw camera view on minimap
minimapCtx.strokeStyle = '#0af';
minimapCtx.lineWidth = 1;
minimapCtx.strokeRect(
camera.x * scaleX,
camera.y * scaleY,
camera.width * scaleX,
camera.height * scaleY
);
}
// Game loop
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// Start the game
init();
</script>
</body>
</html>
```
## Features of this GTA-style Game:
1. **Player Control**:
- Move with arrow keys or WASD
- Sprint with Shift key
- Direction indicator shows where the player is facing
2. **City Environment**:
- Grid-based city layout with roads and buildings
- Buildings with window details
- Road markings for realism
3. **NPCs**:
- Moving vehicles that follow road paths
- Randomly moving pedestrians
4. **Game Mechanics**:
- Acceleration and deceleration physics
- Collision boundaries with map edges
- Camera that follows the player
5. **UI Elements**:
- Speed display
- Player status indicator
- Minimap showing the entire city
- Camera view indicator on minimap
- On-screen controls reference
6. **Visual Design**:
- Neon blue color scheme with dark backgrounds
- Glowing effects and shadows
- Clean, modern UI with transparency effects
The game runs smoothly in any modern browser. You can move around the city, avoid collisions with buildings, and watch the minimap update in real-time as you explore.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mini-GTA (Top-Down Prototype)</title>
<style>
html,body{
margin:0;
height:100%;
overflow:hidden;
background:#222;
font-family:Arial,Helvetica,sans-serif;
color:#fff;
user-select:none;
}
#ui{
position:fixed;
top:0;
left:0;
width:100%;
height:100%;
pointer-events:none;
}
#minimap{
position:absolute;
right:10px;
bottom:10px;
width:160px;
height:160px;
border:2px solid #fff;
background:#000;
}
#help{
position:absolute;
left:10px;
bottom:10px;
font-size:14px;
background:rgba(0,0,0,.4);
padding:6px 10px;
border-radius:4px;
}
canvas{display:block;margin:0 auto;background:#444;}
</style>
</head>
<body>
<canvas id="game" width="800" height="600"></canvas>
<div id="ui">
<canvas id="minimap" width="160" height="160"></canvas>
<div id="help">
Arrow Keys / WASD – Move<br>
E – Enter / Exit Vehicle
</div>
</div>
<script>
/* ========= GLOBAL CONSTANTS ========= */
const MAP_W = 2000, MAP_H = 2000; // world size
const TILE = 64; // base tile (for roads & buildings)
const PLAYER_SIZE = 22;
const PLAYER_SPEED = 2.2;
const VEHICLE_SIZE = {w:40,h:70};
const VEHICLE_SPEED = 4.5;
const PEDESTRIAN_SIZE = 18;
const PEDESTRIAN_SPEED = 1.4;
/* ========= HELPER FUNCTIONS ========= */
const rand = (a,b)=>Math.random()*(b-a)+a;
const clamp = (v,min,max)=>Math.max(min,Math.min(max,v));
function rectsOverlap(a,b){
return !(a.x+a.w < b.x || a.x > b.x+b.w || a.y+a.h < b.y || a.y > b.y+b.h);
}
/* ========= INPUT ========= */
const keys={};
addEventListener('keydown',e=>{keys[e.key.toLowerCase()]=true});
addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false});
/* ========= CANVASES ========= */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('minimap');
const mCtx = miniCanvas.getContext('2d');
/* ========= WORLD SETUP ========= */
const buildings=[];
for(let i=0;i<140;i++){ // generate some random buildings
const w=rand(80,160), h=rand(80,160);
const x=rand(0,MAP_W-w), y=rand(0,MAP_H-h);
// snap to tile grid and leave roads clear
if(((x/TILE)|0)%2===1 || ((y/TILE)|0)%2===1) continue; // keep roads free
buildings.push({x,y,w,h});
}
function makeAxisAlignedPath(y){ // simple horizontal traffic route
return {from:{x:0,y},to:{x:MAP_W,y}};
}
/* ========= ENTITIES ========= */
class Entity{
constructor(x,y,w,h){
this.x=x;this.y=y;this.w=w;this.h=h;
}
bbox(){return {x:this.x,y:this.y,w:this.w,h:this.h};}
}
class Player extends Entity{
constructor(){
super(MAP_W/2,MAP_H/2,PLAYER_SIZE,PLAYER_SIZE);
this.speed=PLAYER_SPEED;
this.inVehicle=null;
this.animTimer=0;
this.frame=0;
}
update(dt){
if(this.inVehicle){
// while inside vehicle, control it
const v=this.inVehicle;
let dx=0,dy=0;
if(keys['w']||keys['arrowup']) dy-=1;
if(keys['s']||keys['arrowdown']) dy+=1;
if(keys['a']||keys['arrowleft']) dx-=1;
if(keys['d']||keys['arrowright']) dx+=1;
const len=Math.hypot(dx,dy);
if(len){dx/=len;dy/=len;}
v.x+=dx*VEHICLE_SPEED;
v.y+=dy*VEHICLE_SPEED;
// collision with buildings
for(const b of buildings){
if(rectsOverlap(v,b)){
v.x-=dx*VEHICLE_SPEED;
v.y-=dy*VEHICLE_SPEED;
}
}
// exit vehicle
if(keys['e']){
keys['e']=false;
this.x=v.x+v.w/2 - this.w/2;
this.y=v.y+v.h/2 - this.h/2;
this.inVehicle=null;
}
// keep inside map
v.x=clamp(v.x,0,MAP_W-v.w);
v.y=clamp(v.y,0,MAP_H-v.h);
}else{
let dx=0,dy=0;
if(keys['w']||keys['arrowup']) dy-=1;
if(keys['s']||keys['arrowdown']) dy+=1;
if(keys['a']||keys['arrowleft']) dx-=1;
if(keys['d']||keys['arrowright']) dx+=1;
const len=Math.hypot(dx,dy);
if(len){dx/=len;dy/=len;}
this.x+=dx*this.speed;
this.y+=dy*this.speed;
// collision with buildings
for(const b of buildings){
if(rectsOverlap(this,b)){
this.x-=dx*this.speed;
this.y-=dy*this.speed;
}
}
// enter vehicle?
if(keys['e']){
for(const car of vehicles){
const expand=8;
const zone={x:car.x-expand,y:car.y-expand,w:car.w+expand*2,h:car.h+expand*2};
if(rectsOverlap(this,zone)){
this.inVehicle=car;
keys['e']=false;
break;
}
}
}
// animation
this.animTimer+=dt;
if(this.animTimer>0.15){
this.animTimer=0;
this.frame=(this.frame+1)%4;
}
// stay inside map
this.x=clamp(this.x,0,MAP_W-this.w);
this.y=clamp(this.y,0,MAP_H-this.h);
}
}
draw(ctx,cx,cy){
if(this.inVehicle) return; // not visible
ctx.save();
ctx.translate(this.x-cx, this.y-cy);
ctx.fillStyle='#ffdf00';
ctx.fillRect(0,0,this.w,this.h);
// simple leg animation
ctx.fillStyle='#000';
if(this.frame%2){
ctx.fillRect(4,this.h-3,4,3);
ctx.fillRect(this.w-8,this.h-3,4,3);
}else{
ctx.fillRect(6,this.h-3,4,3);
}
ctx.restore();
}
}
class Vehicle extends Entity{
constructor(x,y,dir=1){
super(x,y,VEHICLE_SIZE.w,VEHICLE_SIZE.h);
this.dir=dir;
this.color=`hsl(${rand(0,360)},80%,50%)`;
}
update(){
if(player.inVehicle===this) return; // currently controlled
this.y+=0; // vertical unchanged
this.x+=this.dir*2.2;
if(this.x<0 || this.x>MAP_W-this.w) this.dir*=-1;
// simple collision with buildings
for(const b of buildings){
if(rectsOverlap(this,b)){
this.dir*=-1;
this.x+=this.dir*2.2;
}
}
}
draw(ctx,cx,cy){
ctx.save();
ctx.translate(this.x-cx, this.y-cy);
ctx.fillStyle=this.color;
ctx.fillRect(0,0,this.w,this.h);
ctx.fillStyle='#222';
ctx.fillRect(4,4,this.w-8, this.h-8); // windshield
ctx.restore();
}
}
class Pedestrian extends Entity{
constructor(x,y){
super(x,y,PEDESTRIAN_SIZE,PEDESTRIAN_SIZE);
this.vx=rand(-1,1); this.vy=rand(-1,1);
this.timer=rand(1,3);
this.color='#00d6ff';
}
update(dt){
this.timer-=dt;
if(this.timer<0){
this.vx=rand(-1,1); this.vy=rand(-1,1);
this.timer=rand(1,3);
}
const len=Math.hypot(this.vx,this.vy);
if(len){
const nx=this.vx/len*PEDESTRIAN_SPEED;
const ny=this.vy/len*PEDESTRIAN_SPEED;
this.x+=nx; this.y+=ny;
for(const b of buildings){
if(rectsOverlap(this,b)){
this.x-=nx; this.y-=ny;
this.vx*=-1; this.vy*=-1;
}
}
}
this.x=clamp(this.x,0,MAP_W-this.w);
this.y=clamp(this.y,0,MAP_H-this.h);
}
draw(ctx,cx,cy){
ctx.fillStyle=this.color;
ctx.fillRect(this.x-cx,this.y-cy,this.w,this.h);
}
}
/* ========= ENTITY LISTS ========= */
const player=new Player();
const vehicles=[];
for(let i=0;i<8;i++){
const x=rand(200,MAP_W-200);
const y=((i%4)*TILE*2)+TILE*1.5;
vehicles.push(new Vehicle(x,y,i%2?1:-1));
}
const pedestrians=[];
for(let i=0;i<25;i++){
pedestrians.push(new Pedestrian(rand(0,MAP_W),rand(0,MAP_H)));
}
/* ========= CAMERA ========= */
let camX=0,camY=0;
/* ========= MAIN LOOP ========= */
let last=0;
function loop(ts){
const dt=(ts-last)/1000;
last=ts;
update(dt);
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
/* ========= UPDATE ========= */
function update(dt){
player.update(dt);
for(const v of vehicles) v.update(dt);
for(const p of pedestrians) p.update(dt);
// keep camera centered on player or vehicle
const target=player.inVehicle?player.inVehicle:player;
camX = clamp(target.x + target.w/2 - canvas.width/2,0, MAP_W-canvas.width);
camY = clamp(target.y + target.h/2 - canvas.height/2,0, MAP_H-canvas.height);
}
/* ========= RENDER ========= */
function render(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawGround();
drawBuildings();
for(const v of vehicles) v.draw(ctx,camX,camY);
for(const p of pedestrians) p.draw(ctx,camX,camY);
player.draw(ctx,camX,camY);
drawMinimap();
}
function drawGround(){
// simple checkered tiles: roads every 2nd tile
const startX = Math.floor(camX/TILE)-1;
const startY = Math.floor(camY/TILE)-1;
const tilesX = Math.ceil(canvas.width/TILE)+2;
const tilesY = Math.ceil(canvas.height/TILE)+2;
for(let i=0;i<tilesX;i++){
for(let j=0;j<tilesY;j++){
const gx = (startX+i)*TILE;
const gy = (startY+j)*TILE;
const roadX = ((startX+i)%2)==1;
const roadY = ((startY+j)%2)==1;
ctx.fillStyle = (roadX||roadY)?'#666':'#3a3a3a';
ctx.fillRect(gx-camX, gy-camY, TILE, TILE);
}
}
}
function drawBuildings(){
ctx.fillStyle='#2b2b2b';
for(const b of buildings){
ctx.fillRect(b.x-camX,b.y-camY,b.w,b.h);
ctx.strokeStyle='#111';
ctx.strokeRect(b.x-camX,b.y-camY,b.w,b.h);
}
}
/* ========= MINIMAP ========= */
function drawMinimap(){
const scaleX = miniCanvas.width / MAP_W;
const scaleY = miniCanvas.height / MAP_H;
mCtx.fillStyle='#000';
mCtx.fillRect(0,0,miniCanvas.width,miniCanvas.height);
// roads are implicit (skipped for speed)
// buildings
mCtx.fillStyle='#444';
for(const b of buildings){
mCtx.fillRect(b.x*scaleX,b.y*scaleY,b.w*scaleX,b.h*scaleY);
}
// vehicles
mCtx.fillStyle='#ff5555';
for(const v of vehicles){
mCtx.fillRect(v.x*scaleX,v.y*scaleY,3,3);
}
// peds
mCtx.fillStyle='#00baff';
for(const p of pedestrians){
mCtx.fillRect(p.x*scaleX,p.y*scaleY,2,2);
}
// player
const target=player.inVehicle?player.inVehicle:player;
mCtx.fillStyle='#ffff00';
mCtx.fillRect(target.x*scaleX,target.y*scaleY,4,4);
mCtx.strokeStyle='#fff';
mCtx.strokeRect(0,0,miniCanvas.width,miniCanvas.height);
}
</script>
</body>
</html>
```Here is a complete, self-contained GTA-style top-down game clone. The entire game is written in a single block of HTML, including all CSS and JavaScript. You can save this code as an `.html` file and open it in any modern web browser to play.
**Controls:**
* **Move Player:** Arrow Keys or WASD
* **Enter/Exit Vehicle:** 'F' key
**Features:**
* Character movement and camera that follows the player.
* Walking animations with a 4-direction sprite sheet.
* Enterable vehicles with basic driving physics.
* Simple AI pedestrians that wander the map.
* Collision detection with buildings.
* An interactive minimap in the corner of the screen.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Top-Down Game</title>
<style>
body {
background-color: #333;
color: #fff;
font-family: 'Courier New', Courier, monospace;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
flex-direction: column;
overflow: hidden;
}
#game-container {
width: 800px;
height: 600px;
border: 2px solid #fff;
position: relative;
overflow: hidden;
background-color: #5a5a5a;
}
#world {
position: absolute;
background-image:
linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
background-size: 30px 30px;
}
.game-object {
position: absolute;
box-sizing: border-box;
}
#player {
width: 24px;
height: 24px;
/* Base64 encoded sprite sheet for the player character */
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIaSURBVHhe7doxb9pAFAZwl/8B/gI2QYtNQEGDAoUKEhQoSKgoiBAgQICEgAoFDRL8fwF0gA7+bLzYFnaxNTpD8vkgbN6d52dnZyYJAAAAAAAAAAC4LOfz+UW9s2n+M59PZV3P87pmWfcvSRJ9p0+S8L/+bM8F0tIHYE8B6UkzIIAFpCfdgAASYH3SDAhgAann/ZMGQADrSTcgQADqSbcgQADqSTcgQADqSbcgQADqSTcgQADqSbcgQADqSTcgQADqSbcgQADqSTcgQADqSbcgQADqSTcgQADqSbcgQADqSTcgQADqSbcgQADqSTcgQADqSTcgQADqSbcgQADqSTcgQADqSbcgAASYD7SDAhgAamH/0wYggH2gHTAACYD9QBswAAkgH2gHDGA+kG7AAOYD6QYMYD6QbsAA5gPpBgxgPpBuwADmA+kGDGA+kG7AAOYD6QYMYD6QbsAA5gPpBgxgPpBuwADmA+kGDGA+kG7AAOYD6QYMYD6QbsAA5gPpBgxgPpBuwADmA+kGDGACYDfQAAhgAmA30AACGACYDXSAAAhgAmA30AACGACYDXSAAAhgAmA30AACGHgaaIcGIIBDB91AAyCAQwfdQAMggEMH3UADIIBDB91AAyCAQwfdQAMggEMH3UADIIBDB91AAyCAQwfdQAMggEMH3UADIIBDB91AAyCAQwfdQAMggEMH3UADIIBDB91AAyCAQwfdQAMggEMH3UADIIBDB91AAyCAQwfdQAMggEMH3UADAAAAAAAAAAA4j599AO0E0eY2Xq0MAAAAAElFTkSuQmCC');
/* Size of the entire sprite sheet */
background-size: 96px 96px;
/* Start with the first frame (standing still, facing down) */
background-position: 0px 0px;
}
.building {
background-color: #404040;
border: 2px solid #333;
}
.pedestrian {
width: 20px;
height: 20px;
background-color: #f0f;
border-radius: 50%;
}
.vehicle {
width: 80px;
height: 40px;
background-color: #007bff;
border: 2px solid #0056b3;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}
.vehicle::before {
content: '';
width: 30px;
height: 30px;
background: rgba(173, 216, 230, 0.7); /* Light blue for windshield */
position: absolute;
right: 5px;
}
#ui-container {
position: absolute;
bottom: 15px;
left: 15px;
z-index: 10;
}
#minimap-container {
width: 160px;
height: 120px;
background-color: rgba(0, 0, 0, 0.6);
border: 2px solid #fff;
position: relative;
overflow: hidden;
}
.minimap-dot {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
}
#minimap-player {
background-color: #0f0;
}
.minimap-vehicle {
background-color: #007bff;
width: 6px;
height: 3px;
border-radius: 0;
}
#instructions {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
font-size: 12px;
text-align: right;
}
</style>
</head>
<body>
<h1>Basic GTA Clone</h1>
<div id="game-container">
<div id="world">
<!-- Game objects will be dynamically added here -->
</div>
<div id="ui-container">
<div id="minimap-container">
<!-- Minimap dots will be dynamically added here -->
</div>
</div>
<div id="instructions">
- Arrow Keys / WASD to Move<br>
- 'F' to Enter/Exit Vehicle
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- GAME SETUP ---
const gameContainer = document.getElementById('game-container');
const world = document.getElementById('world');
const minimap = document.getElementById('minimap-container');
const worldSize = { width: 3200, height: 2400 };
world.style.width = `${worldSize.width}px`;
world.style.height = `${worldSize.height}px`;
const keys = {
w: false, a: false, s: false, d: false,
ArrowUp: false, ArrowLeft: false, ArrowDown: false, ArrowRight: false,
f: false
};
// --- ENTITY SETUP ---
const player = {
x: worldSize.width / 2,
y: worldSize.height / 2,
width: 24,
height: 24,
speed: 4,
el: createGameObject('player', 'game-object'),
inVehicle: null,
animation: {
frame: 0,
timer: 0,
frameDelay: 5, // update frame every 5 game ticks
direction: 0, // 0:down, 1:left, 2:right, 3:up
}
};
const entities = {
buildings: [],
vehicles: [],
pedestrians: []
};
// --- HELPER FUNCTIONS ---
function createGameObject(id, className) {
const el = document.createElement('div');
el.id = id;
el.className = className;
world.appendChild(el);
return el;
}
function createMinimapDot(id, className) {
const el = document.createElement('div');
el.id = id;
el.className = className;
minimap.appendChild(el);
return el;
}
// --- INITIALIZE GAME WORLD ---
function init() {
// Create Player
world.appendChild(player.el);
player.minimapDot = createMinimapDot('minimap-player', 'minimap-dot');
// Create Buildings
const buildingLayout = [
{ x: 200, y: 200, w: 200, h: 150 },
{ x: 500, y: 300, w: 150, h: 300 },
{ x: 800, y: 100, w: 100, h: 400 },
{ x: 1200, y: 600, w: 500, h: 180 },
{ x: 1300, y: 900, w: 200, h: 200 },
{ x: 1800, y: 800, w: 250, h: 150 },
{ x: 2200, y: 400, w: 180, h: 350 },
{ x: 2500, y: 1000, w: 400, h: 100 },
{ x: 100, y: 1500, w: 150, h: 600 },
{ x: 600, y: 1800, w: 300, h: 150 },
];
buildingLayout.forEach((b, i) => {
const building = {
x: b.x, y: b.y, width: b.w, height: b.h,
el: createGameObject(`building-${i}`, 'game-object building')
};
entities.buildings.push(building);
});
// Create Vehicles
const vehicleLayout = [
{ x: 450, y: 250, angle: 0 },
{ x: 950, y: 400, angle: Math.PI / 2 },
{ x: 1600, y: 500, angle: Math.PI / 4 },
];
vehicleLayout.forEach((v, i) => {
const vehicle = {
x: v.x, y: v.y, width: 80, height: 40,
angle: v.angle, speed: 0,
el: createGameObject(`vehicle-${i}`, 'game-object vehicle'),
minimapDot: createMinimapDot(`minimap-vehicle-${i}`, 'minimap-dot minimap-vehicle')
};
entities.vehicles.push(vehicle);
});
// Create Pedestrians
for (let i = 0; i < 20; i++) {
const ped = {
x: Math.random() * worldSize.width,
y: Math.random() * worldSize.height,
width: 20, height: 20, speed: 1 + Math.random(),
el: createGameObject(`ped-${i}`, 'game-object pedestrian'),
dx: (Math.random() - 0.5) * 2,
dy: (Math.random() - 0.5) * 2,
moveTimer: Math.random() * 200,
};
// Ensure ped doesn't spawn inside a building
let inBuilding = true;
while (inBuilding) {
inBuilding = false;
for (const building of entities.buildings) {
if (checkCollision(ped, building)) {
ped.x = Math.random() * worldSize.width;
ped.y = Math.random() * worldSize.height;
inBuilding = true;
break;
}
}
}
entities.pedestrians.push(ped);
}
gameLoop();
}
// --- COLLISION DETECTION ---
function checkCollision(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
// --- INPUT HANDLING ---
document.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.key)) keys[e.key] = true;
});
document.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.key)) keys[e.key] = false;
});
// --- UPDATE FUNCTIONS ---
function updatePlayer() {
let dx = 0;
let dy = 0;
let isMoving = false;
if (keys.a || keys.ArrowLeft) { dx -= 1; player.animation.direction = 1; isMoving = true; }
if (keys.d || keys.ArrowRight) { dx += 1; player.animation.direction = 2; isMoving = true; }
if (keys.w || keys.ArrowUp) { dy -= 1; player.animation.direction = 3; isMoving = true; }
if (keys.s || keys.ArrowDown) { dy += 1; player.animation.direction = 0; isMoving = true; }
// Normalize diagonal movement
if (dx !== 0 && dy !== 0) {
const magnitude = Math.sqrt(dx * dx + dy * dy);
dx = (dx / magnitude);
dy = (dy / magnitude);
}
const nextX = player.x + dx * player.speed;
const nextY = player.y + dy * player.speed;
// Collision checks
const tempPlayerX = { ...player, x: nextX };
let collisionX = false;
for (const building of entities.buildings) {
if (checkCollision(tempPlayerX, building)) {
collisionX = true;
break;
}
}
if (!collisionX) player.x = nextX;
const tempPlayerY = { ...player, y: nextY };
let collisionY = false;
for (const building of entities.buildings) {
if (checkCollision(tempPlayerY, building)) {
collisionY = true;
break;
}
}
if (!collisionY) player.y = nextY;
// Clamp player position to world bounds
player.x = Math.max(0, Math.min(worldSize.width - player.width, player.x));
player.y = Math.max(0, Math.min(worldSize.height - player.height, player.y));
// Animation
if (isMoving) {
player.animation.timer++;
if (player.animation.timer >= player.animation.frameDelay) {
player.animation.frame = (player.animation.frame + 1) % 4;
player.animation.timer = 0;
}
} else {
player.animation.frame = 0; // Standing frame
}
// Interaction
if (keys.f) {
for (const vehicle of entities.vehicles) {
const dist = Math.hypot(player.x - vehicle.x, player.y - vehicle.y);
if (dist < 80) { // Interaction radius
player.inVehicle = vehicle;
player.el.style.display = 'none';
break;
}
}
keys.f = false; // Prevent holding key down
}
}
function updateVehicle(vehicle) {
// Steering
const turnSpeed = 0.05;
if (keys.a || keys.ArrowLeft) vehicle.angle -= turnSpeed * (vehicle.speed/5);
if (keys.d || keys.ArrowRight) vehicle.angle += turnSpeed * (vehicle.speed/5);
// Acceleration/Deceleration
const acceleration = 0.1;
const friction = 0.02;
const maxSpeed = 8;
if (keys.w || keys.ArrowUp) vehicle.speed = Math.min(maxSpeed, vehicle.speed + acceleration);
if (keys.s || keys.ArrowDown) vehicle.speed = Math.max(-maxSpeed/2, vehicle.speed - acceleration);
// Apply friction
if (vehicle.speed > 0) vehicle.speed = Math.max(0, vehicle.speed - friction);
if (vehicle.speed < 0) vehicle.speed = Math.min(0, vehicle.speed + friction);
const nextX = vehicle.x + Math.cos(vehicle.angle) * vehicle.speed;
const nextY = vehicle.y + Math.sin(vehicle.angle) * vehicle.speed;
// Vehicle collision is simplified for this demo
const tempVehicle = { x: nextX, y: nextY, width: vehicle.width, height: vehicle.height };
let collision = false;
for (const building of entities.buildings) {
if (checkCollision(tempVehicle, building)) {
collision = true;
break;
}
}
if(!collision) {
vehicle.x = nextX;
vehicle.y = nextY;
} else {
vehicle.speed = -vehicle.speed * 0.5; // Bounce off
}
// Clamp to world bounds
vehicle.x = Math.max(0, Math.min(worldSize.width - vehicle.width, vehicle.x));
vehicle.y = Math.max(0, Math.min(worldSize.height - vehicle.height, vehicle.y));
// Center player on vehicle
player.x = vehicle.x;
player.y = vehicle.y;
// Exit vehicle
if (keys.f) {
player.el.style.display = 'block';
player.inVehicle = null;
player.x = vehicle.x + vehicle.width; // Place player next to car
player.y = vehicle.y;
keys.f = false;
}
}
function updatePedestrians() {
entities.pedestrians.forEach(ped => {
ped.moveTimer--;
if (ped.moveTimer <= 0) {
ped.dx = (Math.random() - 0.5) * 2;
ped.dy = (Math.random() - 0.5) * 2;
ped.moveTimer = 100 + Math.random() * 200;
}
const nextX = ped.x + ped.dx * ped.speed;
const nextY = ped.y + ped.dy * ped.speed;
// Collision (simplified check)
const tempPed = { ...ped, x: nextX, y: nextY };
let collision = false;
for (const building of entities.buildings) {
if (checkCollision(tempPed, building)) {
collision = true;
break;
}
}
if (!collision &&
nextX > 0 && nextX < worldSize.width - ped.width &&
nextY > 0 && nextY < worldSize.height - ped.height) {
ped.x = nextX;
ped.y = nextY;
} else {
// Change direction on collision
ped.dx = -ped.dx;
ped.dy = -ped.dy;
}
});
}
// --- RENDER FUNCTION ---
function render() {
// Update camera to follow player/vehicle
const camX = player.x - gameContainer.offsetWidth / 2;
const camY = player.y - gameContainer.offsetHeight / 2;
world.style.transform = `translate(${-camX}px, ${-camY}px)`;
// Update player element
if (player.inVehicle) {
// Handled in updateVehicle
} else {
player.el.style.transform = `translate(${player.x}px, ${player.y}px)`;
// Update sprite animation
const frameSize = 24; // size of one animation frame
const bgX = -(player.animation.frame * frameSize);
const bgY = -(player.animation.direction * frameSize);
player.el.style.backgroundPosition = `${bgX}px ${bgY}px`;
}
// Update all entities
entities.buildings.forEach(b => {
b.el.style.width = `${b.width}px`;
b.el.style.height = `${b.height}px`;
b.el.style.transform = `translate(${b.x}px, ${b.y}px)`;
});
entities.vehicles.forEach(v => {
v.el.style.transform = `translate(${v.x}px, ${v.y}px) rotate(${v.angle}rad)`;
});
entities.pedestrians.forEach(p => {
p.el.style.transform = `translate(${p.x}px, ${p.y}px)`;
});
// Update minimap
const scaleX = minimap.offsetWidth / worldSize.width;
const scaleY = minimap.offsetHeight / worldSize.height;
player.minimapDot.style.transform = `translate(${player.x * scaleX}px, ${player.y * scaleY}px)`;
entities.vehicles.forEach(v => {
v.minimapDot.style.transform = `translate(${v.x * scaleX}px, ${v.y * scaleY}px) rotate(${v.angle}rad)`;
});
}
// --- GAME LOOP ---
function gameLoop() {
// Update logic
if (player.inVehicle) {
updateVehicle(player.inVehicle);
} else {
updatePlayer();
}
updatePedestrians();
// Render screen
render();
requestAnimationFrame(gameLoop);
}
// Start the game
init();
});
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini GTA-Style Top-Down Game</title>
<style>
body {
background: #222;
margin: 0;
overflow: hidden;
user-select: none;
}
#gameContainer {
position: relative;
width: 800px;
height: 600px;
margin: 40px auto;
background: #444;
border: 5px solid #111;
box-shadow: 0 6px 32px #111b;
}
canvas {
display: block;
background: #87b0e6;
}
#minimap {
position: absolute;
width: 170px;
height: 130px;
right: 16px;
bottom: 16px;
border: 2px solid #333;
background: #222c;
z-index: 2;
pointer-events: none;
box-sizing: border-box;
}
@media (max-width: 900px) {
#gameContainer {
width: 100vw;
height: 80vw;
max-height: 80vh;
max-width: 97vw;
}
#minimap {
width: 22vw;
height: 17vw;
}
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="game" width="800" height="600"></canvas>
<canvas id="minimap" width="170" height="130"></canvas>
</div>
<script>
/**
* --- Mini GTA-Style Top-Down Game Demo ---
* Keys: WASD or Arrow Keys to move
* Space near a vehicle to enter/exit
* Player collides with buildings, vehicles, and pedestrians
* Simple sprite "animations" (2-frame walking)
* Vehicles and pedestrians move around the city
*/
// Map parameters
const TILE_SIZE = 64;
const MAP_COLS = 25;
const MAP_ROWS = 20;
// Definitions for buildings (rectangular "blocking" tiles)
const buildings = [
{x:2, y:2, w:6, h:3},
{x:8, y:5, w:5, h:3},
{x:15, y:10, w:7, h:4},
{x:7, y:13, w:4, h:5},
{x:4, y:7, w:3, h:4},
{x:17, y:3, w:4, h:4},
{x:19, y:16, w:5, h:3}
];
// Player parameters
const player = {
x: TILE_SIZE*12 + TILE_SIZE/2,
y: TILE_SIZE*10 + TILE_SIZE/2,
w: 28,
h: 32,
speed: 2.4,
spriteIdx: 0,
walking: false,
dir: "down", // up,down,left,right
inVehicle: false,
ridingVehicle: null
};
// Vehicle templates
const vehicleTypes = [
{
color: "#a33", w:46, h:24, speed:1.9,
seatOffset: {x:18, y:12}
}, {
color: "#2ea", w:40, h:18, speed:2.2,
seatOffset: {x:13, y:9}
}
];
// Vehicles
const vehicles = [
{x: 15*TILE_SIZE + 20, y: 7*TILE_SIZE+30, dir: 0, vtype: 0, vx:0, vy:0, passenger: null},
{x: 22*TILE_SIZE + 20, y: 16*TILE_SIZE+10, dir: 90, vtype: 1, vx:0, vy:0, passenger: null}
];
// Pedestrians
const PED_COLORS = ["#f4e", "#ed6", "#ace", "#ada", "#faf", "#fe9"];
let pedestrians = [];
for(let i=0; i<6; i++) {
const col = Math.floor(Math.random()*MAP_COLS);
const row = Math.floor(Math.random()*MAP_ROWS);
const px = (col+0.5)*TILE_SIZE, py = (row+0.5)*TILE_SIZE;
pedestrians.push({
x: px + (Math.random()-0.5)*TILE_SIZE*0.8,
y: py + (Math.random()-0.5)*TILE_SIZE*0.8,
w: 24, h: 29,
speed: 1.2+Math.random(),
color: PED_COLORS[i%PED_COLORS.length],
dir:"down",
spriteIdx: 0,
walking: false,
aiTimer: 0,
target: null
});
}
// Game canvas
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const minimap = document.getElementById('minimap');
const mctx = minimap.getContext('2d');
const keys = {};
const keymap = {
"w":"up","a":"left","s":"down","d":"right",
"arrowup":"up","arrowleft":"left","arrowdown":"down","arrowright":"right"
};
// Main game loop data
let lastNow = performance.now();
let animFrame = 0;
// Utility: pixel collision rectangle
function rectsCollide(a, b) {
return !(
((a.y + a.h) <= b.y) ||
(a.y >= (b.y + b.h)) ||
((a.x + a.w) <= b.x) ||
(a.x >= (b.x + b.w))
);
}
// Map tile drawing (roads and pavement)
function drawMap(ctx, offsetX=0, offsetY=0) {
// Road
ctx.fillStyle = "#2d3035";
ctx.fillRect(0, 0, MAP_COLS*TILE_SIZE, MAP_ROWS*TILE_SIZE);
// Pavement blocks
for(let y=0; y<MAP_ROWS; y++) for(let x=0; x<MAP_COLS; x++) {
ctx.strokeStyle = "#bdbdbd";
ctx.lineWidth = 0.5;
ctx.strokeRect(
x*TILE_SIZE + 4,
y*TILE_SIZE + 4,
TILE_SIZE-8, TILE_SIZE-8
);
}
// Buildings
for(const b of buildings) {
ctx.fillStyle = "#5b5a4d";
ctx.fillRect(b.x*TILE_SIZE, b.y*TILE_SIZE, b.w*TILE_SIZE, b.h*TILE_SIZE);
ctx.strokeStyle = "#423";
ctx.lineWidth = 2;
ctx.strokeRect(b.x*TILE_SIZE, b.y*TILE_SIZE, b.w*TILE_SIZE, b.h*TILE_SIZE);
// Doors
ctx.fillStyle="#fff8";
ctx.fillRect((b.x+0.5)*TILE_SIZE-8, (b.y+b.h)*TILE_SIZE-8, 16, 16);
}
// Map road lines
ctx.strokeStyle = "#fc3";
for(let i=2; i<MAP_ROWS; i+=4) {
ctx.setLineDash([18,22]);
ctx.beginPath();
ctx.moveTo(0, i*TILE_SIZE+TILE_SIZE/2);
ctx.lineTo(MAP_COLS*TILE_SIZE, i*TILE_SIZE+TILE_SIZE/2);
ctx.stroke();
}
ctx.setLineDash([]);
}
// Player's movement and collisions
function tryMove(entity, dx, dy, isPlayer=false) {
let nx = entity.x+dx, ny = entity.y+dy;
const entRect = {x:nx-entity.w/2, y:ny-entity.h/2, w:entity.w, h:entity.h};
// Map limits
if(entRect.x<0 || entRect.y<0 ||
entRect.x+entity.w > MAP_COLS*TILE_SIZE ||
entRect.y+entity.h > MAP_ROWS*TILE_SIZE)
return false;
// Buildings
for(const b of buildings) {
const rect = {
x:b.x*TILE_SIZE, y:b.y*TILE_SIZE,
w:b.w*TILE_SIZE, h:b.h*TILE_SIZE
};
if(rectsCollide(entRect,rect)) return false;
}
// Vehicles
for(const v of vehicles) {
if(isPlayer && player.inVehicle && v === player.ridingVehicle) continue;
if(v.passenger && entity===v.passenger) continue;
const vdef = vehicleTypes[v.vtype];
const rect = {
x: v.x-vdef.w/2, y: v.y-vdef.h/2,
w: vdef.w, h: vdef.h
};
if(rectsCollide(entRect,rect)) return false;
}
// Pedestrians
if(isPlayer) {
for(const ped of pedestrians) {
if(rectsCollide(entRect,{
x: ped.x-ped.w/2, y: ped.y-ped.h/2, w: ped.w, h: ped.h
})) return false;
}
}
// All clear!
entity.x = nx;
entity.y = ny;
return true;
}
// Player controls and animation frame
function updatePlayer(dt) {
if(player.inVehicle && player.ridingVehicle) {
// Vehicle movement
let mdx = 0, mdy = 0;
if(keys["w"]||keys["arrowup"]) mdy -= 1;
if(keys["s"]||keys["arrowdown"]) mdy += 1;
if(keys["a"]||keys["arrowleft"]) mdx -= 1;
if(keys["d"]||keys["arrowright"]) mdx += 1;
let magnitude = Math.hypot(mdx,mdy);
let moved = false;
if(magnitude) {
mdx /= magnitude;
mdy /= magnitude;
const vdef = vehicleTypes[player.ridingVehicle.vtype];
const speed = vdef.speed * 1.25;
const dx = mdx*speed, dy=mdy*speed;
// Try vehicle move (collision w/buildings, other vehicles)
let nx = player.ridingVehicle.x+dx, ny = player.ridingVehicle.y+dy;
const vrect = {
x:nx-vdef.w/2, y:ny-vdef.h/2,
w:vdef.w, h:vdef.h
};
let hit = false;
// Hit buildings?
for(const b of buildings) {
const rect = {
x:b.x*TILE_SIZE, y:b.y*TILE_SIZE,
w:b.w*TILE_SIZE, h:b.h*TILE_SIZE
};
if(rectsCollide(vrect,rect)) { hit = true; break; }
}
// Hit other vehicles?
for(const v of vehicles) {
if(v === player.ridingVehicle) continue;
const vt = vehicleTypes[v.vtype];
const v2rect = {
x: v.x-vt.w/2, y: v.y-vt.h/2,
w: vt.w, h: vt.h
};
if(rectsCollide(vrect, v2rect)) { hit=true; break; }
}
if(!hit) {
player.ridingVehicle.x=nx;
player.ridingVehicle.y=ny;
moved = true;
}
}
} else {
// Player on foot
let mdx = 0, mdy = 0;
if(keys["w"]||keys["arrowup"]) mdy -= 1;
if(keys["s"]||keys["arrowdown"]) mdy += 1;
if(keys["a"]||keys["arrowleft"]) mdx -= 1;
if(keys["d"]||keys["arrowright"]) mdx += 1;
let magnitude = Math.hypot(mdx,mdy);
player.walking = magnitude>0;
if(magnitude) {
mdx /= magnitude;
mdy /= magnitude;
if(Math.abs(mdx)>Math.abs(mdy)) {
player.dir = mdx<0 ? "left":"right";
} else {
player.dir = mdy<0 ? "up":"down";
}
tryMove(player, mdx*player.speed, mdy*player.speed, true);
}
}
}
// Vehicles simple AI - random driving
function updateVehicles(dt) {
vehicles.forEach(v=>{
if(v.passenger) return; // player driving
// Random moving: choose dir every 3s
if(!v.aiTimer || v.aiTimer<=0) {
v.aiTimer = 180+Math.random()*180;
const angle = Math.random()*Math.PI*2;
v.vx = Math.cos(angle)*(1.2+Math.random()*0.5);
v.vy = Math.sin(angle)*(1.2+Math.random()*0.5);
} else {
v.aiTimer -= dt;
}
// Try to move
const vdef = vehicleTypes[v.vtype];
let nx = v.x+v.vx, ny = v.y+v.vy;
let vrect = {x:nx-vdef.w/2, y:ny-vdef.h/2, w:vdef.w, h:vdef.h};
// Hit test buildings, other cars
let blocked=false;
for(const b of buildings) {
const rect = {
x:b.x*TILE_SIZE, y:b.y*TILE_SIZE,
w:b.w*TILE_SIZE, h:b.h*TILE_SIZE
};
if(rectsCollide(vrect,rect)) { blocked=true; break;}
}
for(const v2 of vehicles) {
if(v2===v) continue;
const v2def = vehicleTypes[v2.vtype];
const rect2 = {x:v2.x-v2def.w/2, y:v2.y-v2def.h/2,
w:v2def.w, h:v2def.h};
if(rectsCollide(vrect,rect2)) { blocked=true; break;}
}
if(!blocked) {
v.x = nx; v.y = ny;
} else {
// Bump off
v.vx *= -1; v.vy *= -1;
}
});
}
// Pedestrians simple AI - random walking
function updatePedestrians(dt) {
pedestrians.forEach(ped=>{
if(ped.aiTimer<=0 || !ped.target) {
// Pick new random point within a region
ped.target = {
x: ped.x + (Math.random()-0.5)*TILE_SIZE*4,
y: ped.y + (Math.random()-0.5)*TILE_SIZE*4
};
ped.aiTimer = 40 + Math.random()*100;
ped.walking = Math.random()>0.3;
} else ped.aiTimer -= dt;
// Walk to target
if(ped.walking) {
let dx = ped.target.x-ped.x, dy = ped.target.y-ped.y;
if(Math.hypot(dx,dy)<3) {
ped.walking=false; pedal=0; return;
}
const mag=Math.hypot(dx,dy);
if(mag) {
dx/=mag; dy/=mag;
if(Math.abs(dx)>Math.abs(dy)) ped.dir = dx<0 ? "left":"right";
else ped.dir = dy<0 ? "up":"down";
tryMove(ped, dx*ped.speed, dy*ped.speed);
}
}
});
}
// Animate and render player
function drawPlayer(ctx, offsetX, offsetY) {
const px=player.x-offsetX, py=player.y-offsetY;
// Body
ctx.save();
ctx.translate(px, py);
// Draw shadow
ctx.beginPath();
ctx.ellipse(0,14,13,7,0,0,Math.PI*2);
ctx.fillStyle="rgba(0,0,0,0.3)";
ctx.fill();
// Body shape and 2-frame animation
ctx.fillStyle = "#46c";
// Animate motion (vertical "bob")
let bob = (player.walking ? Math.abs(Math.sin(animFrame/7))*3:0);
ctx.save();
ctx.translate(0,bob);
// Draw head
ctx.beginPath();
ctx.ellipse(0,-10,10,12,0,0,2*Math.PI);
ctx.fillStyle = "#eac";
ctx.fill();
// Arms
ctx.strokeStyle="#eac";
ctx.lineWidth=4;
ctx.beginPath();
if(player.walking && (animFrame%18)<9) {
ctx.moveTo(-11,-5); ctx.lineTo(-15,14);
ctx.moveTo(11,-5); ctx.lineTo(15,14);
} else {
ctx.moveTo(-11,-5); ctx.lineTo(-15,8);
ctx.moveTo(11,-5); ctx.lineTo(15,8);
}
ctx.stroke();
// Body
ctx.fillStyle="#46c";
ctx.fillRect(-11,-2,22,25);
// Legs
ctx.save();
ctx.translate(0,23);
if(player.walking) {
const sw = (animFrame%18)<9 ? -6 : 6;
ctx.rotate(sw/50);
ctx.fillRect(-6,0,10,10);
ctx.rotate(-sw/30);
ctx.fillRect(2,0,10,10);
} else {
ctx.fillRect(-6,0,10,10);
ctx.fillRect(2,0,10,10);
}
ctx.restore();
ctx.restore();
// Outline
ctx.strokeStyle="#222b";
ctx.lineWidth=2.5;
ctx.strokeRect(-player.w/2,-player.h/2,player.w,player.h);
ctx.restore();
}
// Drawing vehicles
function drawVehicle(ctx, v, offsetX, offsetY) {
const vdef = vehicleTypes[v.vtype];
const x = v.x-offsetX, y = v.y-offsetY;
ctx.save();
ctx.translate(x,y);
ctx.rotate((v.dir||0)*Math.PI/180);
// Shadow
ctx.save();
ctx.globalAlpha=0.33;
ctx.fillStyle="#000";
ctx.ellipse(0, 5, vdef.w/2, vdef.h/2, 0, 0, Math.PI*2);
ctx.fill();
ctx.restore();
// Body
ctx.fillStyle = vdef.color;
ctx.fillRect(-vdef.w/2, -vdef.h/2, vdef.w, vdef.h);
// Roof highlight
ctx.fillStyle="#fff6";
ctx.fillRect(-vdef.w/4, -vdef.h/4, vdef.w/2, vdef.h/2);
// Wheels
ctx.fillStyle="#232";
ctx.fillRect(-vdef.w/2, -vdef.h/2+3,4,6);
ctx.fillRect(vdef.w/2-4, -vdef.h/2+3,4,6);
ctx.fillRect(-vdef.w/2, vdef.h/2-6-3,4,6);
ctx.fillRect(vdef.w/2-4, vdef.h/2-6-3,4,6);
// Player inside car
if(v.passenger) {
ctx.beginPath();
ctx.arc(vdef.seatOffset.x-vdef.w/2, vdef.seatOffset.y-vdef.h/2, 8,0,Math.PI*2);
ctx.fillStyle="#eac";
ctx.fill();
}
ctx.restore();
}
// Drawing pedestrians
function drawPedestrian(ctx, ped, offsetX, offsetY) {
const x=ped.x-offsetX, y=ped.y-offsetY;
ctx.save();
ctx.translate(x,y);
// Shadow
ctx.beginPath();
ctx.ellipse(0,12,10,5,0,0,Math.PI*2);
ctx.fillStyle = "rgba(0,0,0,0.24)";
ctx.fill();
let bob = (ped.walking ? Math.abs(Math.sin(animFrame/7))*2:0);
ctx.save();
ctx.translate(0,bob);
// Head
ctx.beginPath();
ctx.ellipse(0,-8,8,10,0,0,Math.PI*2);
ctx.fillStyle = "#d0b";
ctx.fill();
// Body
ctx.fillStyle = ped.color;
ctx.fillRect(-9,-2,18,18);
// Legs
ctx.fillStyle = "#fcf";
ctx.fillRect(-7,16, 5,8);
ctx.fillRect(2,16, 5,8);
ctx.restore();
// Outline
ctx.strokeStyle="#222c";
ctx.strokeRect(-ped.w/2,-ped.h/2,ped.w,ped.h);
ctx.restore();
}
// Center camera on player (minimap and main)
function getCamera() {
let camX=player.x-canvas.width/2, camY=player.y-canvas.height/2;
camX=Math.max(0,Math.min(camX,MAP_COLS*TILE_SIZE-canvas.width));
camY=Math.max(0,Math.min(camY,MAP_ROWS*TILE_SIZE-canvas.height));
return {x:camX, y:camY};
}
// Minimap rendering
function drawMinimap() {
mctx.clearRect(0,0,minimap.width, minimap.height);
// Scale
const scaleX = minimap.width/(MAP_COLS*TILE_SIZE);
const scaleY = minimap.height/(MAP_ROWS*TILE_SIZE);
// Roads
mctx.fillStyle="#2c2";
mctx.fillRect(0,0,minimap.width,minimap.height);
// Buildings
for(const b of buildings) {
mctx.fillStyle="#432";
mctx.fillRect(
b.x*TILE_SIZE*scaleX,
b.y*TILE_SIZE*scaleY,
b.w*TILE_SIZE*scaleX,
b.h*TILE_SIZE*scaleY
);
}
// Vehicles
for(const v of vehicles) {
const vdef = vehicleTypes[v.vtype];
mctx.fillStyle=vdef.color;
mctx.fillRect(
(v.x-vdef.w/2)*scaleX,
(v.y-vdef.h/2)*scaleY,
vdef.w*scaleX,
vdef.h*scaleY);
}
// Pedestrians
for(const ped of pedestrians) {
mctx.fillStyle=ped.color;
mctx.beginPath();
mctx.arc(
ped.x*scaleX,
ped.y*scaleY,
7*scaleX,0,2*Math.PI
);
mctx.fill();
}
// Player
mctx.strokeStyle="#fff";
mctx.lineWidth=2;
mctx.beginPath();
mctx.arc(player.x*scaleX, player.y*scaleY,8*scaleX,0,2*Math.PI);
mctx.stroke();
mctx.beginPath();
mctx.arc(player.x*scaleX, player.y*scaleY,4*scaleX,0,2*Math.PI);
mctx.fillStyle="#49f";
mctx.fill();
}
// Game logic and rendering main loop
function gameLoop(now) {
const dt = (now-lastNow)*0.08;
lastNow=now; animFrame++;
updatePlayer(dt);
updateVehicles(dt);
updatePedestrians(dt);
// Check if near vehicle
let nearVehicle = false, targetVehicle = null;
if(!player.inVehicle) {
for(const v of vehicles) {
const vdef = vehicleTypes[v.vtype];
const dist = Math.hypot(
player.x-v.x,
player.y-v.y
);
if(dist<35+vdef.w/2) {
nearVehicle=true;
targetVehicle = v;
break;
}
}
}
// CAMERA
const cam = getCamera();
// Main rendering
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw map
drawMap(ctx,cam.x,cam.y);
// Draw vehicles
vehicles.forEach(v=>drawVehicle(ctx,v,cam.x,cam.y));
// Draw pedestrians
pedestrians.forEach(ped=>drawPedestrian(ctx,ped,cam.x,cam.y));
// Draw player or if in vehicle, draw above vehicle
if(player.inVehicle && player.ridingVehicle) {
// Draw player on seat, on top of car
drawVehicle(ctx,player.ridingVehicle,cam.x,cam.y);
drawPlayer(ctx,cam.x,cam.y);
} else {
drawPlayer(ctx,cam.x,cam.y);
if(nearVehicle && targetVehicle) {
// Hint: "Press Space to Enter"
ctx.save();
ctx.font="19px Arial Black";
ctx.textAlign="center";
ctx.strokeStyle="#000f";
ctx.lineWidth=5;
ctx.strokeText("Press SPACE to enter car",canvas.width/2,canvas.height/2-60);
ctx.fillStyle="#fff";
ctx.fillText("Press SPACE to enter car",canvas.width/2,canvas.height/2-60);
ctx.restore();
}
}
// HUD (minimap)
drawMinimap();
// Exit hint
if(player.inVehicle) {
ctx.save();
ctx.font="19px Arial Black";
ctx.textAlign="center";
ctx.strokeStyle="#000f";
ctx.lineWidth=5;
ctx.strokeText("Press SPACE to exit",canvas.width/2,canvas.height/2-60);
ctx.fillStyle="#fff";
ctx.fillText("Press SPACE to exit",canvas.width/2,canvas.height/2-60);
ctx.restore();
}
requestAnimationFrame(gameLoop);
}
// Interaction - Enter/Exit vehicles
window.addEventListener("keydown",e=>{
keys[e.key.toLowerCase()]=true;
// Interact: Enter/Exit vehicles
if(e.code==="Space") {
if(player.inVehicle && player.ridingVehicle) {
// Exit: Place player beside vehicle, free seat
const v = player.ridingVehicle;
const vdef = vehicleTypes[v.vtype];
const outX = v.x+vdef.w/2+10, outY = v.y;
// Place player here if not blocking
player.x = outX; player.y = outY;
player.inVehicle = false;
v.passenger=null;
player.ridingVehicle = null;
} else if(!player.inVehicle) {
// Near a vehicle?
for(const v of vehicles) {
const vdef = vehicleTypes[v.vtype];
const dist = Math.hypot(
player.x-v.x,
player.y-v.y
);
if(dist<35+vdef.w/2 && !v.passenger) {
// Enter it!
// Move player into seat
player.x = v.x+vdef.seatOffset.x-vdef.w/2;
player.y = v.y+vdef.seatOffset.y-vdef.h/2;
player.inVehicle = true;
v.passenger = player;
player.ridingVehicle = v;
break;
}
}
}
e.preventDefault();
}
});
// Keyup for motion
window.addEventListener("keyup",e=>{
keys[e.key.toLowerCase()]=false;
});
// Start game
drawMap(ctx,0,0);
gameLoop(performance.now());
</script>
</body>
</html>
```
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GTA-Style Top-Down Game Clone</title>
<style>
body {
margin: 0;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#gameCanvas {
border: 1px solid black;
background-color: #ffffff;
}
#miniMap {
position: absolute;
top: 10px;
right: 10px;
border: 1px solid black;
background-color: #ffffff;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<canvas id="miniMap" width="200" height="150"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('miniMap');
const miniCtx = miniCanvas.getContext('2d');
// World settings
const WORLD_WIDTH = 2000;
const WORLD_HEIGHT = 2000;
const VIEW_WIDTH = canvas.width;
const VIEW_HEIGHT = canvas.height;
// Player
let player = {
x: WORLD_WIDTH / 2,
y: WORLD_HEIGHT / 2,
size: 10,
speed: 3,
dx: 0,
dy: 0,
direction: 'down', // up, down, left, right
walking: false,
frame: 0, // for animation
inVehicle: false,
vehicle: null
};
// Buildings (static obstacles)
const buildings = [
{x: 400, y: 400, width: 200, height: 200},
{x: 800, y: 600, width: 150, height: 150},
{x: 1200, y: 300, width: 300, height: 100},
{x: 1500, y: 800, width: 100, height: 300},
// Add more for a city feel
{x: 200, y: 1000, width: 250, height: 150},
{x: 600, y: 1200, width: 180, height: 220},
{x: 1000, y: 1400, width: 200, height: 200},
{x: 1400, y: 500, width: 150, height: 250}
];
// Vehicles
const vehicles = [
{x: 500, y: 500, width: 40, height: 20, color: 'blue', dx: 1, dy: 0, occupied: false},
{x: 700, y: 700, width: 40, height: 20, color: 'red', dx: 0, dy: 1, occupied: false},
{x: 900, y: 900, width: 40, height: 20, color: 'green', dx: -1, dy: 0, occupied: false}
];
// Pedestrians
const pedestrians = [
{x: 600, y: 600, size: 8, color: 'purple', dx: 0.5, dy: 0.5, frame: 0},
{x: 800, y: 800, size: 8, color: 'orange', dx: -0.5, dy: 0.5, frame: 0},
{x: 1000, y: 400, size: 8, color: 'yellow', dx: 0.5, dy: -0.5, frame: 0}
];
// Keyboard controls
const keys = {};
window.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
});
window.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
});
// Game loop
function update() {
handleInput();
movePlayer();
moveVehicles();
movePedestrians();
checkCollisions();
draw();
drawMiniMap();
requestAnimationFrame(update);
}
function handleInput() {
player.dx = 0;
player.dy = 0;
player.walking = false;
let speed = player.inVehicle ? 5 : player.speed;
if (keys['w'] || keys['arrowup']) {
player.dy = -speed;
player.direction = 'up';
player.walking = true;
}
if (keys['s'] || keys['arrowdown']) {
player.dy = speed;
player.direction = 'down';
player.walking = true;
}
if (keys['a'] || keys['arrowleft']) {
player.dx = -speed;
player.direction = 'left';
player.walking = true;
}
if (keys['d'] || keys['arrowright']) {
player.dx = speed;
player.direction = 'right';
player.walking = true;
}
// Interact with vehicle (enter/exit on space)
if (keys[' ']) {
if (player.inVehicle) {
// Exit vehicle
player.inVehicle = false;
player.vehicle.occupied = false;
player.x = player.vehicle.x + player.vehicle.width + 10;
player.y = player.vehicle.y;
player.vehicle.dx = 0;
player.vehicle.dy = 0;
player.vehicle = null;
} else {
// Check for nearby vehicle to enter
for (let veh of vehicles) {
if (!veh.occupied && Math.hypot(player.x - (veh.x + veh.width/2), player.y - (veh.y + veh.height/2)) < 20) {
player.inVehicle = true;
player.vehicle = veh;
veh.occupied = true;
break;
}
}
}
keys[' '] = false; // Prevent continuous triggering
}
}
function movePlayer() {
let moveX = player.dx;
let moveY = player.dy;
if (player.inVehicle) {
player.vehicle.dx = player.dx / player.speed * 5; // Vehicle speed
player.vehicle.dy = player.dy / player.speed * 5;
player.x = player.vehicle.x + player.vehicle.width / 2;
player.y = player.vehicle.y + player.vehicle.height / 2;
} else {
player.x += moveX;
player.y += moveY;
}
// Boundaries
player.x = Math.max(player.size, Math.min(player.x, WORLD_WIDTH - player.size));
player.y = Math.max(player.size, Math.min(player.y, WORLD_HEIGHT - player.size));
// Animation frame
if (player.walking) {
player.frame = (player.frame + 0.2) % 2;
} else {
player.frame = 0;
}
}
function moveVehicles() {
vehicles.forEach(veh => {
if (!veh.occupied) {
veh.x += veh.dx;
veh.y += veh.dy;
// Simple AI: bounce off edges
if (veh.x < 0 || veh.x > WORLD_WIDTH - veh.width) veh.dx *= -1;
if (veh.y < 0 || veh.y > WORLD_HEIGHT - veh.height) veh.dy *= -1;
} else if (player.vehicle !== veh) {
veh.x += veh.dx;
veh.y += veh.dy;
}
});
}
function movePedestrians() {
pedestrians.forEach(ped => {
ped.x += ped.dx;
ped.y += ped.dy;
// Bounce off edges
if (ped.x < 0 || ped.x > WORLD_WIDTH) ped.dx *= -1;
if (ped.y < 0 || ped.y > WORLD_HEIGHT) ped.dy *= -1;
// Animation
ped.frame = (ped.frame + 0.1) % 2;
});
}
function checkCollisions() {
// Player vs Buildings
buildings.forEach(build => {
if (player.inVehicle) {
if (checkRectCollision(player.vehicle, build)) {
// Bounce back
player.vehicle.x -= player.vehicle.dx;
player.vehicle.y -= player.vehicle.dy;
player.x = player.vehicle.x + player.vehicle.width / 2;
player.y = player.vehicle.y + player.vehicle.height / 2;
}
} else {
if (player.x + player.size > build.x && player.x - player.size < build.x + build.width &&
player.y + player.size > build.y && player.y - player.size < build.y + build.height) {
player.x -= player.dx;
player.y -= player.dy;
}
}
});
// Player vs Pedestrians (simple bump)
pedestrians.forEach(ped => {
if (Math.hypot(player.x - ped.x, player.y - ped.y) < player.size + ped.size) {
// Bump: reverse direction
ped.dx *= -1;
ped.dy *= -1;
}
});
// Vehicles vs Buildings (for non-occupied)
vehicles.forEach(veh => {
if (!veh.occupied) {
buildings.forEach(build => {
if (checkRectCollision(veh, build)) {
veh.x -= veh.dx;
veh.y -= veh.dy;
veh.dx *= -1;
veh.dy *= -1;
}
});
}
});
}
function checkRectCollision(rect1, rect2) {
return (rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y);
}
function draw() {
ctx.clearRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT);
// Camera follows player
const camX = player.x - VIEW_WIDTH / 2;
const camY = player.y - VIEW_HEIGHT / 2;
// Draw map (roads as grid)
ctx.fillStyle = '#333'; // Roads
for (let i = 0; i < WORLD_WIDTH; i += 100) {
ctx.fillRect(i - camX, 0 - camY, 50, WORLD_HEIGHT);
ctx.fillRect(0 - camX, i - camY, WORLD_WIDTH, 50);
}
// Draw buildings
ctx.fillStyle = 'gray';
buildings.forEach(build => {
ctx.fillRect(build.x - camX, build.y - camY, build.width, build.height);
});
// Draw vehicles
vehicles.forEach(veh => {
ctx.fillStyle = veh.color;
ctx.fillRect(veh.x - camX, veh.y - camY, veh.width, veh.height);
// Wheels
ctx.fillStyle = 'black';
ctx.fillCircle(veh.x + 5 - camX, veh.y + veh.height - camX, 3); // Simplified fillCircle function below
ctx.fillCircle(veh.x + veh.width - 5 - camX, veh.y + veh.height - camX, 3);
});
// Draw pedestrians (simple stick figure with animation)
pedestrians.forEach(ped => {
drawStickFigure(ped.x - camX, ped.y - camY, ped.size, ped.color, ped.frame);
});
// Draw player (stick figure with animation, or vehicle if in one)
if (player.inVehicle) {
// Player is inside vehicle, don't draw separately
} else {
drawStickFigure(player.x - camX, player.y - camY, player.size, 'red', player.frame, player.direction);
}
}
function drawStickFigure(x, y, size, color, frame, direction = 'down') {
ctx.fillStyle = color;
// Head
ctx.beginPath();
ctx.arc(x, y - size, size / 2, 0, 2 * Math.PI);
ctx.fill();
// Body
ctx.beginPath();
ctx.moveTo(x, y - size / 2);
ctx.lineTo(x, y + size / 2);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
// Arms
ctx.beginPath();
ctx.moveTo(x - size / 2, y);
ctx.lineTo(x + size / 2, y);
ctx.stroke();
// Legs animation
const legOffset = frame > 1 ? -size / 4 : size / 4;
ctx.beginPath();
ctx.moveTo(x, y + size / 2);
ctx.lineTo(x - size / 4 + legOffset, y + size);
ctx.moveTo(x, y + size / 2);
ctx.lineTo(x + size / 4 - legOffset, y + size);
ctx.stroke();
}
// Utility for filling circle (since fillCircle isn't native)
CanvasRenderingContext2D.prototype.fillCircle = function(x, y, r) {
this.beginPath();
this.arc(x, y, r, 0, 2 * Math.PI);
this.fill();
};
function drawMiniMap() {
miniCtx.clearRect(0, 0, 200, 150);
const scaleX = 200 / WORLD_WIDTH;
const scaleY = 150 / WORLD_HEIGHT;
// Draw buildings
miniCtx.fillStyle = 'gray';
buildings.forEach(build => {
miniCtx.fillRect(build.x * scaleX, build.y * scaleY, build.width * scaleX, build.height * scaleY);
});
// Draw player
miniCtx.fillStyle = 'red';
miniCtx.fillCircle(player.x * scaleX, player.y * scaleY, 2);
// Draw vehicles
vehicles.forEach(veh => {
miniCtx.fillStyle = veh.color;
miniCtx.fillRect(veh.x * scaleX, veh.y * scaleY, veh.width * scaleX, veh.height * scaleY);
});
// Draw pedestrians
pedestrians.forEach(ped => {
miniCtx.fillStyle = ped.color;
miniCtx.fillCircle(ped.x * scaleX, ped.y * scaleY, 1);
});
}
update();
</script>
</body>
</html>
```# GTA-Style Top-Down Game Clone
I've created a complete GTA-style top-down game with smooth player movement, sprite animations, collision detection, pedestrians, vehicles, and a functional minimap. The game includes the following features:
- Arrow key or WASD movement control
- Animated character sprites in 8 directions
- Collision detection with buildings
- AI-controlled pedestrians and vehicles
- Interactive minimap showing player position and map overview
- Realistic city environment with roads, buildings, and traffic
- Pickups for health and coins
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GTA-Style Top-Down Game</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Courier New', monospace;
}
body {
background: #000;
overflow: hidden;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#game-container {
position: relative;
width: 800px;
height: 600px;
}
#game-canvas {
background-color: #6491c9;
border: 4px solid #e74c3c;
border-radius: 5px;
box-shadow: 0 0 20px rgba(231, 76, 60, 0.6);
}
#minimap {
position: absolute;
bottom: 15px;
right: 15px;
width: 200px;
height: 200px;
border: 3px solid #e74c3c;
background-color: rgba(30, 30, 40, 0.85);
z-index: 10;
}
#ui {
position: absolute;
top: 15px;
left: 15px;
z-index: 10;
color: white;
text-shadow: 2px 2px 1px #000;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 10px;
border: 1px solid #e74c3c;
font-size: 16px;
}
#title {
position: absolute;
top: 20px;
width: 100%;
text-align: center;
z-index: 10;
color: #e74c3c;
font-size: 38px;
text-shadow: 3px 3px 0 black, 5px 5px 0 #c0392b;
letter-spacing: 3px;
text-transform: uppercase;
font-weight: bold;
}
#controls {
position: absolute;
bottom: 15px;
left: 15px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 10px;
border: 1px solid #3498db;
font-size: 14px;
z-index: 10;
}
.health-bar {
height: 20px;
width: 200px;
background-color: #555;
border-radius: 10px;
overflow: hidden;
margin-top: 10px;
}
.health-fill {
height: 100%;
width: 100%;
background: linear-gradient(to right, red, orange, yellow, #2ecc40);
border-radius: 10px;
transition: width 0.3s ease;
}
</style>
</head>
<body>
<div id="game-container">
<div id="title">GTA BLOCKS</div>
<canvas id="game-canvas" width="800" height="600"></canvas>
<canvas id="minimap" width="200" height="200"></canvas>
<div id="ui">
<div>SCORE: <span id="score">0</span></div>
<div>COINS: <span id="coins">0</span></div>
<div>HEALTH:</div>
<div class="health-bar">
<div class="health-fill" id="health-fill"></div>
</div>
</div>
<div id="controls">
<div>CONTROLS: WASD or Arrow Keys to Move</div>
<div>CAPTURE CARS: Walk towards them</div>
<div>AVOID PEDESTRIANS: They hurt you!</div>
</div>
</div>
<script>
// Canvas and context setup
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const minimapCanvas = document.getElementById('minimap');
const minimapCtx = minimapCanvas.getContext('2d');
// UI Elements
const scoreElement = document.getElementById('score');
const coinsElement = document.getElementById('coins');
const healthFill = document.getElementById('health-fill');
// Game constants
const PLAYER_SPEED = 4;
const MINIMAP_SCALE = 0.2;
const MAP_WIDTH = 3000;
const MAP_HEIGHT = 3000;
const TILE_SIZE = 80;
const ROAD_WIDTH = 200;
// Game variables
let player = {
x: MAP_WIDTH / 2,
y: MAP_HEIGHT / 2,
width: 50,
height: 50,
speed: PLAYER_SPEED,
direction: 0, // Angle in degrees
health: 100,
score: 0,
coins: 0,
inVehicle: false
};
let pedestrians = [];
let vehicles = [];
let pickups = [];
let keys = {
up: false,
down: false,
left: false,
right: false
};
// Map definition
const buildings = [];
// Generate buildings and roads
function generateCity() {
// Create a grid of buildings with roads
const cols = Math.floor(MAP_WIDTH / TILE_SIZE);
const rows = Math.floor(MAP_HEIGHT / TILE_SIZE);
for (let x = 0; x < cols; x++) {
for (let y = 0; y < rows; y++) {
// Create roads in a grid pattern every 3 rows/cols
const isRoadX = x % 3 === 0;
const isRoadY = y % 3 === 0;
// Skip road areas
if (isRoadX || isRoadY) continue;
// 80% chance to place a building
if (Math.random() > 0.2) {
const width = TILE_SIZE * 2 + Math.floor(Math.random() * 40);
const height = TILE_SIZE * 2 + Math.floor(Math.random() * 40);
buildings.push({
x: x * TILE_SIZE + (TILE_SIZE* 0.5),
y: y * TILE_SIZE + (TILE_SIZE* 0.5),
width: width,
height: height,
color: '#34495e'
});
}
}
}
// Generate some pedestrians
for (let i = 0; i < 25; i++) {
pedestrians.push({
x: Math.random() * MAP_WIDTH,
y: Math.random() * MAP_HEIGHT,
width: 25,
height: 25,
speed: Math.random() * 2 + 1,
direction: Math.random() * Math.PI * 2,
directionCooldown: 0,
color: `hsl(${Math.random() * 360}, 80%, 60%)`
});
}
// Generate some vehicles
for (let i = 0; i < 15; i++) {
vehicles.push({
x: Math.random() * MAP_WIDTH,
y: Math.random() * MAP_HEIGHT,
width: 70,
height: 40,
speed: Math.random() * 4 + 2,
direction: Math.random() * Math.PI * 2,
directionCooldown: 0,
color: `hsl(${Math.random() * 360}, 80%, 40%)`,
captured: false
});
}
// Generate pickups (health and coins)
for (let i = 0; i < 20; i++) {
pickups.push({
x: Math.random() * MAP_WIDTH,
y: Math.random() * MAP_HEIGHT,
width: 30,
height: 30,
type: Math.random() > 0.7 ? 'health' : 'coin',
collected: false
});
}
}
// Collision detection - rectangle vs rectangle
function rectCollision(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
// Move based on keys pressed
function movePlayer() {
const prevX = player.x;
const prevY = player.y;
if (!player.inVehicle) {
if (keys.up || keys.w) {
player.y -= player.speed;
player.direction = -Math.PI/2;
}
if (keys.down || keys.s) {
player.y += player.speed;
player.direction = Math.PI/2;
}
if (keys.left || keys.a) {
player.x -= player.speed;
player.direction = Math.PI;
}
if (keys.right || keys.d) {
player.x += player.speed;
player.direction = 0;
}
// Diagonal movement (combine directions)
if ((keys.up || keys.w) && (keys.right || keys.d)) player.direction = -Math.PI/4;
if ((keys.up || keys.w) && (keys.left || keys.a)) player.direction = -3*Math.PI/4;
if ((keys.down || keys.s) && (keys.right || keys.d)) player.direction = Math.PI/4;
if ((keys.down || keys.s) && (keys.left || keys.a)) player.direction = 3*Math.PI/4;
}
// Check for collisions with buildings
for (const building of buildings) {
if (rectCollision(player, building)) {
player.x = prevX;
player.y = prevY;
return;
}
}
// Keep player within map bounds
player.x = Math.max(0, Math.min(MAP_WIDTH - player.width, player.x));
player.y = Math.max(0, Math.min(MAP_HEIGHT - player.height, player.y));
}
// Move and update pedestrians
function updatePedestrians() {
for (const ped of pedestrians) {
// Occasionally change direction
if (ped.directionCooldown <= 0) {
ped.directionCooldown = Math.floor(Math.random() * 100) + 60;
ped.direction = Math.random() * Math.PI * 2;
} else {
ped.directionCooldown--;
}
// Move
ped.x += Math.cos(ped.direction) * ped.speed;
ped.y += Math.sin(ped.direction) * ped.speed;
// Keep within map bounds
ped.x = Math.max(0, Math.min(MAP_WIDTH - ped.width, ped.x));
ped.y = Math.max(0, Math.min(MAP_HEIGHT - ped.height, ped.y));
// Check for collisions with buildings
for (const building of buildings) {
if (rectCollision(ped, building)) {
// If colliding, change direction
ped.direction = (ped.direction + Math.PI) % (Math.PI * 2);
ped.x = Math.max(0, Math.min(MAP_WIDTH - ped.width, ped.x));
ped.y = Math.max(0, Math.min(MAP_HEIGHT - ped.height, ped.y));
break;
}
}
// Check for collisions with player
if (rectCollision(player, ped) && !player.inVehicle) {
player.health -= 0.5;
if (player.health < 0) player.health = 0;
healthFill.style.width = `${player.health}%`;
}
}
}
// Move and update vehicles
function updateVehicles() {
for (const vehicle of vehicles) {
// Occasionally change direction
if (vehicle.directionCooldown <= 0) {
vehicle.directionCooldown = Math.floor(Math.random() * 80) + 100;
vehicle.direction = Math.random() * Math.PI * 2;
} else {
vehicle.directionCooldown--;
}
// Move vehicles faster when not captured
const currentSpeed = vehicle.captured ? player.speed : vehicle.speed;
// Move vehicle
vehicle.x += Math.cos(vehicle.direction) * currentSpeed;
vehicle.y += Math.sin(vehicle.direction) * currentSpeed;
// Keep within map bounds
vehicle.x = Math.max(0, Math.min(MAP_WIDTH - vehicle.width, vehicle.x));
vehicle.y = Math.max(0, Math.min(MAP_HEIGHT - vehicle.height, vehicle.y));
// Check for collisions with buildings
for (const building of buildings) {
if (rectCollision(vehicle, building)) {
vehicle.direction = (vehicle.direction + Math.PI) % (Math.PI * 2);
vehicle.x = Math.max(0, Math.min(MAP_WIDTH - vehicle.width, vehicle.x));
vehicle.y = Math.max(0, Math.min(MAP_HEIGHT - vehicle.height, vehicle.y));
break;
}
}
}
}
// Update pickups
function updatePickups() {
for (const pickup of pickups) {
if (!pickup.collected && rectCollision(player, pickup)) {
pickup.collected = true;
if (pickup.type === 'coin') {
player.coins++;
coinsElement.textContent = player.coins;
player.score += 20;
} else {
player.health = Math.min(100, player.health + 30);
healthFill.style.width = `${player.health}%`;
player.score += 10;
}
scoreElement.textContent = player.score;
}
}
}
// Check for vehicle capture
function captureVehicle() {
for (const vehicle of vehicles) {
if (!vehicle.captured && rectCollision(player, vehicle)) {
vehicle.captured = true;
player.inVehicle = true;
vehicle.color = 'rgb(60, 200, 60)';
player.speed *= 1.5;
player.score += 50;
scoreElement.textContent = player.score;
// Expire after 5 seconds
setTimeout(() => {
vehicle.captured = false;
player.inVehicle = false;
player.speed = PLAYER_SPEED;
vehicle.color = `hsl(${Math.random() * 360}, 80%, 40%)`;
}, 5000);
}
}
}
// Render a rotating character sprite
function drawPlayer() {
ctx.save();
ctx.translate(player.x - player.width/2, player.y - player.height/2);
ctx.rotate(player.direction);
if (player.inVehicle) {
// Draw vehicle
ctx.fillStyle = '#2ecc40';
ctx.fillRect(-10, -25, 60, 40);
// Draw windshield
ctx.fillStyle = '#3498db';
ctx.fillRect(30, -20, 15, 30);
// Draw wheels
ctx.fillStyle = '#2c3e50';
ctx.fillRect(0, -30, 15, 10);
ctx.fillRect(0, 15, 15, 10);
ctx.fillRect(35, -30, 15, 10);
ctx.fillRect(35, 15, 15, 10);
} else {
// Draw player body (head and body)
ctx.fillStyle = '#e74c3c';
// Draw head
ctx.beginPath();
ctx.arc(0, -5, 18, 0, Math.PI * 2);
ctx.fill();
// Draw body
ctx.fillRect(-15, 5, 30, 30);
// Draw legs
ctx.fillRect(-15, 35, 10, 20);
ctx.fillRect(5, 35, 10, 20);
// Draw arms - direction based
const armRotation = Math.sin(Date.now() / 100) * 0.3;
ctx.fillStyle = '#c0392b';
ctx.save();
ctx.translate(-15, 15);
ctx.rotate(armRotation);
ctx.fillRect(0, 0, 25, 8);
ctx.restore();
ctx.save();
ctx.translate(15, 15);
ctx.rotate(-armRotation);
ctx.fillRect(-25, 0, 25, 8);
ctx.restore();
}
ctx.restore();
}
// Draw the game world
function drawWorld() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Fill background
ctx.fillStyle = '#6491c9';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Calculate the offset to center on the player
const offsetX = canvas.width/2 - player.x;
const offsetY = canvas.height/2 - player.y;
ctx.save();
ctx.translate(offsetX, offsetY);
// Draw roads
ctx.fillStyle = '#2c3e50';
for (let x = ROAD_WIDTH; x < MAP_WIDTH; x += ROAD_WIDTH * 3) {
ctx.fillRect(x - ROAD_WIDTH/2, 0, ROAD_WIDTH, MAP_HEIGHT);
}
for (let y = ROAD_WIDTH; y < MAP_HEIGHT; y += ROAD_WIDTH * 3) {
ctx.fillRect(0, y - ROAD_WIDTH/2, MAP_WIDTH, ROAD_WIDTH);
}
// Draw center lines
ctx.fillStyle = '#f1c40f';
for (let x = ROAD_WIDTH; x < MAP_WIDTH; x += ROAD_WIDTH * 3) {
for (let i = 0; i < MAP_HEIGHT; i += 80) {
ctx.fillRect(x - 2.5, i + ((Date.now()/20 + i) % 80), 5, 30);
}
}
for (let y = ROAD_WIDTH; y < MAP_HEIGHT; y += ROAD_WIDTH * 3) {
for (let i = 0; i < MAP_WIDTH; i += 80) {
ctx.fillRect(i + ((Date.now()/20 + i) % 80), y - 2.5, 30, 5);
}
}
// Draw buildings
for (const building of buildings) {
ctx.fillStyle = building.color;
const shadowIntensity = 50;
ctx.shadowColor = `rgba(0, 0, 0, 0.4)`;
ctx.shadowBlur = 15;
ctx.fillRect(building.x, building.y, building.width, building.height);
// Draw windows
ctx.shadowBlur = 0;
ctx.fillStyle = '#3498db';
for (let i = 5; i < building.width; i += 20) {
for (let j = 5; j < building.height; j += 30) {
ctx.fillRect(building.x + i, building.y + j, 10, 15);
}
}
}
// Draw pickups
for (const pickup of pickups) {
if (pickup.collected) continue;
ctx.fillStyle = pickup.type === 'coin' ? '#f1c40f' : '#e74c3c';
ctx.beginPath();
ctx.arc(pickup.x, pickup.y, 15, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#000';
if (pickup.type === 'coin') {
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('$', pickup.x, pickle.y);
} else {
// Health cross
ctx.fillRect(pickup.x - 8, pickup.y - 2, 16, 4);
ctx.fillRect(pickup.x - 2, pickup.y - 8, 4, 16);
}
}
// Draw pedestrians
for (const ped of pedestrians) {
ctx.fillStyle = ped.color;
ctx.beginPath();
ctx.arc(ped.x, ped.y, 15, 0, Math.PI*2);
ctx.fill();
// Draw head
ctx.fillStyle = '#ecf0f1';
ctx.beginPath();
ctx.arc(ped.x, ped.y - 6, 5, 0, Math.PI*2);
ctx.fill();
}
// Draw vehicles
for (const vehicle of vehicles) {
const angle = vehicle.direction;
ctx.save();
ctx.translate(vehicle.x, vehicle.y);
ctx.rotate(angle);
ctx.fillStyle = vehicle.color;
ctx.fillRect(-vehicle.width/2, -vehicle.height/2, vehicle.width, vehicle.height);
// Draw windshield
ctx.fillStyle = '#3498db';
ctx.fillRect(vehicle.width * 0.4 - vehicle.width/2, -vehicle.height/2, vehicle.width * 0.4, vehicle.height);
// Draw wheels
ctx.fillStyle = '#2c3e50';
ctx.fillRect(-vehicle.width/2, -vehicle.height/2 - 5, 10, 5);
ctx.fillRect(vehicle.width/2 - 10, -vehicle.height/2 - 5, 10, 5);
ctx.fillRect(-vehicle.width/2, vehicle.height/2, 10, 5);
ctx.fillRect(vehicle.width/2 - 10, vehicle.height/2, 10, 5);
ctx.restore();
}
// Draw player
drawPlayer();
// Reset translation
ctx.restore();
}
// Draw the minimap
function drawMinimap() {
minimapCtx.clearRect(0, 0, minimapCanvas.width, minimapCanvas.height);
// Background
minimapCtx.fillStyle = '#233';
minimapCtx.fillRect(0, 0, minimapCanvas.width, minimapCanvas.height);
const scale = MINIMAP_SCALE * minimapCanvas.width / MAP_WIDTH;
const circleSize = 10;
// Draw roads
minimapCtx.fillStyle = '#2c3e50';
for (let x = ROAD_WIDTH; x < MAP_WIDTH; x += ROAD_WIDTH * 3) {
minimapCtx.fillRect(x * scale - (ROAD_WIDTH/2 * scale), 0, ROAD_WIDTH * scale, MAP_HEIGHT * scale);
}
for (let y = ROAD_WIDTH; y < MAP_HEIGHT; y += ROAD_WIDTH * 3) {
minimapCtx.fillRect(0, y * scale - (ROAD_WIDTH/2 * scale), MAP_WIDTH * scale, ROAD_WIDTH * scale);
}
// Draw buildings as small rectangles
minimapCtx.fillStyle = '#34495e';
for (const building of buildings) {
minimapCtx.fillRect(
building.x * scale,
building.y * scale,
building.width * scale,
building.height * scale
);
}
// Draw pickups
for (const pickup of pickups) {
if (pickup.collected) continue;
minimapCtx.fillStyle = pickup.type === 'coin' ? '#f1c40f' : '#e74c3c';
minimapCtx.beginPath();
minimapCtx.arc(
pickup.x * scale,
pickup.y * scale,
2, 0, Math.PI * 2
);
minimapCtx.fill();
}
// Draw vehicles on minimap
for (const vehicle of vehicles) {
minimapCtx.fillStyle = vehicle.color;
minimapCtx.beginPath();
minimapCtx.arc(
vehicle.x * scale,
vehicle.y * scale,
3, 0, Math.PI * 2
);
minimapCtx.fill();
}
// Draw player on minimap
minimapCtx.fillStyle = player.inVehicle ? '#2ecc40' : '#e74c3c';
minimapCtx.beginPath();
minimapCtx.arc(
player.x * scale,
player.y * scale,
circleSize, 0, Math.PI * 2
);
minimapCtx.fill();
// Draw circle around player
minimapCtx.strokeStyle = 'white';
minimapCtx.lineWidth = 2;
minimapCtx.beginPath();
minimapCtx.arc(
player.x * scale,
player.y * scale,
circleSize + 4, 0, Math.PI * 2
);
minimapCtx.stroke();
// Draw directional marker
minimapCtx.translate(player.x * scale, player.y * scale);
minimapCtx.rotate(player.direction);
minimapCtx.fillStyle = 'white';
minimapCtx.beginPath();
minimapCtx.moveTo(0, -circleSize - 2);
minimapCtx.lineTo(-5, -circleSize - 7);
minimapCtx.lineTo(5, -circleSize - 7);
minimapCtx.closePath();
minimapCtx.fill();
minimapCtx.resetTransform();
}
// Game update loop
function update() {
movePlayer();
updatePedestrians();
updateVehicles();
updatePickups();
captureVehicle();
// Random events to add pedestrians occasionally
if (Math.random() < 0.02 && pedestrians.length < 40) {
pedestrians.push({
x: Math.random() * MAP_WIDTH,
y: Math.random() * MAP_HEIGHT,
width: 25,
height: 25,
speed: Math.random() * 2 + 1,
direction: Math.random() * Math.PI * 2,
directionCooldown: 0,
color: `hsl(${Math.random() * 360}, 80%, 60%)`
});
}
// Add a coin occasionally
if (pickups.filter(p => p.type === 'coin' && !p.collected).length < 8 && Math.random() < 0.01) {
pickups.push({
x: Math.random() * MAP_WIDTH,
y: Math.random() * MAP_HEIGHT,
width: 30,
height: 30,
type: 'coin',
collected: false
});
}
// Score increase over time when in vehicle
if (player.inVehicle) {
player.score += 0.1;
scoreElement.textContent = Math.floor(player.score);
}
}
// Game draw loop
function draw() {
drawWorld();
drawMinimap();
}
// Main game loop
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// Initialize key listeners
function initInput() {
document.addEventListener('keydown', (e) => {
if (e.key === 'w' || e.key === 'W' || e.key === 'ArrowUp') keys.up = true;
if (e.key === 's' || e.key === 'S' || e.key === 'ArrowDown') keys.down = true;
if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') keys.left = true;
if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') keys.right = true;
});
document.addEventListener('keyup', (e) => {
if (e.key === 'w' || e.key === 'W' || e.key === 'ArrowUp') keys.up = false;
if (e.key === 's' || e.key === 'S' || e.key === 'ArrowDown') keys.down = false;
if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') keys.left = false;
if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') keys.right = false;
});
}
// Initialize the game
function init() {
generateCity();
initInput();
gameLoop();
}
// Start the game
window.onload = init;
</script>
</body>
</html>
```
## Game Features
- **Character Control**: Use WASD or arrow keys to move your character in all directions
- **Sprite Animation**: The player character shows smooth walking animations with directional movement
- **Collision Detection**: You can't walk through buildings or other obstacles
- **Vehicle Interaction**: Approach vehicles to temporarily drive them (increases speed)
- **AI Pedestrians**: Random civilians that roam the city and damage you when touched
- **Minimap UI**: Shows an overview of the map with your position in the corner
- **Pickups**: Collect coins for points and health packs to restore health
- **Health System**: Keep an eye on your health bar at the top-left
The city includes roads and buildings arranged in a grid pattern, with pedestrians and vehicles moving around. The minimap displays the player as a red circle (green when in a vehicle). Capture vehicles for a temporary speed boost and higher scoring potential!```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GTA-Style Top-Down Game</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #000;
overflow: hidden;
}
#gameContainer {
position: relative;
width: 800px;
height: 600px;
margin: 0 auto;
border: 2px solid #333;
}
#gameCanvas {
background: #2a2a2a;
display: block;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
z-index: 100;
}
#minimap {
position: absolute;
top: 10px;
right: 10px;
width: 150px;
height: 150px;
background: rgba(0, 0, 0, 0.7);
border: 2px solid #fff;
z-index: 100;
}
#health {
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
}
.health-bar {
width: 100px;
height: 20px;
background: #333;
border: 1px solid #fff;
margin-top: 5px;
}
.health-fill {
height: 100%;
background: linear-gradient(90deg, #ff0000, #ffff00, #00ff00);
transition: width 0.3s;
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
font-size: 12px;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="ui">
<div id="health">
<div>Health: <span id="healthText">100</span></div>
<div class="health-bar">
<div class="health-fill" id="healthFill" style="width: 100%;"></div>
</div>
</div>
</div>
<canvas id="minimap" width="150" height="150"></canvas>
<div id="controls">
Controls:<br>
WASD / Arrow Keys: Move<br>
E: Enter/Exit Vehicle<br>
R: Punch Pedestrians
</div>
</div>
<script>
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.minimapCanvas = document.getElementById('minimap');
this.minimapCtx = this.minimapCanvas.getContext('2d');
this.width = this.canvas.width;
this.height = this.canvas.height;
this.camera = { x: 0, y: 0 };
this.worldSize = { width: 1600, height: 1200 };
this.keys = {};
this.entities = [];
this.player = new Player(400, 300);
this.entities.push(this.player);
this.init();
this.gameLoop();
}
init() {
this.setupControls();
this.generateWorld();
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.keys[e.key.toLowerCase()] = true;
});
document.addEventListener('keyup', (e) => {
this.keys[e.key.toLowerCase()] = false;
});
}
generateWorld() {
// Generate buildings
for (let i = 0; i < 20; i++) {
const x = Math.random() * (this.worldSize.width - 100);
const y = Math.random() * (this.worldSize.height - 100);
this.entities.push(new Building(x, y));
}
// Generate vehicles
for (let i = 0; i < 8; i++) {
const x = Math.random() * this.worldSize.width;
const y = Math.random() * this.worldSize.height;
this.entities.push(new Vehicle(x, y));
}
// Generate pedestrians
for (let i = 0; i < 15; i++) {
const x = Math.random() * this.worldSize.width;
const y = Math.random() * this.worldSize.height;
this.entities.push(new Pedestrian(x, y));
}
}
update() {
// Update camera to follow player
this.camera.x = this.player.x - this.width / 2;
this.camera.y = this.player.y - this.height / 2;
// Keep camera within world bounds
this.camera.x = Math.max(0, Math.min(this.worldSize.width - this.width, this.camera.x));
this.camera.y = Math.max(0, Math.min(this.worldSize.height - this.height, this.camera.y));
// Update all entities
this.entities.forEach(entity => {
if (entity.update) {
entity.update(this);
}
});
// Handle collisions
this.handleCollisions();
}
handleCollisions() {
for (let i = 0; i < this.entities.length; i++) {
for (let j = i + 1; j < this.entities.length; j++) {
const a = this.entities[i];
const b = this.entities[j];
if (this.checkCollision(a, b)) {
this.resolveCollision(a, b);
}
}
}
}
checkCollision(a, b) {
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}
resolveCollision(a, b) {
if (a.type === 'player' || b.type === 'player') {
const player = a.type === 'player' ? a : b;
const other = a.type === 'player' ? b : a;
if (other.type === 'building') {
// Push player out of building
const overlapX = Math.min(player.x + player.width - other.x, other.x + other.width - player.x);
const overlapY = Math.min(player.y + player.height - other.y, other.y + other.height - player.y);
if (overlapX < overlapY) {
player.x += player.x < other.x ? -overlapX : overlapX;
} else {
player.y += player.y < other.y ? -overlapY : overlapY;
}
}
}
}
render() {
// Clear canvas
this.ctx.fillStyle = '#2a2a2a';
this.ctx.fillRect(0, 0, this.width, this.height);
// Draw grid (streets)
this.drawGrid();
// Sort entities by y position for proper layering
const visibleEntities = this.entities
.filter(entity => this.isVisible(entity))
.sort((a, b) => a.y - b.y);
// Render all visible entities
visibleEntities.forEach(entity => {
entity.render(this.ctx, this.camera);
});
// Render minimap
this.renderMinimap();
// Update UI
this.updateUI();
}
drawGrid() {
this.ctx.strokeStyle = '#444';
this.ctx.lineWidth = 1;
const gridSize = 50;
const startX = -this.camera.x % gridSize;
const startY = -this.camera.y % gridSize;
for (let x = startX; x < this.width; x += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, this.height);
this.ctx.stroke();
}
for (let y = startY; y < this.height; y += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(this.width, y);
this.ctx.stroke();
}
}
isVisible(entity) {
return entity.x < this.camera.x + this.width &&
entity.x + entity.width > this.camera.x &&
entity.y < this.camera.y + this.height &&
entity.y + entity.height > this.camera.y;
}
renderMinimap() {
const scale = 0.1;
// Clear minimap
this.minimapCtx.fillStyle = '#000';
this.minimapCtx.fillRect(0, 0, 150, 150);
// Draw entities on minimap
this.entities.forEach(entity => {
const x = entity.x * scale;
const y = entity.y * scale;
const width = Math.max(1, entity.width * scale);
const height = Math.max(1, entity.height * scale);
if (entity.type === 'player') {
this.minimapCtx.fillStyle = '#0ff';
} else if (entity.type === 'vehicle') {
this.minimapCtx.fillStyle = '#ff0';
} else if (entity.type === 'building') {
this.minimapCtx.fillStyle = '#666';
} else if (entity.type === 'pedestrian') {
this.minimapCtx.fillStyle = '#0f0';
}
this.minimapCtx.fillRect(x, y, width, height);
});
// Draw camera view area
this.minimapCtx.strokeStyle = '#fff';
this.minimapCtx.lineWidth = 1;
this.minimapCtx.strokeRect(
this.camera.x * scale,
this.camera.y * scale,
this.width * scale,
this.height * scale
);
}
updateUI() {
document.getElementById('healthText').textContent = this.player.health;
document.getElementById('healthFill').style.width = `${this.player.health}%`;
}
gameLoop() {
this.update();
this.render();
requestAnimationFrame(() => this.gameLoop());
}
}
class Entity {
constructor(x, y, width, height, type) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.type = type;
}
render(ctx, camera) {
const screenX = this.x - camera.x;
const screenY = this.y - camera.y;
ctx.fillStyle = this.color || '#fff';
ctx.fillRect(screenX, screenY, this.width, this.height);
}
}
class Player extends Entity {
constructor(x, y) {
super(x, y, 20, 20, 'player');
this.color = '#00ff00';
this.speed = 3;
this.health = 100;
this.inVehicle = null;
this.animFrame = 0;
}
update(game) {
if (this.inVehicle) {
this.updateInVehicle(game);
} else {
this.updateOnFoot(game);
}
// Handle interactions
if (game.keys['e']) {
this.handleInteraction(game);
}
// Handle combat
if (game.keys['r']) {
this.punch(game);
}
// Keep player in world bounds
this.x = Math.max(0, Math.min(game.worldSize.width - this.width, this.x));
this.y = Math.max(0, Math.min(game.worldSize.height - this.height, this.y));
this.animFrame++;
}
updateOnFoot(game) {
let dx = 0, dy = 0;
if (game.keys['w'] || game.keys['arrowup']) dy -= this.speed;
if (game.keys['s'] || game.keys['arrowdown']) dy += this.speed;
if (game.keys['a'] || game.keys['arrowleft']) dx -= this.speed;
if (game.keys['d'] || game.keys['arrowright']) dx += this.speed;
this.x += dx;
this.y += dy;
}
updateInVehicle(game) {
this.x = this.inVehicle.x;
this.y = this.inVehicle.y;
let dx = 0, dy = 0;
if (game.keys['w'] || game.keys['arrowup']) dy -= this.inVehicle.speed;
if (game.keys['s'] || game.keys['arrowdown']) dy += this.inVehicle.speed;
if (game.keys['a'] || game.keys['arrowleft']) dx -= this.inVehicle.speed;
if (game.keys['d'] || game.keys['arrowright']) dx += this.inVehicle.speed;
this.inVehicle.x += dx;
this.inVehicle.y += dy;
this.inVehicle.x = Math.max(0, Math.min(game.worldSize.width - this.inVehicle.width, this.inVehicle.x));
this.inVehicle.y = Math.max(0, Math.min(game.worldSize.height - this.inVehicle.height, this.inVehicle.y));
}
handleInteraction(game) {
if (this.inVehicle) {
// Exit vehicle
this.x = this.inVehicle.x + this.inVehicle.width + 5;
this.inVehicle = null;
} else {
// Try to enter vehicle
const nearbyVehicle = game.entities.find(entity =>
entity.type === 'vehicle' &&
Math.abs(entity.x - this.x) < 40 &&
Math.abs(entity.y - this.y) < 40
);
if (nearbyVehicle) {
this.inVehicle = nearbyVehicle;
}
}
}
punch(game) {
const nearbyPeds = game.entities.filter(entity =>
entity.type === 'pedestrian' &&
Math.abs(entity.x - this.x) < 30 &&
Math.abs(entity.y - this.y) < 30
);
nearbyPeds.forEach(ped => {
ped.health -= 25;
if (ped.health <= 0) {
const index = game.entities.indexOf(ped);
if (index > -1) {
game.entities.splice(index, 1);
}
}
});
}
render(ctx, camera) {
if (this.inVehicle) return; // Don't render player when in vehicle
const screenX = this.x - camera.x;
const screenY = this.y - camera.y;
// Simple walking animation
const bobOffset = Math.sin(this.animFrame * 0.3) * 2;
ctx.fillStyle = this.color;
ctx.fillRect(screenX, screenY + bobOffset, this.width, this.height);
// Draw direction indicator
ctx.fillStyle = '#fff';
ctx.fillRect(screenX + 8, screenY + 5, 4, 4);
}
}
class Vehicle extends Entity {
constructor(x, y) {
super(x, y, 30, 50, 'vehicle');
this.color = `hsl(${Math.random() * 360}, 70%, 50%)`;
this.speed = 5;
}
render(ctx, camera) {
const screenX = this.x - camera.x;
const screenY = this.y - camera.y;
// Car body
ctx.fillStyle = this.color;
ctx.fillRect(screenX, screenY, this.width, this.height);
// Windows
ctx.fillStyle = '#333';
ctx.fillRect(screenX + 5, screenY + 5, this.width - 10, 15);
ctx.fillRect(screenX + 5, screenY + 30, this.width - 10, 15);
// Wheels
ctx.fillStyle = '#000';
ctx.fillRect(screenX - 2, screenY + 5, 4, 8);
ctx.fillRect(screenX + this.width - 2, screenY + 5, 4, 8);
ctx.fillRect(screenX - 2, screenY + this.height - 13, 4, 8);
ctx.fillRect(screenX + this.width - 2, screenY + this.height - 13, 4, 8);
}
}
class Pedestrian extends Entity {
constructor(x, y) {
super(x, y, 15, 15, 'pedestrian');
this.color = `hsl(${Math.random() * 360}, 50%, 60%)`;
this.speed = 1;
this.health = 100;
this.direction = Math.random() * Math.PI * 2;
this.walkTimer = 0;
this.animFrame = 0;
}
update(game) {
// Simple AI: walk randomly
this.walkTimer++;
if (this.walkTimer > 120) { // Change direction every 2 seconds
this.direction = Math.random() * Math.PI * 2;
this.walkTimer = 0;
}
const dx = Math.cos(this.direction) * this.speed;
const dy = Math.sin(this.direction) * this.speed;
this.x += dx;
this.y += dy;
// Keep in world bounds
this.x = Math.max(0, Math.min(game.worldSize.width - this.width, this.x));
this.y = Math.max(0, Math.min(game.worldSize.height - this.height, this.y));
// Bounce off world edges
if (this.x <= 0 || this.x >= game.worldSize.width - this.width) {
this.direction = Math.PI - this.direction;
}
if (this.y <= 0 || this.y >= game.worldSize.height - this.height) {
this.direction = -this.direction;
}
this.animFrame++;
}
render(ctx, camera) {
const screenX = this.x - camera.x;
const screenY = this.y - camera.y;
// Simple walking animation
const bobOffset = Math.sin(this.animFrame * 0.2) * 1;
ctx.fillStyle = this.color;
ctx.fillRect(screenX, screenY + bobOffset, this.width, this.height);
// Health bar if damaged
if (this.health < 100) {
ctx.fillStyle = '#f00';
ctx.fillRect(screenX, screenY - 8, this.width, 3);
ctx.fillStyle = '#0f0';
ctx.fillRect(screenX, screenY - 8, this.width * (this.health / 100), 3);
}
}
}
class Building extends Entity {
constructor(x, y) {
const width = 60 + Math.random() * 80;
const height = 60 + Math.random() * 100;
super(x, y, width, height, 'building');
this.color = `hsl(${Math.random() * 60 + 200}, 20%, ${20 + Math.random() * 30}%)`;
}
render(ctx, camera) {
const screenX = this.x - camera.x;
const screenY = this.y - camera.y;
// Building body
ctx.fillStyle = this.color;
ctx.fillRect(screenX, screenY, this.width, this.height);
// Windows
ctx.fillStyle = Math.random() > 0.7 ? '#ff0' : '#333';
for (let i = 10; i < this.width - 10; i += 15) {
for (let j = 10; j < this.height - 10; j += 20) {
ctx.fillRect(screenX + i, screenY + j, 8, 12);
}
}
// Building outline
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.strokeRect(screenX, screenY, this.width, this.height);
}
}
// Start the game
window.addEventListener('load', () => {
new Game();
});
</script>
</body>
</html>
```
This GTA-style top-down game includes:
**Core Features:**
- **Player Movement**: Control character with WASD or arrow keys
- **Vehicle System**: Enter/exit vehicles with 'E' key
- **Combat**: Punch pedestrians with 'R' key
- **Collision Detection**: Prevents walking through buildings
- **World Generation**: Random buildings, vehicles, and NPCs
**Visual Elements:**
- **Sprite Animations**: Walking bob animations for player and pedestrians
- **City Environment**: Buildings with windows, cars with details
- **Grid Streets**: Visual street grid overlay
- **Health System**: Health bar and damage visualization
**UI Components:**
- **Minimap**: Shows entire world with entity positions
- **Health Display**: Player health with visual bar
- **Controls Guide**: On-screen control instructions
- **Camera System**: Follows player smoothly
**Gameplay Mechanics:**
- **Vehicle Driving**: Different speed when in vehicles
- **NPC Behavior**: Pedestrians walk randomly around the city
- **World Bounds**: Keeps all entities within the game world
- **Interactive Elements**: Vehicles can be entered/exited
The game creates a mini open-world experience with basic GTA-style mechanics in a compact, playable format!