inital commit with game file and Readme

This commit is contained in:
2025-08-10 17:53:58 +01:00
commit 75a3ae6a6f
2 changed files with 685 additions and 0 deletions

12
Readme.md Normal file
View File

@@ -0,0 +1,12 @@
# ZIP!
This is a very small javascrpt game made using the Claude AI tool (using Claude-sonnet4)
## Instructions
Use the arrow keys ( <- and ->) or the 'a' and 'd' keys to guide the green player to collect as much loot as possible. The enemy player (red) will be trying to do the same.
The loot spawns randomly and is worth less points the longer it is un-collected.
First to 1000 points wins!

673
index.html Normal file
View File

@@ -0,0 +1,673 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loot Collector</title>
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(135deg, #1a1a2e, #16213e, #0f3460);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Courier New', monospace;
color: white;
}
canvas {
border: 3px solid #00ff88;
border-radius: 10px;
box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
}
.ui {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
z-index: 10;
}
.score {
font-size: 24px;
color: #00ff88;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.instructions {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
color: #aaa;
}
.countdown {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 72px;
color: #00ff88;
text-shadow: 0 0 30px rgba(0, 255, 136, 0.8);
z-index: 20;
font-weight: bold;
}
.winner-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 30;
}
.winner-text {
font-size: 64px;
color: #00ff88;
text-shadow: 0 0 40px rgba(0, 255, 136, 1);
margin-bottom: 20px;
animation: pulse 2s infinite;
}
.winner-score {
font-size: 32px;
color: #fff;
margin-bottom: 30px;
}
.restart-button {
padding: 15px 30px;
font-size: 24px;
background: linear-gradient(45deg, #00ff88, #00cc6a);
color: #000;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: bold;
margin-bottom: 15px;
transition: transform 0.2s;
}
.restart-button:hover {
transform: scale(1.05);
}
.space-hint {
color: #aaa;
font-size: 16px;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
</style>
</head>
<body>
<div class="ui">
<div class="score">Player: <span id="playerScore">0</span> | Enemy: <span id="enemyScore">0</span></div>
</div>
<div id="countdownDisplay" class="countdown">5</div>
<div id="winnerScreen" class="winner-screen">
<div id="winnerText" class="winner-text">PLAYER WINS!</div>
<div id="finalScore" class="winner-score">Final Score: Player 1000 - Enemy 850</div>
<button id="restartButton" class="restart-button">PLAY AGAIN</button>
<div class="space-hint">or press SPACE to restart</div>
</div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="instructions">
Use A/D or Left/Right arrows to steer • Collect the glowing loot!
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const playerScoreElement = document.getElementById('playerScore');
const enemyScoreElement = document.getElementById('enemyScore');
const countdownElement = document.getElementById('countdownDisplay');
const winnerScreen = document.getElementById('winnerScreen');
const winnerText = document.getElementById('winnerText');
const finalScore = document.getElementById('finalScore');
const restartButton = document.getElementById('restartButton');
// Game state
let playerScore = 0;
let enemyScore = 0;
let gameRunning = false; // Start paused for countdown
let gameEnded = false;
let countdown = 5; // 5 second countdown
let countdownTimer = 0;
// Player object
const player = {
x: 0, // Will be set randomly
y: 0, // Will be set randomly
size: 20,
speed: 4,
color: '#00ff88',
direction: 0, // angle in radians
velocity: { x: 0, y: 0 }
};
// Trail system
let playerTrail = [];
// Enemy object
const enemy = {
x: 0, // Will be set randomly
y: 0, // Will be set randomly
size: 20,
speed: 3.5,
color: '#ff4444',
direction: 0, // Will be set randomly
velocity: { x: 0, y: 0 },
turnCooldown: 0,
targetLoot: null
};
// Enemy trail system
let enemyTrail = [];
// Loot array
let loot = [];
let lootSpawnTimer = 0;
const lootSpawnInterval = 120; // frames between spawns (2 seconds at 60fps)
// Input handling
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
// Restart with spacebar
if (e.key === ' ' && gameEnded) {
restartGame();
}
});
document.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
});
// Create loot
function createLoot() {
const lootItem = {
x: Math.random() * (canvas.width - 20) + 10,
y: Math.random() * (canvas.height - 20) + 10,
size: 12,
color: `hsl(0, 100%, 60%)`, // Start red
glow: Math.random() * 10 + 5,
pulse: Math.random() * 0.1 + 0.05,
alpha: 0, // Start invisible
fadeSpeed: 0.02,
age: 1 // Start at age 1
};
loot.push(lootItem);
}
// Restart button event
restartButton.addEventListener('click', restartGame);
// Randomize starting positions
function randomizeStartingPositions() {
// Keep players away from edges and each other
const margin = 50;
let validPosition = false;
let attempts = 0;
// Set player position
player.x = Math.random() * (canvas.width - 2 * margin) + margin;
player.y = Math.random() * (canvas.height - 2 * margin) + margin;
player.direction = Math.random() * 2 * Math.PI;
// Set enemy position (make sure it's not too close to player)
while (!validPosition && attempts < 20) {
enemy.x = Math.random() * (canvas.width - 2 * margin) + margin;
enemy.y = Math.random() * (canvas.height - 2 * margin) + margin;
const distance = Math.sqrt(
Math.pow(player.x - enemy.x, 2) +
Math.pow(player.y - enemy.y, 2)
);
if (distance > 100) { // Minimum distance between players
validPosition = true;
}
attempts++;
}
enemy.direction = Math.random() * 2 * Math.PI;
}
// Restart game function
function restartGame() {
// Reset game state
playerScore = 0;
enemyScore = 0;
gameRunning = false;
gameEnded = false;
countdown = 5;
countdownTimer = 0;
// Reset UI
playerScoreElement.textContent = '0';
enemyScoreElement.textContent = '0';
countdownElement.textContent = '5';
countdownElement.style.display = 'block';
winnerScreen.style.display = 'none';
// Randomize starting positions
randomizeStartingPositions();
// Reset velocities
player.velocity.x = 0;
player.velocity.y = 0;
enemy.velocity.x = 0;
enemy.velocity.y = 0;
enemy.turnCooldown = 0;
// Clear trails and loot
playerTrail = [];
enemyTrail = [];
loot = [];
lootSpawnTimer = 0;
}
// Check for winner
function checkWinner() {
if (gameEnded) return;
if (playerScore >= 1000) {
gameEnded = true;
gameRunning = false;
winnerText.textContent = "PLAYER WINS!";
winnerText.style.color = "#00ff88";
finalScore.textContent = `Final Score: Player ${playerScore} - Enemy ${enemyScore}`;
winnerScreen.style.display = 'flex';
} else if (enemyScore >= 1000) {
gameEnded = true;
gameRunning = false;
winnerText.textContent = "ENEMY WINS!";
winnerText.style.color = "#ff4444";
finalScore.textContent = `Final Score: Player ${playerScore} - Enemy ${enemyScore}`;
winnerScreen.style.display = 'flex';
}
}
// Update countdown
function updateCountdown() {
if (countdown > 0) {
countdownTimer++;
if (countdownTimer >= 60) { // 1 second at 60fps
countdown--;
countdownTimer = 0;
if (countdown > 0) {
countdownElement.textContent = countdown;
} else {
countdownElement.textContent = "GO!";
// Start the game after a brief "GO!" display
setTimeout(() => {
gameRunning = true;
countdownElement.style.display = 'none';
// Spawn initial loot
for (let i = 0; i < 5; i++) {
const lootItem = {
x: Math.random() * (canvas.width - 20) + 10,
y: Math.random() * (canvas.height - 20) + 10,
size: 12,
color: `hsl(0, 100%, 60%)`,
glow: Math.random() * 10 + 5,
pulse: Math.random() * 0.1 + 0.05,
alpha: 1,
fadeSpeed: 0.02,
age: 1
};
loot.push(lootItem);
}
}, 500);
}
}
}
}
// Update player position
function updatePlayer() {
if (!gameRunning || gameEnded) return; // Don't move during countdown or when game ended
// Handle steering input
let turning = 0;
if (keys['a'] || keys['arrowleft']) turning -= 0.06;
if (keys['d'] || keys['arrowright']) turning += 0.06;
// Apply turning
player.direction += turning;
// Always move forward in current direction
player.velocity.x = Math.cos(player.direction) * player.speed;
player.velocity.y = Math.sin(player.direction) * player.speed;
// Update position
player.x += player.velocity.x;
player.y += player.velocity.y;
// Bounce off walls
if (player.x - player.size < 0) {
player.x = player.size;
player.direction = Math.PI - player.direction; // Reflect horizontally
}
if (player.x + player.size > canvas.width) {
player.x = canvas.width - player.size;
player.direction = Math.PI - player.direction; // Reflect horizontally
}
if (player.y - player.size < 0) {
player.y = player.size;
player.direction = -player.direction; // Reflect vertically
}
if (player.y + player.size > canvas.height) {
player.y = canvas.height - player.size;
player.direction = -player.direction; // Reflect vertically
}
// Add current position to trail
playerTrail.push({
x: player.x,
y: player.y,
alpha: 1.0,
size: player.size
});
// Limit trail length and fade out
if (playerTrail.length > 15) {
playerTrail.shift();
}
// Fade trail segments
playerTrail.forEach((segment, index) => {
segment.alpha = (index + 1) / playerTrail.length * 0.7;
segment.size = player.size * (segment.alpha * 0.8 + 0.2);
});
}
// Update enemy AI
function updateEnemy() {
if (!gameRunning || gameEnded) return; // Don't move during countdown or when game ended
// Find closest loot
let closestLoot = null;
let closestDistance = Infinity;
loot.forEach(item => {
if (item.alpha > 0.5) { // Only target visible loot
const dx = enemy.x - item.x;
const dy = enemy.y - item.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestLoot = item;
}
}
});
// AI steering towards loot
if (closestLoot && enemy.turnCooldown <= 0) {
const dx = closestLoot.x - enemy.x;
const dy = closestLoot.y - enemy.y;
const targetAngle = Math.atan2(dy, dx);
// Calculate angle difference
let angleDiff = targetAngle - enemy.direction;
// Normalize angle difference to -PI to PI
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Turn towards target (similar turning speed to player)
if (Math.abs(angleDiff) > 0.1) {
if (angleDiff > 0) {
enemy.direction += 0.05;
} else {
enemy.direction -= 0.05;
}
}
}
// Reduce turn cooldown
if (enemy.turnCooldown > 0) enemy.turnCooldown--;
// Always move forward in current direction
enemy.velocity.x = Math.cos(enemy.direction) * enemy.speed;
enemy.velocity.y = Math.sin(enemy.direction) * enemy.speed;
// Update position
enemy.x += enemy.velocity.x;
enemy.y += enemy.velocity.y;
// Bounce off walls (same as player)
if (enemy.x - enemy.size < 0) {
enemy.x = enemy.size;
enemy.direction = Math.PI - enemy.direction;
enemy.turnCooldown = 30; // Brief cooldown after bouncing
}
if (enemy.x + enemy.size > canvas.width) {
enemy.x = canvas.width - enemy.size;
enemy.direction = Math.PI - enemy.direction;
enemy.turnCooldown = 30;
}
if (enemy.y - enemy.size < 0) {
enemy.y = enemy.size;
enemy.direction = -enemy.direction;
enemy.turnCooldown = 30;
}
if (enemy.y + enemy.size > canvas.height) {
enemy.y = canvas.height - enemy.size;
enemy.direction = -enemy.direction;
enemy.turnCooldown = 30;
}
// Add current position to enemy trail
enemyTrail.push({
x: enemy.x,
y: enemy.y,
alpha: 1.0,
size: enemy.size
});
// Limit trail length and fade out
if (enemyTrail.length > 15) {
enemyTrail.shift();
}
// Fade trail segments
enemyTrail.forEach((segment, index) => {
segment.alpha = (index + 1) / enemyTrail.length * 0.7;
segment.size = enemy.size * (segment.alpha * 0.8 + 0.2);
});
}
// Update loot spawning
function updateLoot() {
if (!gameRunning || gameEnded) return; // Don't spawn during countdown or when game ended
lootSpawnTimer++;
// Spawn new loot periodically
if (lootSpawnTimer >= lootSpawnInterval) {
createLoot();
lootSpawnTimer = 0;
}
// Update existing loot
loot.forEach(item => {
// Fade in effect
if (item.alpha < 1) {
item.alpha += item.fadeSpeed;
if (item.alpha > 1) item.alpha = 1;
}
// Age the loot
item.age += 0.008; // Slow aging rate
// Update color based on age (red -> orange -> yellow -> green -> blue -> purple)
const hue = Math.min((item.age - 1) * 60, 300); // 0 to 300 degrees over time
item.color = `hsl(${hue}, 100%, 60%)`;
// Update glow animation
item.glow += item.pulse;
if (item.glow > 15 || item.glow < 5) {
item.pulse = -item.pulse;
}
});
}
// Check collisions
function checkCollisions() {
loot = loot.filter(item => {
const playerDx = player.x - item.x;
const playerDy = player.y - item.y;
const playerDistance = Math.sqrt(playerDx * playerDx + playerDy * playerDy);
const enemyDx = enemy.x - item.x;
const enemyDy = enemy.y - item.y;
const enemyDistance = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy);
// Check player collision
if (playerDistance < player.size + item.size && item.alpha > 0.5) {
// Calculate points based on age (younger = more points)
const points = Math.max(50 - Math.floor(item.age * 10), 5);
playerScore += points;
playerScoreElement.textContent = playerScore;
checkWinner();
return false; // Remove this loot
}
// Check enemy collision
if (enemyDistance < enemy.size + item.size && item.alpha > 0.5) {
// Calculate points based on age (younger = more points)
const points = Math.max(50 - Math.floor(item.age * 10), 5);
enemyScore += points;
enemyScoreElement.textContent = enemyScore;
checkWinner();
return false; // Remove this loot
}
return true;
});
}
// Draw everything
function draw() {
// Clear canvas completely
ctx.fillStyle = 'rgb(26, 26, 46)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw player trail
playerTrail.forEach(segment => {
if (segment.alpha > 0) {
ctx.shadowColor = player.color;
ctx.shadowBlur = 10 * segment.alpha;
ctx.fillStyle = `rgba(0, 255, 136, ${segment.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(segment.x, segment.y, segment.size, 0, Math.PI * 2);
ctx.fill();
}
});
// Draw enemy trail
enemyTrail.forEach(segment => {
if (segment.alpha > 0) {
ctx.shadowColor = enemy.color;
ctx.shadowBlur = 10 * segment.alpha;
ctx.fillStyle = `rgba(255, 68, 68, ${segment.alpha * 0.6})`;
ctx.beginPath();
ctx.arc(segment.x, segment.y, segment.size, 0, Math.PI * 2);
ctx.fill();
}
});
// Draw loot with glow effect
loot.forEach(item => {
if (item.alpha > 0) {
// Draw glow
ctx.shadowColor = item.color;
ctx.shadowBlur = item.glow * item.alpha;
ctx.fillStyle = item.color.replace('60%)', `${60 * item.alpha}%)`);
ctx.beginPath();
ctx.arc(item.x, item.y, item.size * item.alpha, 0, Math.PI * 2);
ctx.fill();
// Draw inner bright core
ctx.shadowBlur = 0;
ctx.fillStyle = `rgba(255, 255, 255, ${item.alpha})`;
ctx.beginPath();
ctx.arc(item.x, item.y, item.size * 0.3 * item.alpha, 0, Math.PI * 2);
ctx.fill();
}
});
// Draw player (main body)
ctx.shadowColor = player.color;
ctx.shadowBlur = 15;
ctx.fillStyle = player.color;
ctx.beginPath();
ctx.arc(player.x, player.y, player.size, 0, Math.PI * 2);
ctx.fill();
// Player inner glow
ctx.shadowBlur = 0;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(player.x, player.y, player.size * 0.4, 0, Math.PI * 2);
ctx.fill();
// Draw enemy (main body)
ctx.shadowColor = enemy.color;
ctx.shadowBlur = 15;
ctx.fillStyle = enemy.color;
ctx.beginPath();
ctx.arc(enemy.x, enemy.y, enemy.size, 0, Math.PI * 2);
ctx.fill();
// Enemy inner glow
ctx.shadowBlur = 0;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(enemy.x, enemy.y, enemy.size * 0.4, 0, Math.PI * 2);
ctx.fill();
}
// Game loop
function gameLoop() {
updateCountdown();
updatePlayer();
updateEnemy();
updateLoot();
checkCollisions();
draw();
requestAnimationFrame(gameLoop);
}
// Start the game with random positions
randomizeStartingPositions();
gameLoop();
</script>
</body>
</html>