|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Retro GTA 1-Style Game</title>
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
<style>
|
|
|
body {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
overflow: hidden;
|
|
|
background-color: #222;
|
|
|
font-family: 'Courier New', monospace;
|
|
|
}
|
|
|
|
|
|
#game-container {
|
|
|
position: relative;
|
|
|
width: 100vw;
|
|
|
height: 100vh;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
#game-canvas {
|
|
|
background-color: #333;
|
|
|
border: 4px solid #555;
|
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
|
image-rendering: pixelated;
|
|
|
}
|
|
|
|
|
|
#hud {
|
|
|
position: absolute;
|
|
|
top: 10px;
|
|
|
left: 10px;
|
|
|
color: white;
|
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
|
padding: 10px;
|
|
|
border: 2px solid #555;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
#menu {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
left: 50%;
|
|
|
transform: translate(-50%, -50%);
|
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
|
border: 4px solid #555;
|
|
|
padding: 20px;
|
|
|
color: white;
|
|
|
text-align: center;
|
|
|
z-index: 100;
|
|
|
}
|
|
|
|
|
|
.pixel-button {
|
|
|
background-color: #444;
|
|
|
color: white;
|
|
|
border: 2px solid #666;
|
|
|
padding: 10px 20px;
|
|
|
margin: 5px;
|
|
|
font-family: 'Courier New', monospace;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.2s;
|
|
|
}
|
|
|
|
|
|
.pixel-button:hover {
|
|
|
background-color: #666;
|
|
|
border-color: #888;
|
|
|
}
|
|
|
|
|
|
.mission-marker {
|
|
|
position: absolute;
|
|
|
width: 16px;
|
|
|
height: 16px;
|
|
|
background-color: red;
|
|
|
border: 2px solid white;
|
|
|
border-radius: 50%;
|
|
|
transform: translate(-50%, -50%);
|
|
|
}
|
|
|
|
|
|
.car {
|
|
|
position: absolute;
|
|
|
width: 32px;
|
|
|
height: 32px;
|
|
|
background-color: blue;
|
|
|
transform-origin: center;
|
|
|
}
|
|
|
|
|
|
.pedestrian {
|
|
|
position: absolute;
|
|
|
width: 12px;
|
|
|
height: 12px;
|
|
|
background-color: green;
|
|
|
border-radius: 50%;
|
|
|
}
|
|
|
|
|
|
.building {
|
|
|
position: absolute;
|
|
|
background-color: #555;
|
|
|
border: 2px solid #333;
|
|
|
}
|
|
|
|
|
|
.road {
|
|
|
position: absolute;
|
|
|
background-color: #444;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<div id="game-container">
|
|
|
<canvas id="game-canvas"></canvas>
|
|
|
|
|
|
<div id="hud">
|
|
|
<div>Time: <span id="time">12:00</span></div>
|
|
|
<div>Score: <span id="score">0</span></div>
|
|
|
<div>Wanted: <span id="wanted">★</span></div>
|
|
|
<div>Health: <span id="health">100%</span></div>
|
|
|
<div>Money: $<span id="money">1000</span></div>
|
|
|
</div>
|
|
|
|
|
|
<div id="menu" style="display: none;">
|
|
|
<h1 class="text-2xl mb-4">RETRO GTA</h1>
|
|
|
<div class="mb-6">
|
|
|
<button id="start-game" class="pixel-button">START GAME</button>
|
|
|
<button id="controls" class="pixel-button">CONTROLS</button>
|
|
|
</div>
|
|
|
<div id="controls-info" style="display: none;">
|
|
|
<p class="mb-2">WASD or Arrow Keys - Move</p>
|
|
|
<p class="mb-2">Space - Handbrake</p>
|
|
|
<p class="mb-2">E - Enter/Exit Vehicle</p>
|
|
|
<p class="mb-2">M - Mission Start</p>
|
|
|
<button id="back-to-menu" class="pixel-button mt-4">BACK</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
const gameState = {
|
|
|
player: {
|
|
|
x: 400,
|
|
|
y: 300,
|
|
|
speed: 0,
|
|
|
angle: 0,
|
|
|
maxSpeed: 5,
|
|
|
acceleration: 0.1,
|
|
|
rotationSpeed: 3,
|
|
|
inVehicle: true,
|
|
|
health: 100,
|
|
|
money: 1000,
|
|
|
score: 0,
|
|
|
wantedLevel: 0
|
|
|
},
|
|
|
time: {
|
|
|
hours: 12,
|
|
|
minutes: 0
|
|
|
},
|
|
|
vehicles: [],
|
|
|
pedestrians: [],
|
|
|
buildings: [],
|
|
|
roads: [],
|
|
|
mission: {
|
|
|
active: false,
|
|
|
marker: { x: 600, y: 200 }
|
|
|
},
|
|
|
keys: {}
|
|
|
};
|
|
|
|
|
|
|
|
|
const canvas = document.getElementById('game-canvas');
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
const menu = document.getElementById('menu');
|
|
|
const startButton = document.getElementById('start-game');
|
|
|
const controlsButton = document.getElementById('controls');
|
|
|
const controlsInfo = document.getElementById('controls-info');
|
|
|
const backButton = document.getElementById('back-to-menu');
|
|
|
const timeDisplay = document.getElementById('time');
|
|
|
const scoreDisplay = document.getElementById('score');
|
|
|
const wantedDisplay = document.getElementById('wanted');
|
|
|
const healthDisplay = document.getElementById('health');
|
|
|
const moneyDisplay = document.getElementById('money');
|
|
|
|
|
|
|
|
|
function resizeCanvas() {
|
|
|
canvas.width = window.innerWidth * 0.9;
|
|
|
canvas.height = window.innerHeight * 0.9;
|
|
|
}
|
|
|
|
|
|
|
|
|
function initGame() {
|
|
|
resizeCanvas();
|
|
|
showMenu();
|
|
|
generateCity();
|
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
window.addEventListener('keyup', handleKeyUp);
|
|
|
|
|
|
startButton.addEventListener('click', startGame);
|
|
|
controlsButton.addEventListener('click', showControls);
|
|
|
backButton.addEventListener('click', showMenu);
|
|
|
}
|
|
|
|
|
|
|
|
|
function showMenu() {
|
|
|
menu.style.display = 'block';
|
|
|
controlsInfo.style.display = 'none';
|
|
|
}
|
|
|
|
|
|
|
|
|
function showControls() {
|
|
|
menu.style.display = 'block';
|
|
|
controlsInfo.style.display = 'block';
|
|
|
}
|
|
|
|
|
|
|
|
|
function startGame() {
|
|
|
menu.style.display = 'none';
|
|
|
gameLoop();
|
|
|
setInterval(updateTime, 60000);
|
|
|
}
|
|
|
|
|
|
|
|
|
function generateCity() {
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
gameState.roads.push({
|
|
|
x: i * 100,
|
|
|
y: 200,
|
|
|
width: 80,
|
|
|
height: 20
|
|
|
});
|
|
|
|
|
|
gameState.roads.push({
|
|
|
x: 400,
|
|
|
y: i * 80,
|
|
|
width: 20,
|
|
|
height: 60
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 15; i++) {
|
|
|
gameState.buildings.push({
|
|
|
x: Math.random() * canvas.width,
|
|
|
y: Math.random() * canvas.height,
|
|
|
width: 60 + Math.random() * 100,
|
|
|
height: 80 + Math.random() * 120
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
gameState.pedestrians.push({
|
|
|
x: Math.random() * canvas.width,
|
|
|
y: Math.random() * canvas.height,
|
|
|
speed: 0.5 + Math.random() * 1.5,
|
|
|
direction: Math.random() * 360
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
gameState.vehicles.push({
|
|
|
x: Math.random() * canvas.width,
|
|
|
y: Math.random() * canvas.height,
|
|
|
speed: 1 + Math.random() * 3,
|
|
|
angle: Math.random() * 360,
|
|
|
color: `hsl(${Math.random() * 360}, 70%, 50%)`
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function handleKeyDown(e) {
|
|
|
gameState.keys[e.key] = true;
|
|
|
|
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
if (menu.style.display === 'none') {
|
|
|
menu.style.display = 'block';
|
|
|
} else {
|
|
|
menu.style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (e.key === 'm' && !gameState.mission.active) {
|
|
|
gameState.mission.active = true;
|
|
|
gameState.mission.marker = {
|
|
|
x: Math.random() * canvas.width,
|
|
|
y: Math.random() * canvas.height
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function handleKeyUp(e) {
|
|
|
gameState.keys[e.key] = false;
|
|
|
}
|
|
|
|
|
|
|
|
|
function updateTime() {
|
|
|
gameState.time.minutes += 10;
|
|
|
if (gameState.time.minutes >= 60) {
|
|
|
gameState.time.minutes = 0;
|
|
|
gameState.time.hours = (gameState.time.hours + 1) % 24;
|
|
|
}
|
|
|
|
|
|
const hoursStr = gameState.time.hours.toString().padStart(2, '0');
|
|
|
const minsStr = gameState.time.minutes.toString().padStart(2, '0');
|
|
|
timeDisplay.textContent = `${hoursStr}:${minsStr}`;
|
|
|
}
|
|
|
|
|
|
|
|
|
function update() {
|
|
|
const player = gameState.player;
|
|
|
|
|
|
|
|
|
if (player.inVehicle) {
|
|
|
|
|
|
if (gameState.keys['ArrowUp'] || gameState.keys['w']) {
|
|
|
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed);
|
|
|
} else if (gameState.keys['ArrowDown'] || gameState.keys['s']) {
|
|
|
player.speed = Math.max(player.speed - player.acceleration, -player.maxSpeed / 2);
|
|
|
} else {
|
|
|
|
|
|
player.speed *= 0.95;
|
|
|
if (Math.abs(player.speed) < 0.1) player.speed = 0;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (player.speed !== 0) {
|
|
|
if (gameState.keys['ArrowLeft'] || gameState.keys['a']) {
|
|
|
player.angle -= player.rotationSpeed * (player.speed / player.maxSpeed);
|
|
|
}
|
|
|
if (gameState.keys['ArrowRight'] || gameState.keys['d']) {
|
|
|
player.angle += player.rotationSpeed * (player.speed / player.maxSpeed);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (gameState.keys[' ']) {
|
|
|
player.speed *= 0.8;
|
|
|
}
|
|
|
|
|
|
|
|
|
const rad = player.angle * Math.PI / 180;
|
|
|
player.x += Math.sin(rad) * player.speed;
|
|
|
player.y -= Math.cos(rad) * player.speed;
|
|
|
|
|
|
|
|
|
for (const building of gameState.buildings) {
|
|
|
if (checkCollision(
|
|
|
player.x, player.y, 32, 32,
|
|
|
building.x, building.y, building.width, building.height
|
|
|
)) {
|
|
|
|
|
|
player.speed = -player.speed * 0.5;
|
|
|
player.health -= 5;
|
|
|
healthDisplay.textContent = `${player.health}%`;
|
|
|
|
|
|
|
|
|
if (gameState.player.wantedLevel < 5) {
|
|
|
gameState.player.wantedLevel++;
|
|
|
wantedDisplay.textContent = '★'.repeat(gameState.player.wantedLevel);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
for (const vehicle of gameState.vehicles) {
|
|
|
const rad = vehicle.angle * Math.PI / 180;
|
|
|
vehicle.x += Math.sin(rad) * vehicle.speed;
|
|
|
vehicle.y -= Math.cos(rad) * vehicle.speed;
|
|
|
|
|
|
|
|
|
if (Math.random() < 0.01) {
|
|
|
vehicle.angle += (Math.random() - 0.5) * 30;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (vehicle.x < 0) vehicle.x = canvas.width;
|
|
|
if (vehicle.x > canvas.width) vehicle.x = 0;
|
|
|
if (vehicle.y < 0) vehicle.y = canvas.height;
|
|
|
if (vehicle.y > canvas.height) vehicle.y = 0;
|
|
|
}
|
|
|
|
|
|
|
|
|
for (const ped of gameState.pedestrians) {
|
|
|
const rad = ped.direction * Math.PI / 180;
|
|
|
ped.x += Math.sin(rad) * ped.speed;
|
|
|
ped.y -= Math.cos(rad) * ped.speed;
|
|
|
|
|
|
|
|
|
if (Math.random() < 0.02) {
|
|
|
ped.direction += (Math.random() - 0.5) * 90;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (ped.x < 0 || ped.x > canvas.width || ped.y < 0 || ped.y > canvas.height) {
|
|
|
ped.direction = Math.atan2(canvas.width / 2 - ped.x, canvas.height / 2 - ped.y) * 180 / Math.PI;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (gameState.mission.active) {
|
|
|
const marker = gameState.mission.marker;
|
|
|
if (Math.sqrt((player.x - marker.x) ** 2 + (player.y - marker.y) ** 2) < 30) {
|
|
|
gameState.mission.active = false;
|
|
|
player.money += 500;
|
|
|
player.score += 1000;
|
|
|
moneyDisplay.textContent = player.money;
|
|
|
scoreDisplay.textContent = player.score;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function checkCollision(x1, y1, w1, h1, x2, y2, w2, h2) {
|
|
|
return x1 < x2 + w2 &&
|
|
|
x1 + w1 > x2 &&
|
|
|
y1 < y2 + h2 &&
|
|
|
y1 + h1 > y2;
|
|
|
}
|
|
|
|
|
|
|
|
|
function render() {
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#444';
|
|
|
for (const road of gameState.roads) {
|
|
|
ctx.fillRect(road.x, road.y, road.width, road.height);
|
|
|
}
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#555';
|
|
|
ctx.strokeStyle = '#333';
|
|
|
ctx.lineWidth = 2;
|
|
|
for (const building of gameState.buildings) {
|
|
|
ctx.fillRect(building.x, building.y, building.width, building.height);
|
|
|
ctx.strokeRect(building.x, building.y, building.width, building.height);
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#777';
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
for (let j = 0; j < 3; j++) {
|
|
|
ctx.fillRect(
|
|
|
building.x + 10 + i * 15,
|
|
|
building.y + 10 + j * 15,
|
|
|
8, 8
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
ctx.fillStyle = '#555';
|
|
|
}
|
|
|
|
|
|
|
|
|
ctx.fillStyle = 'green';
|
|
|
for (const ped of gameState.pedestrians) {
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(ped.x, ped.y, 6, 0, Math.PI * 2);
|
|
|
ctx.fill();
|
|
|
}
|
|
|
|
|
|
|
|
|
for (const vehicle of gameState.vehicles) {
|
|
|
ctx.save();
|
|
|
ctx.translate(vehicle.x, vehicle.y);
|
|
|
ctx.rotate(vehicle.angle * Math.PI / 180);
|
|
|
ctx.fillStyle = vehicle.color;
|
|
|
ctx.fillRect(-16, -16, 32, 32);
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#333';
|
|
|
ctx.fillRect(-12, -12, 24, 8);
|
|
|
ctx.fillRect(-12, 4, 24, 8);
|
|
|
ctx.restore();
|
|
|
}
|
|
|
|
|
|
|
|
|
const player = gameState.player;
|
|
|
ctx.save();
|
|
|
ctx.translate(player.x, player.y);
|
|
|
ctx.rotate(player.angle * Math.PI / 180);
|
|
|
ctx.fillStyle = 'blue';
|
|
|
ctx.fillRect(-16, -16, 32, 32);
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#333';
|
|
|
ctx.fillRect(-12, -12, 24, 8);
|
|
|
ctx.fillRect(-12, 4, 24, 8);
|
|
|
ctx.fillStyle = 'red';
|
|
|
ctx.fillRect(12, -8, 4, 4);
|
|
|
ctx.fillRect(12, 4, 4, 4);
|
|
|
ctx.fillStyle = 'yellow';
|
|
|
ctx.fillRect(-16, -8, 4, 4);
|
|
|
ctx.fillRect(-16, 4, 4, 4);
|
|
|
ctx.restore();
|
|
|
|
|
|
|
|
|
if (gameState.mission.active) {
|
|
|
ctx.fillStyle = 'red';
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(
|
|
|
gameState.mission.marker.x,
|
|
|
gameState.mission.marker.y,
|
|
|
10, 0, Math.PI * 2
|
|
|
);
|
|
|
ctx.fill();
|
|
|
ctx.strokeStyle = 'white';
|
|
|
ctx.lineWidth = 2;
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
ctx.beginPath();
|
|
|
ctx.moveTo(player.x, player.y);
|
|
|
ctx.lineTo(gameState.mission.marker.x, gameState.mission.marker.y);
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
|
|
ctx.lineWidth = 1;
|
|
|
ctx.stroke();
|
|
|
}
|
|
|
|
|
|
|
|
|
const minimapSize = 150;
|
|
|
const minimapX = canvas.width - minimapSize - 20;
|
|
|
const minimapY = 20;
|
|
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
|
ctx.fillRect(minimapX, minimapY, minimapSize, minimapSize);
|
|
|
ctx.strokeStyle = '#555';
|
|
|
ctx.lineWidth = 2;
|
|
|
ctx.strokeRect(minimapX, minimapY, minimapSize, minimapSize);
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#666';
|
|
|
for (const road of gameState.roads) {
|
|
|
const mx = minimapX + (road.x / canvas.width) * minimapSize;
|
|
|
const my = minimapY + (road.y / canvas.height) * minimapSize;
|
|
|
const mw = (road.width / canvas.width) * minimapSize;
|
|
|
const mh = (road.height / canvas.height) * minimapSize;
|
|
|
ctx.fillRect(mx, my, mw, mh);
|
|
|
}
|
|
|
|
|
|
|
|
|
ctx.fillStyle = 'blue';
|
|
|
const px = minimapX + (player.x / canvas.width) * minimapSize;
|
|
|
const py = minimapY + (player.y / canvas.height) * minimapSize;
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(px, py, 3, 0, Math.PI * 2);
|
|
|
ctx.fill();
|
|
|
|
|
|
|
|
|
if (gameState.mission.active) {
|
|
|
ctx.fillStyle = 'red';
|
|
|
const mx = minimapX + (gameState.mission.marker.x / canvas.width) * minimapSize;
|
|
|
const my = minimapY + (gameState.mission.marker.y / canvas.height) * minimapSize;
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(mx, my, 3, 0, Math.PI * 2);
|
|
|
ctx.fill();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function gameLoop() {
|
|
|
update();
|
|
|
render();
|
|
|
requestAnimationFrame(gameLoop);
|
|
|
}
|
|
|
|
|
|
|
|
|
window.onload = initGame;
|
|
|
</script>
|
|
|
</body>
|
|
|
|
|
|
</html> |