GTA Tunis - Day 6
Advanced LevelThe world is larger than the viewport. The camera follows the player, showing only a portion of the world.
// World dimensions (larger than canvas)
const worldWidth = 2000;
const worldHeight = 2000;
// Camera tracks player position
const camera = { x: 0, y: 0 };
function updateCamera() {
// Center camera on player
camera.x = player.x - canvas.width / 2;
camera.y = player.y - canvas.height / 2;
// Clamp to world bounds
camera.x = Math.max(0, Math.min(worldWidth - canvas.width, camera.x));
camera.y = Math.max(0, Math.min(worldHeight - canvas.height, camera.y));
}
// Convert world coords to screen coords for drawing
function worldToScreen(worldX, worldY) {
return {
x: worldX - camera.x,
y: worldY - camera.y
};
}
// Only draw objects visible on screen (optimization)
function isVisible(obj) {
return obj.x + obj.width > camera.x &&
obj.x < camera.x + canvas.width &&
obj.y + obj.height > camera.y &&
obj.y < camera.y + canvas.height;
}
Cars have acceleration, deceleration, and steering that depends on speed. No physics library needed.
const car = {
x: 500,
y: 500,
angle: 0, // Facing direction (radians)
speed: 0,
maxSpeed: 8,
acceleration: 0.2,
friction: 0.98 // Speed multiplier when not accelerating
};
function updateCar() {
// Acceleration / braking
if (keys.up) {
car.speed = Math.min(car.maxSpeed, car.speed + car.acceleration);
} else if (keys.down) {
car.speed = Math.max(-car.maxSpeed / 2, car.speed - car.acceleration);
} else {
// Apply friction when not accelerating
car.speed *= car.friction;
if (Math.abs(car.speed) < 0.1) car.speed = 0;
}
// Steering - turn rate depends on speed
const turnRate = 0.05 * (car.speed / car.maxSpeed);
if (keys.left) car.angle -= turnRate;
if (keys.right) car.angle += turnRate;
// Apply velocity based on angle
car.x += Math.cos(car.angle) * car.speed;
car.y += Math.sin(car.angle) * car.speed;
}
Steering that depends on speed (slower = tighter turns) feels more realistic than constant turn rate.
The city is generated programmatically with a grid of roads and randomly placed buildings.
function generateCity() {
const blockSize = 300;
const roadWidth = 60;
// Generate grid of roads
for (let y = blockSize; y < worldHeight; y += blockSize) {
roads.push({
x: 0, y: y - roadWidth/2,
width: worldWidth, height: roadWidth,
type: 'horizontal'
});
}
for (let x = blockSize; x < worldWidth; x += blockSize) {
roads.push({
x: x - roadWidth/2, y: 0,
width: roadWidth, height: worldHeight,
type: 'vertical'
});
}
// Generate buildings in blocks (between roads)
for (let bx = 50; bx < worldWidth - blockSize; bx += blockSize) {
for (let by = 50; by < worldHeight - blockSize; by += blockSize) {
const numBuildings = 2 + Math.floor(Math.random() * 3);
for (let i = 0; i < numBuildings; i++) {
buildings.push({
x: bx + Math.random() * (blockSize - 150),
y: by + Math.random() * (blockSize - 150),
width: 60 + Math.random() * 80,
height: 60 + Math.random() * 80,
color: ['#e8dcc4', '#f5f0e1', '#87ceeb'][Math.floor(Math.random() * 3)],
hasDome: Math.random() > 0.7 // Tunisian architecture
});
}
}
}
}
Police spawn when wanted level increases and chase the player using simple seek behavior.
function updatePolice() {
// Spawn police based on wanted level
if (police.length < wantedLevel * 2) {
// Spawn off-screen, near player
const angle = Math.random() * Math.PI * 2;
const dist = 400;
police.push({
x: player.x + Math.cos(angle) * dist,
y: player.y + Math.sin(angle) * dist,
speed: 2.5
});
}
// Chase player
for (const cop of police) {
// Calculate angle to player
const dx = player.x - cop.x;
const dy = player.y - cop.y;
cop.angle = Math.atan2(dy, dx);
// Move toward player
cop.x += Math.cos(cop.angle) * cop.speed;
cop.y += Math.sin(cop.angle) * cop.speed;
// Check if caught player
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance < 30 && !player.inCar) {
bustPlayer(); // Caught!
}
}
}
// Wanted level decays over time when not causing trouble
if (wantedLevel > 0 && !isShooting) {
wantedLevel -= 0.001;
}
The minimap shows a scaled-down view of the world with player and police positions.
function drawMinimap() {
const mmWidth = 100, mmHeight = 100;
const mmX = canvas.width - mmWidth - 10;
const mmY = 10;
// Scale factors
const scaleX = mmWidth / worldWidth;
const scaleY = mmHeight / worldHeight;
// Background
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(mmX, mmY, mmWidth, mmHeight);
// Draw roads
ctx.fillStyle = '#444';
for (const road of roads) {
ctx.fillRect(
mmX + road.x * scaleX,
mmY + road.y * scaleY,
road.width * scaleX,
road.height * scaleY
);
}
// Player position (green dot)
ctx.fillStyle = '#00ff00';
ctx.beginPath();
ctx.arc(mmX + player.x * scaleX, mmY + player.y * scaleY, 3, 0, Math.PI * 2);
ctx.fill();
// Police positions (red dots)
ctx.fillStyle = '#ff0000';
for (const cop of police) {
ctx.beginPath();
ctx.arc(mmX + cop.x * scaleX, mmY + cop.y * scaleY, 2, 0, Math.PI * 2);
ctx.fill();
}
// Border
ctx.strokeStyle = '#f39c12';
ctx.strokeRect(mmX, mmY, mmWidth, mmHeight);
}
When the player collides with a building, we push them out based on which side they hit.
function handleBuildingCollision() {
for (const b of buildings) {
if (player.x > b.x && player.x < b.x + b.width &&
player.y > b.y && player.y < b.y + b.height) {
// Find building center
const cx = b.x + b.width / 2;
const cy = b.y + b.height / 2;
// Player position relative to center
const px = player.x - cx;
const py = player.y - cy;
// Push out on axis with less penetration
if (Math.abs(px) > Math.abs(py)) {
// Push horizontally
player.x = px > 0
? b.x + b.width + 5 // Push right
: b.x - 5; // Push left
} else {
// Push vertically
player.y = py > 0
? b.y + b.height + 5 // Push down
: b.y - 5; // Push up
}
}
}
}