anycoder-7bd4b02e / index.html
Matanga's picture
Upload folder using huggingface_hub
9cecdf5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASTRAL COLONIZER - Procedural Space Simulation</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--neon-cyan: #00f3ff;
--neon-pink: #ff00ff;
--neon-amber: #ffaa00;
--deep-space: #050510;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.1);
}
body {
background-color: var(--deep-space);
color: white;
font-family: 'Space Grotesk', sans-serif;
overflow: hidden;
user-select: none;
}
.mono { font-family: 'JetBrains Mono', monospace; }
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a1a; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--neon-cyan); }
/* Glassmorphism */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
}
.glass-card {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.01) 100%);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
}
.glass-card:hover:not(:disabled) {
border-color: var(--neon-cyan);
background: rgba(0, 243, 255, 0.05);
transform: translateY(-2px);
}
/* Animations */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 5px var(--neon-cyan); }
50% { box-shadow: 0 0 20px var(--neon-cyan); }
}
@keyframes scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
.scan-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(to bottom, transparent 50%, rgba(0, 243, 255, 0.02) 51%);
background-size: 100% 4px;
pointer-events: none;
z-index: 40;
}
.crt-flicker {
animation: flicker 0.15s infinite;
}
@keyframes flicker {
0% { opacity: 0.97; }
100% { opacity: 1; }
}
/* Game UI Elements */
.resource-bar-fill {
transition: width 0.3s ease-out;
}
.btn-action {
position: relative;
overflow: hidden;
}
.btn-action::after {
content: '';
position: absolute;
top: 0; left: -100%;
width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: 0.5s;
}
.btn-action:hover::after {
left: 100%;
}
/* Tooltip */
#tooltip {
position: absolute;
pointer-events: none;
z-index: 100;
opacity: 0;
transition: opacity 0.1s;
transform: translate(15px, 15px);
}
canvas {
image-rendering: pixelated; /* Retro feel */
}
</style>
</head>
<body class="h-screen w-screen flex flex-col overflow-hidden crt-flicker">
<!-- Background Starfield Canvas -->
<canvas id="bgCanvas" class="fixed top-0 left-0 w-full h-full z-0"></canvas>
<!-- CRT Scanline Overlay -->
<div class="scan-overlay"></div>
<!-- Main Game Container -->
<div class="relative z-10 flex flex-col h-full p-4 gap-4">
<!-- Header -->
<header class="glass-panel rounded-lg p-3 flex justify-between items-center shrink-0">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center shadow-[0_0_15px_rgba(6,182,212,0.6)]">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
</div>
<div>
<h1 class="text-xl font-bold tracking-widest uppercase mono text-cyan-400">Astral Colonizer</h1>
<div class="text-[10px] text-gray-400 mono flex gap-4">
<span>SECTOR: <span id="sector-id" class="text-white">ALPHA-9</span></span>
<span>CYCLE: <span id="game-cycle" class="text-white">1</span></span>
</div>
</div>
</div>
<div class="flex gap-6 text-sm mono">
<div class="flex flex-col items-end w-32">
<div class="flex justify-between w-full text-xs text-gray-400 mb-1">
<span>ENERGY</span>
<span id="val-energy">100</span>
</div>
<div class="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div id="bar-energy" class="h-full bg-yellow-400 resource-bar-fill" style="width: 100%"></div>
</div>
</div>
<div class="flex flex-col items-end w-32">
<div class="flex justify-between w-full text-xs text-gray-400 mb-1">
<span>MATTER</span>
<span id="val-matter">50</span>
</div>
<div class="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div id="bar-matter" class="h-full bg-blue-500 resource-bar-fill" style="width: 50%"></div>
</div>
</div>
<div class="flex flex-col items-end w-32">
<div class="flex justify-between w-full text-xs text-gray-400 mb-1">
<span>POPULATION</span>
<span id="val-pop">0</span>
</div>
<div class="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div id="bar-pop" class="h-full bg-green-500 resource-bar-fill" style="width: 0%"></div>
</div>
</div>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-[10px] mono text-gray-500 hover:text-cyan-400 transition-colors border border-white/10 px-2 py-1 rounded">
Built with anycoder
</a>
</header>
<!-- Game Area -->
<div class="flex flex-1 gap-4 min-h-0">
<!-- Left Panel: Controls & Log -->
<aside class="w-64 flex flex-col gap-4 shrink-0">
<!-- Build Menu -->
<div class="glass-panel rounded-lg p-4 flex-1 flex flex-col gap-2 overflow-y-auto">
<h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 border-b border-white/10 pb-1">Construction</h3>
<button onclick="game.selectTool('solar')" id="btn-solar" class="glass-card p-3 rounded flex items-center gap-3 text-left btn-action group">
<div class="w-8 h-8 rounded bg-yellow-500/20 text-yellow-400 flex items-center justify-center border border-yellow-500/30 group-hover:bg-yellow-500 group-hover:text-black transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
</div>
<div>
<div class="text-sm font-bold text-white">Solar Array</div>
<div class="text-[10px] text-yellow-500 mono">Cost: 20 M</div>
</div>
</button>
<button onclick="game.selectTool('mine')" id="btn-mine" class="glass-card p-3 rounded flex items-center gap-3 text-left btn-action group">
<div class="w-8 h-8 rounded bg-blue-500/20 text-blue-400 flex items-center justify-center border border-blue-500/30 group-hover:bg-blue-500 group-hover:text-black transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
</div>
<div>
<div class="text-sm font-bold text-white">Extractor</div>
<div class="text-[10px] text-blue-500 mono">Cost: 30 M, 10 E</div>
</div>
</button>
<button onclick="game.selectTool('habitat')" id="btn-habitat" class="glass-card p-3 rounded flex items-center gap-3 text-left btn-action group">
<div class="w-8 h-8 rounded bg-green-500/20 text-green-400 flex items-center justify-center border border-green-500/30 group-hover:bg-green-500 group-hover:text-black transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
</div>
<div>
<div class="text-sm font-bold text-white">Habitat</div>
<div class="text-[10px] text-green-500 mono">Cost: 100 M, 50 E</div>
</div>
</button>
<button onclick="game.selectTool('defense')" id="btn-defense" class="glass-card p-3 rounded flex items-center gap-3 text-left btn-action group">
<div class="w-8 h-8 rounded bg-red-500/20 text-red-400 flex items-center justify-center border border-red-500/30 group-hover:bg-red-500 group-hover:text-black transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<div>
<div class="text-sm font-bold text-white">Laser Turret</div>
<div class="text-[10px] text-red-500 mono">Cost: 80 M, 40 E</div>
</div>
</button>
<div class="mt-auto pt-4 border-t border-white/10">
<button onclick="game.selectTool('demolish')" id="btn-demolish" class="w-full py-2 border border-red-900/50 bg-red-900/10 text-red-400 text-xs font-bold rounded hover:bg-red-900/30 transition-colors">
DEMOLISH MODE
</button>
</div>
</div>
<!-- Event Log -->
<div class="glass-panel rounded-lg p-3 h-48 flex flex-col">
<h3 class="text-[10px] font-bold text-gray-400 uppercase mb-2">System Log</h3>
<div id="game-log" class="flex-1 overflow-y-auto text-[10px] mono space-y-1 pr-1">
<div class="text-cyan-400">> System initialized...</div>
<div class="text-gray-400">> Awaiting commands.</div>
</div>
</div>
</aside>
<!-- Center: The World -->
<main class="flex-1 glass-panel rounded-lg relative overflow-hidden cursor-crosshair" id="canvas-container">
<canvas id="gameCanvas" class="block w-full h-full"></canvas>
<!-- Overlay Info -->
<div class="absolute top-4 left-4 pointer-events-none">
<div class="bg-black/60 backdrop-blur px-3 py-1 rounded border border-white/10 text-xs mono">
<span class="text-gray-400">COORDS:</span> <span id="mouse-coords" class="text-white">0, 0</span>
</div>
</div>
<!-- Game Over / Win Screen -->
<div id="overlay-screen" class="absolute inset-0 bg-black/80 backdrop-blur-md z-50 flex flex-col items-center justify-center hidden">
<h2 id="overlay-title" class="text-4xl font-bold text-white mb-4 tracking-widest uppercase">GAME OVER</h2>
<p id="overlay-msg" class="text-gray-300 mb-8 mono text-center max-w-md">The colony has fallen.</p>
<button onclick="location.reload()" class="px-8 py-3 bg-cyan-600 hover:bg-cyan-500 text-white font-bold rounded shadow-[0_0_20px_rgba(8,145,178,0.6)] transition-all">
REBOOT SYSTEM
</button>
</div>
</main>
</div>
</div>
<!-- Tooltip Element -->
<div id="tooltip" class="glass-panel p-2 rounded border border-cyan-500/30 text-xs max-w-[200px]">
<div id="tt-title" class="font-bold text-cyan-400 mb-1">Building</div>
<div id="tt-desc" class="text-gray-300 leading-tight">Description here.</div>
</div>
<script>
/**
* UTILITIES & CONFIG
*/
const CONSTANTS = {
TILE_SIZE: 40,
GRID_W: 20,
GRID_H: 15,
COLORS: {
VOID: '#050510',
STAR: '#ffffff',
PLANET_BASE: '#1a1a2e',
PLANET_HIGHLIGHT: '#2a2a4e',
SOLAR: '#fbbf24',
MINE: '#3b82f6',
HABITAT: '#22c55e',
DEFENSE: '#ef4444',
SELECTED: '#00f3ff'
}
};
// Simple PRNG for deterministic map generation if needed, but we'll use Math.random for gameplay variability
const rand = (min, max) => Math.random() * (max - min) + min;
const randInt = (min, max) => Math.floor(rand(min, max));
/**
* AUDIO SYSTEM (Synthesizer)
* Using Web Audio API to generate sounds without external assets
*/
const AudioSys = {
ctx: null,
init: function() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
},
playTone: function(freq, type, duration, vol = 0.1) {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
},
playClick: () => AudioSys.playTone(800, 'sine', 0.1, 0.05),
playBuild: () => {
AudioSys.playTone(400, 'triangle', 0.1, 0.1);
setTimeout(() => AudioSys.playTone(600, 'triangle', 0.2, 0.1), 100);
},
playError: () => AudioSys.playTone(150, 'sawtooth', 0.3, 0.1),
playShoot: () => {
if(!AudioSys.ctx) return;
const osc = AudioSys.ctx.createOscillator();
const gain = AudioSys.ctx.createGain();
osc.frequency.setValueAtTime(800, AudioSys.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, AudioSys.ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.1, AudioSys.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, AudioSys.ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(AudioSys.ctx.destination);
osc.start();
osc.stop(AudioSys.ctx.currentTime + 0.2);
}
};
/**
* GAME CLASSES
*/
class Particle {
constructor(x, y, color, speed, life) {
this.x = x;
this.y = y;
this.color = color;
this.vx = (Math.random() - 0.5) * speed;
this.vy = (Math.random() - 0.5) * speed;
this.life = life;
this.maxLife = life;
this.size = Math.random() * 3 + 1;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life--;
this.size *= 0.95;
}
draw(ctx) {
ctx.globalAlpha = this.life / this.maxLife;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
class Projectile {
constructor(x, y, target) {
this.x = x;
this.y = y;
this.target = target;
this.speed = 8;
this.active = true;
this.color = '#ff0000';
}
update() {
if (!this.target || this.target.hp <= 0) {
this.active = false;
return;
}
const dx = this.target.x - this.x;
const dy = this.target.y - this.y;
const dist = Math.hypot(dx, dy);
if (dist < this.speed) {
this.target.takeDamage(25);
this.active = false;
game.addParticles(this.x, this.y, '#ffaa00', 5);
} else {
this.x += (dx / dist) * this.speed;
this.y += (dy / dist) * this.speed;
}
}
draw(ctx) {
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - (this.x - this.target.x)*0.2, this.y - (this.y - this.target.y)*0.2); // Trail
ctx.stroke();
}
}
class Enemy {
constructor(x, y) {
this.x = x;
this.y = y;
this.hp = 100;
this.maxHp = 100;
this.speed = 0.5;
this.radius = 8;
this.targetX = 0; // Will be set to colony center
this.targetY = 0;
this.angle = 0;
}
update() {
// Move towards center of map (approximate colony center)
// In a real game, pathfinding would be better, but we move to center (0,0 relative to grid)
// Actually, let's move towards the nearest building or center
const dx = (game.canvas.width/2) - this.x; // Simplified target
const dy = (game.canvas.height/2) - this.y;
const dist = Math.hypot(dx, dy);
this.x += (dx/dist) * this.speed;
this.y += (dy/dist) * this.speed;
this.angle += 0.05;
// Collision with buildings
// Simplified: if close to center, damage colony
if (dist < 20) {
game.resources.energy -= 10;
game.resources.matter -= 10;
this.hp = 0; // Kamikaze
game.addLog("ALERT: Enemy breached defenses!", "red");
game.addParticles(this.x, this.y, '#ff0000', 10);
AudioSys.playError();
}
}
takeDamage(amount) {
this.hp -= amount;
if (this.hp <= 0) {
game.addParticles(this.x, this.y, '#aaffaa', 8);
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = '#ff0055';
ctx.shadowBlur = 10;
ctx.shadowColor = '#ff0055';
// Draw spikey shape
ctx.beginPath();
for(let i=0; i<5; i++) {
ctx.lineTo(Math.cos(i*1.25)*this.radius, Math.sin(i*1.25)*this.radius);
ctx.lineTo(Math.cos(i*1.25+0.6)*(this.radius/2), Math.sin(i*1.25+0.6)*(this.radius/2));
}
ctx.closePath();
ctx.fill();
// HP Bar
ctx.rotate(-this.angle); // Unrotate for bar
ctx.fillStyle = 'red';
ctx.fillRect(-10, -15, 20, 3);
ctx.fillStyle = '#0f0';
ctx.fillRect(-10, -15, 20 * (this.hp/this.maxHp), 3);
ctx.restore();
}
}
class Building {
constructor(type, gridX, gridY) {
this.type = type;
this.gridX = gridX;
this.gridY = gridY;
this.level = 1;
this.timer = 0;
// Stats based on type
switch(type) {
case 'solar':
this.production = {e: 2, m: 0};
this.color = CONSTANTS.COLORS.SOLAR;
break;
case 'mine':
this.production = {e: -1, m: 3};
this.color = CONSTANTS.COLORS.MINE;
break;
case 'habitat':
this.production = {e: -2, m: 0};
this.population = 10;
this.color = CONSTANTS.COLORS.HABITAT;
break;
case 'defense':
this.production = {e: -3, m: 0};
this.range = 200;
this.cooldown = 0;
this.color = CONSTANTS.COLORS.DEFENSE;
break;
}
}
update() {
// Production happens in main loop based on production stats
// Defense logic
if (this.type === 'defense') {
this.cooldown--;
if (this.cooldown <= 0) {
// Find target
const target = game.findEnemyInRange(this.x, this.y, this.range);
if (target) {
game.projectiles.push(new Projectile(this.x, this.y, target));
this.cooldown = 60; // 1 sec at 60fps
AudioSys.playShoot();
}
}
}
}
draw(ctx, screenX, screenY) {
this.x = screenX;
this.y = screenY;
const size = CONSTANTS.TILE_SIZE * 0.8;
ctx.save();
ctx.translate(screenX, screenY);
// Glow
ctx.shadowBlur = 15;
ctx.shadowColor = this.color;
// Base
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(-size/2, -size/2, size, size);
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.strokeRect(-size/2, -size/2, size, size);
// Icon/Detail
ctx.fillStyle = this.color;
if (this.type === 'solar') {
// Sun symbol
ctx.beginPath();
ctx.arc(0, 0, size/4, 0, Math.PI*2);
ctx.fill();
// Rays
for(let i=0; i<4; i++) {
ctx.rotate(Math.PI/2);
ctx.fillRect(-2, -size/3, 4, size/6);
}
} else if (this.type === 'mine') {
// Drill
ctx.beginPath();
ctx.moveTo(0, -size/3);
ctx.lineTo(size/3, size/3);
ctx.lineTo(-size/3, size/3);
ctx.fill();
} else if (this.type === 'habitat') {
// Dome
ctx.beginPath();
ctx.arc(0, size/4, size/3, Math.PI, 0);
ctx.fill();
ctx.fillRect(-size/3, size/4, size/1.5, size/4);
} else if (this.type === 'defense') {
// Turret
ctx.fillRect(-size/4, -size/4, size/2, size/2);
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(0, 0, size/6, 0, Math.PI*2);
ctx.fill();
// Range indicator (faint)
if (game.selectedTool === 'defense') {
ctx.beginPath();
ctx.arc(0, 0, this.range, 0, Math.PI*2);
ctx.strokeStyle = 'rgba(255,0,0,0.1)';
ctx.stroke();
}
}
// Level indicator
if (this.level > 1) {
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.fillText('+' + (this.level-1), -size/2 + 2, -size/2 + 10);
}
ctx.restore();
}
}
/**
* MAIN GAME ENGINE
*/
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.bgCanvas = document.getElementById('bgCanvas');
this.bgCtx = this.bgCanvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.grid = []; // 2D array
this.buildings = [];
this.enemies = [];
this.projectiles = [];
this.particles = [];
this.resources = {
energy: 100,
matter: 200,
population: 0,
maxPop: 0
};
this.selectedTool = null;
this.cycle = 1;
this.gameOver = false;
// Mouse interaction
this.mouse = { x: 0, y: 0, gridX: 0, gridY: 0 };
this.canvas.addEventListener('mousemove', e => this.handleMouseMove(e));
this.canvas.addEventListener('click', e => this.handleClick(e));
this.canvas.addEventListener('mouseleave', () => {
document.getElementById('tooltip').style.opacity = 0;
});
// Start loops
this.lastTime = 0;
this.spawnTimer = 0;
this.productionTimer = 0;
this.initGrid();
this.generateStars();
requestAnimationFrame(t => this.loop(t));
// Initial Log
this.addLog("Colony vessel landed.");
this.addLog("Mission: Survive.");
}
resize() {
const container = document.getElementById('canvas-container');
this.canvas.width = container.clientWidth;
this.canvas.height = container.clientHeight;
this.bgCanvas.width = window.innerWidth;
this.bgCanvas.height = window.innerHeight;
}
initGrid() {
// Initialize empty grid
// Center the grid on screen
this.offsetX = (this.canvas.width - (CONSTANTS.GRID_W * CONSTANTS.TILE_SIZE)) / 2;
this.offsetY = (this.canvas.height - (CONSTANTS.GRID_H * CONSTANTS.TILE_SIZE)) / 2;
}
generateStars() {
this.bgCtx.fillStyle = '#050510';
this.bgCtx.fillRect(0, 0, this.bgCanvas.width, this.bgCanvas.height);
for(let i=0; i<200; i++) {
const x = Math.random() * this.bgCanvas.width;
const y = Math.random() * this.bgCanvas.height;
const size = Math.random() * 2;
const opacity = Math.random();
this.bgCtx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
this.bgCtx.beginPath();
this.bgCtx.arc(x, y, size, 0, Math.PI*2);
this.bgCtx.fill();
}
}
selectTool(tool) {
AudioSys.init(); // Ensure audio context is started on user interaction
AudioSys.playClick();
this.selectedTool = tool;
// UI Update
document.querySelectorAll('.glass-card').forEach(el => {
el.classList.remove('border-cyan-400', 'bg-cyan-900/20');
});
if (tool) {
const btn = document.getElementById('btn-' + tool);
if (btn) btn.classList.add('border-cyan-400', 'bg-cyan-900/20');
}
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
// Calculate Grid Coords
this.mouse.gridX = Math.floor((this.mouse.x - this.offsetX) / CONSTANTS.TILE_SIZE);
this.mouse.gridY = Math.floor((this.mouse.y - this.offsetY) / CONSTANTS.TILE_SIZE);
document.getElementById('mouse-coords').innerText = `${this.mouse.gridX}, ${this.mouse.gridY}`;
// Tooltip logic
const b = this.getBuildingAt(this.mouse.gridX, this.mouse.gridY);
const tt = document.getElementById('tooltip');
if (b) {
tt.style.opacity = 1;
tt.style.left = (e.pageX + 15) + 'px';
tt.style.top = (e.pageY + 15) + 'px';
document.getElementById('tt-title').innerText = b.type.toUpperCase() + (b.level > 1 ? ' MK.'+b.level : '');
let desc = '';
if(b.type === 'solar') desc = `Generates +${b.production.e} Energy/cycle.`;
if(b.type === 'mine') desc = `Generates +${b.production.m} Matter/cycle. Consumes ${Math.abs(b.production.e)} Energy.`;
if(b.type === 'habitat') desc = `Houses ${b.population} colonists. Consumes Energy.`;
if(b.type === 'defense') desc = `Automated defense turret. High Energy upkeep.`;
document.getElementById('tt-desc').innerText = desc;
} else {
tt.style.opacity = 0;
}
}
handleClick(e) {
if (this.gameOver) return;
AudioSys.init();
const gx = this.mouse.gridX;
const gy = this.mouse.gridY;
// Bounds check
if (gx < 0 || gx >= CONSTANTS.GRID_W || gy < 0 || gy >= CONSTANTS.GRID_H) return;
const existing = this.getBuildingAt(gx, gy);
if (this.selectedTool === 'demolish') {
if (existing) {
this.buildings = this.buildings.filter(b => b !== existing);
this.addLog(`Structure demolished at ${gx},${gy}`);
this.addParticles(this.offsetX + gx*CONSTANTS.TILE_SIZE + CONSTANTS.TILE_SIZE/2, this.offsetY + gy*CONSTANTS.TILE_SIZE + CONSTANTS.TILE_SIZE/2, '#fff', 10);
AudioSys.playClick();
}
return;
}
if (!existing && this.selectedTool) {
this.attemptBuild(this.selectedTool, gx, gy);
} else if (existing && this.selectedTool === existing.type) {
// Upgrade logic could go here
// For now, just log
// this.addLog("Upgrade logic not implemented in demo.");
}
}
attemptBuild(type, gx, gy) {
let costE = 0, costM = 0;
if (type === 'solar') { costM = 20; }
else if (type === 'mine') { costM = 30; costE = 10; }
else if (type === 'habitat') { costM = 100; costE = 50; }
else if (type === 'defense') { costM = 80; costE = 40; }
if (this.resources.energy >= costE && this.resources.matter >= costM) {
this.resources.energy -= costE;
this.resources.matter -= costM;
const b = new Building(type, gx, gy);
this.buildings.push(b);
// Visuals
const sx = this.offsetX + gx * CONSTANTS.TILE_SIZE + CONSTANTS.TILE_SIZE/2;
const sy = this.offsetY + gy * CONSTANTS.TILE_SIZE + CONSTANTS.TILE_SIZE/2;
this.addParticles(sx, sy, b.color, 15);
AudioSys.playBuild();
this.addLog(`Constructed ${type.toUpperCase()}`);
} else {
AudioSys.playError();
this.addLog("Insufficient resources!", "red");
}
}
getBuildingAt(gx, gy) {
return this.buildings.find(b => b.gridX === gx && b.gridY === gy);
}
findEnemyInRange(x, y, range) {
// Simple distance check
let closest = null;
let minDst = range;
for (let e of this.enemies) {
const d = Math.hypot(e.x - x, e.y - y);
if (d < minDst) {
minDst = d;
closest = e;
}
}
return closest;
}
addParticles(x, y, color, count) {
for(let i=0; i<count; i++) {
this.particles.push(new Particle(x, y, color, 3, 30));
}
}
addLog(msg, color="gray") {
const log = document.getElementById('game-log');
const div = document.createElement('div');
div.className = `text-${color}-400`;
div.innerText = `> ${msg}`;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
update(dt) {
if (this.gameOver) return;
// Production Loop (Every 2 seconds approx)
this.productionTimer += dt;
if (this.productionTimer > 2000) {
this.productionTimer = 0;
this.cycle++;
document.getElementById('game-cycle').innerText = this.cycle;
// Calculate Net Production
let netE = 0;
let netM = 0;
let maxPop = 0;
this.buildings.forEach(b => {
netE += b.production.e;
netM += b.production.m;
if (b.type === 'habitat') maxPop += b.population;
});
// Population Growth
if (this.resources.energy > 0 && this.resources.matter > 0 && this.resources.population < maxPop) {
if (Math.random() > 0.5) this.resources.population++;
}
// Consume resources for population
if (this.resources.population > 0) {
netE -= Math.floor(this.resources.population / 5); // Pop consumes energy
}
this.resources.energy += netE;
this.resources.matter += netM;
this.resources.maxPop = maxPop;
// Cap resources
if (this.resources.matter > 999) this.resources.matter = 999;
if (this.resources.energy > 999) this.resources.energy = 999;
// Check Loss
if (this.resources.energy < 0) {
this.resources.energy = 0;
// Penalty?
if (this.resources.population > 0 && Math.random() > 0.7) {
this.resources.population--;
this.addLog("Colonists lost due to energy shortage!", "red");
}
}
// Win/Loss Check
if (this.resources.population >= 100) {
this.triggerEnd("VICTORY", "Colony population reached sustainable levels. The future is secured.");
} else if (this.resources.population <= 0 && this.cycle > 10 && this.buildings.length > 0) {
// If you have buildings but no people for a while, you lose?
// Let's make it stricter: if pop is 0 and you have habitats, you are failing.
// If pop is 0 and no habitats, just starting.
const hasHabs = this.buildings.some(b => b.type === 'habitat');
if (hasHabs) {
this.triggerEnd("COLONY FAILED", "All colonists have perished.");
}
}
}
// Spawning Logic
this.spawnTimer += dt;
// Spawn rate increases with cycle
const spawnRate = Math.max(500, 5000 - (this.cycle * 200));
if (this.spawnTimer > spawnRate) {
this.spawnTimer = 0;
this.spawnEnemy();
}
// Updates
this.buildings.forEach(b => b.update());
this.enemies.forEach(e => e.update());
this.enemies = this.enemies.filter(e => e.hp > 0);
this.projectiles.forEach(p => p.update());
this.projectiles = this.projectiles.filter(p => p.active);
this.particles.forEach(p => p.update());
this.particles = this.particles.filter(p => p.life > 0);
// UI Updates
this.updateUI();
}
spawnEnemy() {
// Spawn at edge
const edge = randInt(0, 4); // 0:top, 1:right, 2:bottom, 3:left
let x, y;
const margin = 50;
if (edge === 0) { x = rand(0, this.canvas.width); y = -margin; }
else if (edge === 1) { x = this.canvas.width + margin; y = rand(0, this.canvas.height); }
else if (edge === 2) { x = rand(0, this.canvas.width); y = this.canvas.height + margin; }
else { x = -margin; y = rand(0, this.canvas.height); }
this.enemies.push(new Enemy(x, y));
// this.addLog("Incoming signature detected...");
}
updateUI() {
document.getElementById('val-energy').innerText = Math.floor(this.resources.energy);
document.getElementById('bar-energy').style.width = Math.min(100, (this.resources.energy / 200) * 100) + '%';
document.getElementById('val-matter').innerText = Math.floor(this.resources.matter);
document.getElementById('bar-matter').style.width = Math.min(100, (this.resources.matter / 500) * 100) + '%';
document.getElementById('val-pop').innerText = this.resources.population + '/' + this.resources.maxPop;
document.getElementById('bar-pop').style.width = this.resources.maxPop > 0 ? (this.resources.population / this.resources.maxPop) * 100 + '%' : '0%';
}
triggerEnd(title, msg) {
this.gameOver = true;
document.getElementById('overlay-title').innerText = title;
document.getElementById('overlay-msg').innerText = msg;
document.getElementById('overlay-screen').classList.remove('hidden');
AudioSys.playTone(200, 'sawtooth', 1, 0.2);
}
draw() {
// Clear
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw Grid
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
this.ctx.lineWidth = 1;
for (let x = 0; x <= CONSTANTS.GRID_W