Upload files to "js"
This commit is contained in:
188
js/audio.js
Normal file
188
js/audio.js
Normal file
@@ -0,0 +1,188 @@
|
||||
// Audio System
|
||||
const Audio = {
|
||||
ctx: null,
|
||||
enabled: true,
|
||||
volume: 0.3,
|
||||
|
||||
// Initialize audio context
|
||||
init() {
|
||||
// Create on first user interaction
|
||||
document.addEventListener('click', () => this.ensureContext(), { once: true });
|
||||
document.addEventListener('keydown', () => this.ensureContext(), { once: true });
|
||||
},
|
||||
|
||||
ensureContext() {
|
||||
if (!this.ctx) {
|
||||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
},
|
||||
|
||||
// Play a tone
|
||||
playTone(frequency, duration, type = 'square', volume = null) {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
|
||||
try {
|
||||
const oscillator = this.ctx.createOscillator();
|
||||
const gainNode = this.ctx.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(this.ctx.destination);
|
||||
|
||||
oscillator.frequency.value = frequency;
|
||||
oscillator.type = type;
|
||||
|
||||
const vol = (volume || this.volume) * this.volume;
|
||||
gainNode.gain.setValueAtTime(vol, this.ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
|
||||
|
||||
oscillator.start(this.ctx.currentTime);
|
||||
oscillator.stop(this.ctx.currentTime + duration);
|
||||
} catch (e) {
|
||||
// Ignore audio errors
|
||||
}
|
||||
},
|
||||
|
||||
// Play noise burst (for explosions, etc)
|
||||
playNoise(duration, volume = null) {
|
||||
if (!this.enabled || !this.ctx) return;
|
||||
|
||||
try {
|
||||
const bufferSize = this.ctx.sampleRate * duration;
|
||||
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
const noise = this.ctx.createBufferSource();
|
||||
const gainNode = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
noise.buffer = buffer;
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.value = 1000;
|
||||
|
||||
noise.connect(filter);
|
||||
filter.connect(gainNode);
|
||||
gainNode.connect(this.ctx.destination);
|
||||
|
||||
const vol = (volume || this.volume) * this.volume;
|
||||
gainNode.gain.setValueAtTime(vol, this.ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
|
||||
|
||||
noise.start(this.ctx.currentTime);
|
||||
} catch (e) {
|
||||
// Ignore audio errors
|
||||
}
|
||||
},
|
||||
|
||||
// Sound effects
|
||||
playPlace() {
|
||||
this.playTone(200, 0.1, 'square', 0.2);
|
||||
setTimeout(() => this.playTone(300, 0.08, 'square', 0.15), 50);
|
||||
},
|
||||
|
||||
playDelete() {
|
||||
this.playTone(150, 0.15, 'sawtooth', 0.2);
|
||||
setTimeout(() => this.playTone(100, 0.1, 'sawtooth', 0.15), 80);
|
||||
},
|
||||
|
||||
playMine() {
|
||||
this.playTone(100 + Math.random() * 50, 0.08, 'triangle', 0.15);
|
||||
},
|
||||
|
||||
playMineComplete() {
|
||||
this.playTone(400, 0.1, 'sine', 0.2);
|
||||
setTimeout(() => this.playTone(500, 0.08, 'sine', 0.15), 60);
|
||||
},
|
||||
|
||||
playShoot(towerType) {
|
||||
switch(towerType) {
|
||||
case 'gun_turret':
|
||||
this.playNoise(0.05, 0.3);
|
||||
this.playTone(150, 0.05, 'square', 0.2);
|
||||
break;
|
||||
case 'flame_turret':
|
||||
this.playNoise(0.15, 0.25);
|
||||
this.playTone(80, 0.1, 'sawtooth', 0.15);
|
||||
break;
|
||||
case 'laser_turret':
|
||||
this.playTone(800, 0.15, 'sine', 0.25);
|
||||
this.playTone(1200, 0.1, 'sine', 0.15);
|
||||
break;
|
||||
case 'tesla_turret':
|
||||
this.playTone(200, 0.05, 'sawtooth', 0.2);
|
||||
this.playTone(400, 0.1, 'sawtooth', 0.15);
|
||||
this.playTone(300, 0.08, 'sawtooth', 0.1);
|
||||
break;
|
||||
case 'cannon_turret':
|
||||
this.playNoise(0.2, 0.4);
|
||||
this.playTone(60, 0.2, 'triangle', 0.3);
|
||||
break;
|
||||
default:
|
||||
this.playTone(200, 0.05, 'square', 0.2);
|
||||
}
|
||||
},
|
||||
|
||||
playExplosion() {
|
||||
this.playNoise(0.3, 0.4);
|
||||
this.playTone(50, 0.3, 'triangle', 0.3);
|
||||
setTimeout(() => this.playTone(40, 0.2, 'triangle', 0.2), 100);
|
||||
},
|
||||
|
||||
playEnemyHit() {
|
||||
this.playTone(150, 0.05, 'square', 0.1);
|
||||
},
|
||||
|
||||
playEnemyDeath() {
|
||||
this.playTone(200, 0.1, 'sawtooth', 0.2);
|
||||
setTimeout(() => this.playTone(100, 0.15, 'sawtooth', 0.15), 50);
|
||||
},
|
||||
|
||||
playWaveStart() {
|
||||
const notes = [300, 400, 300, 500];
|
||||
notes.forEach((freq, i) => {
|
||||
setTimeout(() => this.playTone(freq, 0.2, 'square', 0.25), i * 150);
|
||||
});
|
||||
},
|
||||
|
||||
playWaveComplete() {
|
||||
const notes = [400, 500, 600, 800];
|
||||
notes.forEach((freq, i) => {
|
||||
setTimeout(() => this.playTone(freq, 0.15, 'sine', 0.25), i * 100);
|
||||
});
|
||||
},
|
||||
|
||||
playBuildingDamage() {
|
||||
this.playTone(100, 0.1, 'sawtooth', 0.2);
|
||||
},
|
||||
|
||||
playBuildingDestroyed() {
|
||||
this.playNoise(0.3, 0.35);
|
||||
this.playTone(80, 0.3, 'sawtooth', 0.25);
|
||||
},
|
||||
|
||||
playUIClick() {
|
||||
this.playTone(400, 0.05, 'sine', 0.1);
|
||||
},
|
||||
|
||||
playError() {
|
||||
this.playTone(150, 0.15, 'square', 0.2);
|
||||
setTimeout(() => this.playTone(100, 0.15, 'square', 0.15), 100);
|
||||
},
|
||||
|
||||
playDevMode() {
|
||||
this.playTone(600, 0.1, 'sine', 0.2);
|
||||
setTimeout(() => this.playTone(800, 0.1, 'sine', 0.2), 100);
|
||||
},
|
||||
|
||||
// Toggle sound
|
||||
toggle() {
|
||||
this.enabled = !this.enabled;
|
||||
if (this.enabled) {
|
||||
this.playUIClick();
|
||||
}
|
||||
return this.enabled;
|
||||
}
|
||||
};
|
||||
163
js/buildings.js
Normal file
163
js/buildings.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// Building Management
|
||||
const Buildings = {
|
||||
// All placed buildings
|
||||
list: [],
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
this.list = [];
|
||||
},
|
||||
|
||||
// Get building at tile position
|
||||
getAt(x, y) {
|
||||
return this.list.find(b => {
|
||||
const size = Utils.getBuildingSize(b.type);
|
||||
return x >= b.x && x < b.x + size.w && y >= b.y && y < b.y + size.h;
|
||||
});
|
||||
},
|
||||
|
||||
// Check if building can be placed
|
||||
canPlace(type, x, y) {
|
||||
const size = Utils.getBuildingSize(type);
|
||||
|
||||
// Check bounds and collisions
|
||||
for (let dy = 0; dy < size.h; dy++) {
|
||||
for (let dx = 0; dx < size.w; dx++) {
|
||||
const tx = x + dx;
|
||||
const ty = y + dy;
|
||||
if (!Utils.inBounds(tx, ty)) return false;
|
||||
if (this.getAt(tx, ty)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Miners must be on resources
|
||||
if (type === 'miner') {
|
||||
let hasResource = false;
|
||||
for (let dy = 0; dy < size.h; dy++) {
|
||||
for (let dx = 0; dx < size.w; dx++) {
|
||||
if (Terrain.hasResource(x + dx, y + dy)) {
|
||||
hasResource = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasResource) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// Place a building
|
||||
place(type, x, y, rotation) {
|
||||
const cost = CONFIG.COSTS[type];
|
||||
if (!Resources.canAfford(cost)) {
|
||||
Audio.playError();
|
||||
return false;
|
||||
}
|
||||
if (!this.canPlace(type, x, y)) {
|
||||
Audio.playError();
|
||||
return false;
|
||||
}
|
||||
|
||||
const building = {
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
rotation: rotation || 0,
|
||||
inventory: {},
|
||||
output: {},
|
||||
progress: 0,
|
||||
health: 100,
|
||||
maxHealth: 100,
|
||||
recipe: type === 'assembler' ? 'gear' : null
|
||||
};
|
||||
|
||||
this.list.push(building);
|
||||
Resources.payCost(cost);
|
||||
Audio.playPlace();
|
||||
return true;
|
||||
},
|
||||
|
||||
// Remove a building
|
||||
remove(x, y) {
|
||||
const building = this.getAt(x, y);
|
||||
if (!building) return false;
|
||||
|
||||
const idx = this.list.indexOf(building);
|
||||
if (idx !== -1) {
|
||||
this.list.splice(idx, 1);
|
||||
Resources.refundHalf(CONFIG.COSTS[building.type]);
|
||||
Audio.playDelete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// Get all buildings by type
|
||||
getByType(type) {
|
||||
return this.list.filter(b => b.type === type);
|
||||
},
|
||||
|
||||
// Add item to building inventory
|
||||
addToInventory(building, item, amount = 1) {
|
||||
building.inventory[item] = (building.inventory[item] || 0) + amount;
|
||||
},
|
||||
|
||||
// Remove item from building inventory
|
||||
removeFromInventory(building, item, amount = 1) {
|
||||
if ((building.inventory[item] || 0) < amount) return false;
|
||||
building.inventory[item] -= amount;
|
||||
if (building.inventory[item] <= 0) delete building.inventory[item];
|
||||
return true;
|
||||
},
|
||||
|
||||
// Add item to building output
|
||||
addToOutput(building, item, amount = 1) {
|
||||
building.output[item] = (building.output[item] || 0) + amount;
|
||||
},
|
||||
|
||||
// Remove item from building output
|
||||
removeFromOutput(building, item, amount = 1) {
|
||||
if ((building.output[item] || 0) < amount) return false;
|
||||
building.output[item] -= amount;
|
||||
if (building.output[item] <= 0) delete building.output[item];
|
||||
return true;
|
||||
},
|
||||
|
||||
// Get inventory count
|
||||
getInventoryCount(building, item) {
|
||||
return building.inventory[item] || 0;
|
||||
},
|
||||
|
||||
// Get output count
|
||||
getOutputCount(building, item) {
|
||||
return building.output[item] || 0;
|
||||
},
|
||||
|
||||
// Get total output items
|
||||
getTotalOutput(building) {
|
||||
return Object.values(building.output || {}).reduce((a, b) => a + b, 0);
|
||||
},
|
||||
|
||||
// Get total inventory items
|
||||
getTotalInventory(building) {
|
||||
return Object.values(building.inventory || {}).reduce((a, b) => a + b, 0);
|
||||
},
|
||||
|
||||
// Get all buildings data for saving
|
||||
getData() {
|
||||
return this.list.map(b => ({
|
||||
...b,
|
||||
inventory: { ...b.inventory },
|
||||
output: { ...b.output }
|
||||
}));
|
||||
},
|
||||
|
||||
// Load buildings data
|
||||
setData(data) {
|
||||
this.list = data.map(b => ({
|
||||
...b,
|
||||
inventory: { ...b.inventory },
|
||||
output: { ...b.output }
|
||||
}));
|
||||
}
|
||||
};
|
||||
117
js/config.js
Normal file
117
js/config.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// Game Configuration Constants
|
||||
const CONFIG = {
|
||||
// Dev mode - toggle with backtick key (`)
|
||||
DEV_MODE: false,
|
||||
|
||||
TILE_SIZE: 40,
|
||||
MAP_WIDTH: 100,
|
||||
MAP_HEIGHT: 100,
|
||||
|
||||
// Directions: 0=right, 1=down, 2=left, 3=up
|
||||
DIR: [
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
{ x: -1, y: 0 },
|
||||
{ x: 0, y: -1 }
|
||||
],
|
||||
|
||||
// Building sizes
|
||||
BUILDING_SIZES: {
|
||||
'miner': { w: 2, h: 2 },
|
||||
'furnace': { w: 2, h: 2 },
|
||||
'assembler': { w: 3, h: 3 },
|
||||
'belt': { w: 1, h: 1 },
|
||||
'inserter': { w: 1, h: 1 },
|
||||
'chest': { w: 1, h: 1 }
|
||||
},
|
||||
|
||||
// Building costs
|
||||
COSTS: {
|
||||
'miner': { 'iron-plate': 10 },
|
||||
'belt': { 'iron-plate': 1 },
|
||||
'inserter': { 'iron-plate': 2, 'gear': 1 },
|
||||
'furnace': { 'iron': 10 },
|
||||
'assembler': { 'iron-plate': 5, 'gear': 3 },
|
||||
'chest': { 'iron-plate': 5 }
|
||||
},
|
||||
|
||||
// Production speeds (items per second at 1x speed)
|
||||
SPEEDS: {
|
||||
'miner': 1.0,
|
||||
'furnace': 0.5,
|
||||
'assembler': 0.3,
|
||||
'inserter': 2.0,
|
||||
'belt': 3.0
|
||||
},
|
||||
|
||||
// Manual mining
|
||||
MANUAL_MINE_RATE: 0.5, // seconds per ore
|
||||
MANUAL_MINE_AMOUNT: 1,
|
||||
|
||||
// Recipes
|
||||
RECIPES: {
|
||||
'gear': {
|
||||
inputs: { 'iron-plate': 2 },
|
||||
output: 'gear',
|
||||
outputCount: 1
|
||||
},
|
||||
'circuit': {
|
||||
inputs: { 'iron-plate': 1, 'copper-plate': 3 },
|
||||
output: 'circuit',
|
||||
outputCount: 1
|
||||
}
|
||||
},
|
||||
|
||||
// Smelting recipes
|
||||
SMELTING: {
|
||||
'iron': { output: 'iron-plate', fuel: 'coal' },
|
||||
'copper': { output: 'copper-plate', fuel: 'coal' }
|
||||
},
|
||||
|
||||
// Resource types and their colors
|
||||
RESOURCE_COLORS: {
|
||||
'iron': { light: '#9aaabb', dark: '#6a7a8a' },
|
||||
'copper': { light: '#e8b878', dark: '#b87830' },
|
||||
'coal': { light: '#3a3a3a', dark: '#1a1a1a' }
|
||||
},
|
||||
|
||||
// Building styles for rendering
|
||||
BUILDING_STYLES: {
|
||||
'miner': { light: '#ffbb44', dark: '#cc7700', accent: '#ffdd88' },
|
||||
'belt': { light: '#777', dark: '#444', accent: '#999' },
|
||||
'inserter': { light: '#ffdd55', dark: '#bb9900', accent: '#ffee88' },
|
||||
'furnace': { light: '#ee5533', dark: '#992211', accent: '#ff8866' },
|
||||
'assembler': { light: '#55aaee', dark: '#226699', accent: '#88ccff' },
|
||||
'chest': { light: '#aa7744', dark: '#664422', accent: '#cc9966' }
|
||||
},
|
||||
|
||||
// Starting resources
|
||||
STARTING_RESOURCES: {
|
||||
'iron': 0,
|
||||
'copper': 0,
|
||||
'coal': 0,
|
||||
'iron-plate': 20,
|
||||
'copper-plate': 10,
|
||||
'gear': 5,
|
||||
'circuit': 0,
|
||||
'xp': 0
|
||||
},
|
||||
|
||||
// Resource display order
|
||||
RESOURCE_ORDER: ['iron', 'copper', 'coal', 'iron-plate', 'copper-plate', 'gear', 'circuit', 'xp'],
|
||||
|
||||
// Resource display names
|
||||
RESOURCE_NAMES: {
|
||||
'iron': 'Iron Ore',
|
||||
'copper': 'Copper Ore',
|
||||
'coal': 'Coal',
|
||||
'iron-plate': 'Iron Plates',
|
||||
'copper-plate': 'Copper Plates',
|
||||
'gear': 'Gears',
|
||||
'circuit': 'Circuits',
|
||||
'xp': 'XP'
|
||||
},
|
||||
|
||||
// Tower types for toolbar
|
||||
TOWER_ORDER: ['gun_turret', 'flame_turret', 'laser_turret', 'tesla_turret', 'cannon_turret']
|
||||
};
|
||||
351
js/enemies.js
Normal file
351
js/enemies.js
Normal file
@@ -0,0 +1,351 @@
|
||||
// Enemy System
|
||||
const Enemies = {
|
||||
list: [],
|
||||
wave: 0,
|
||||
waveTimer: 0,
|
||||
timeBetweenWaves: 120, // seconds between waves
|
||||
waveActive: false,
|
||||
enemiesRemaining: 0,
|
||||
spawnQueue: [],
|
||||
spawnTimer: 0,
|
||||
|
||||
// Enemy types
|
||||
TYPES: {
|
||||
'biter': {
|
||||
health: 50,
|
||||
maxHealth: 50,
|
||||
speed: 30, // pixels per second
|
||||
damage: 10,
|
||||
attackSpeed: 1, // attacks per second
|
||||
range: 40, // attack range in pixels
|
||||
color: '#44aa44',
|
||||
colorDark: '#227722',
|
||||
size: 0.6, // tile fraction
|
||||
xp: 10
|
||||
},
|
||||
'spitter': {
|
||||
health: 30,
|
||||
maxHealth: 30,
|
||||
speed: 25,
|
||||
damage: 15,
|
||||
attackSpeed: 0.5,
|
||||
range: 120,
|
||||
color: '#aa44aa',
|
||||
colorDark: '#772277',
|
||||
size: 0.5,
|
||||
xp: 15
|
||||
},
|
||||
'big_biter': {
|
||||
health: 200,
|
||||
maxHealth: 200,
|
||||
speed: 20,
|
||||
damage: 30,
|
||||
attackSpeed: 0.8,
|
||||
range: 50,
|
||||
color: '#228822',
|
||||
colorDark: '#115511',
|
||||
size: 0.9,
|
||||
xp: 50
|
||||
},
|
||||
'tank_biter': {
|
||||
health: 500,
|
||||
maxHealth: 500,
|
||||
speed: 15,
|
||||
damage: 50,
|
||||
attackSpeed: 0.5,
|
||||
range: 60,
|
||||
color: '#116611',
|
||||
colorDark: '#0a440a',
|
||||
size: 1.2,
|
||||
xp: 100
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
this.list = [];
|
||||
this.wave = 0;
|
||||
this.waveTimer = this.timeBetweenWaves * 0.5; // First wave comes sooner
|
||||
this.waveActive = false;
|
||||
this.enemiesRemaining = 0;
|
||||
this.spawnQueue = [];
|
||||
this.spawnTimer = 0;
|
||||
},
|
||||
|
||||
// Update enemies
|
||||
update(dt) {
|
||||
// Wave timer
|
||||
if (!this.waveActive) {
|
||||
this.waveTimer -= dt;
|
||||
if (this.waveTimer <= 0) {
|
||||
this.startWave();
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn queued enemies
|
||||
if (this.spawnQueue.length > 0) {
|
||||
this.spawnTimer -= dt;
|
||||
if (this.spawnTimer <= 0) {
|
||||
this.spawnTimer = 0.5; // Spawn every 0.5 seconds
|
||||
const spawn = this.spawnQueue.shift();
|
||||
this.spawnEnemy(spawn.type, spawn.x, spawn.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Update each enemy
|
||||
for (let i = this.list.length - 1; i >= 0; i--) {
|
||||
const enemy = this.list[i];
|
||||
this.updateEnemy(enemy, dt);
|
||||
|
||||
// Remove dead enemies
|
||||
if (enemy.health <= 0) {
|
||||
this.list.splice(i, 1);
|
||||
this.enemiesRemaining--;
|
||||
Resources.add('xp', enemy.xp || 10);
|
||||
Audio.playEnemyDeath();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if wave is complete
|
||||
if (this.waveActive && this.enemiesRemaining <= 0 && this.spawnQueue.length === 0) {
|
||||
this.waveActive = false;
|
||||
this.waveTimer = this.timeBetweenWaves;
|
||||
Audio.playWaveComplete();
|
||||
}
|
||||
},
|
||||
|
||||
// Update single enemy
|
||||
updateEnemy(enemy, dt) {
|
||||
const type = this.TYPES[enemy.type];
|
||||
|
||||
// Find target (nearest building or base center)
|
||||
const target = this.findTarget(enemy);
|
||||
|
||||
if (target) {
|
||||
const dx = target.x - enemy.x;
|
||||
const dy = target.y - enemy.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Move toward target if not in range
|
||||
if (dist > type.range) {
|
||||
const moveX = (dx / dist) * type.speed * dt;
|
||||
const moveY = (dy / dist) * type.speed * dt;
|
||||
enemy.x += moveX;
|
||||
enemy.y += moveY;
|
||||
enemy.angle = Math.atan2(dy, dx);
|
||||
} else {
|
||||
// Attack
|
||||
enemy.attackCooldown = (enemy.attackCooldown || 0) - dt;
|
||||
if (enemy.attackCooldown <= 0) {
|
||||
enemy.attackCooldown = 1 / type.attackSpeed;
|
||||
this.attack(enemy, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation
|
||||
enemy.animTime = (enemy.animTime || 0) + dt * 5;
|
||||
},
|
||||
|
||||
// Find nearest target for enemy
|
||||
findTarget(enemy) {
|
||||
let nearest = null;
|
||||
let nearestDist = Infinity;
|
||||
|
||||
// Check buildings
|
||||
Buildings.list.forEach(b => {
|
||||
const size = Utils.getBuildingSize(b.type);
|
||||
const bx = (b.x + size.w / 2) * CONFIG.TILE_SIZE;
|
||||
const by = (b.y + size.h / 2) * CONFIG.TILE_SIZE;
|
||||
const dist = Math.sqrt((bx - enemy.x) ** 2 + (by - enemy.y) ** 2);
|
||||
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearest = { x: bx, y: by, building: b };
|
||||
}
|
||||
});
|
||||
|
||||
// Check towers
|
||||
Towers.list.forEach(t => {
|
||||
const tx = (t.x + 0.5) * CONFIG.TILE_SIZE;
|
||||
const ty = (t.y + 0.5) * CONFIG.TILE_SIZE;
|
||||
const dist = Math.sqrt((tx - enemy.x) ** 2 + (ty - enemy.y) ** 2);
|
||||
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearest = { x: tx, y: ty, tower: t };
|
||||
}
|
||||
});
|
||||
|
||||
// If no targets, go to map center
|
||||
if (!nearest) {
|
||||
nearest = {
|
||||
x: CONFIG.MAP_WIDTH * CONFIG.TILE_SIZE / 2,
|
||||
y: CONFIG.MAP_HEIGHT * CONFIG.TILE_SIZE / 2
|
||||
};
|
||||
}
|
||||
|
||||
return nearest;
|
||||
},
|
||||
|
||||
// Enemy attacks target
|
||||
attack(enemy, target) {
|
||||
const type = this.TYPES[enemy.type];
|
||||
|
||||
if (target.building) {
|
||||
target.building.health = (target.building.health || 100) - type.damage;
|
||||
Audio.playBuildingDamage();
|
||||
if (target.building.health <= 0) {
|
||||
Buildings.remove(target.building.x, target.building.y);
|
||||
Audio.playBuildingDestroyed();
|
||||
}
|
||||
} else if (target.tower) {
|
||||
target.tower.health -= type.damage;
|
||||
Audio.playBuildingDamage();
|
||||
if (target.tower.health <= 0) {
|
||||
Towers.remove(target.tower.x, target.tower.y);
|
||||
Audio.playBuildingDestroyed();
|
||||
}
|
||||
}
|
||||
|
||||
// Visual feedback
|
||||
enemy.attacking = true;
|
||||
setTimeout(() => enemy.attacking = false, 100);
|
||||
},
|
||||
|
||||
// Start a new wave
|
||||
startWave() {
|
||||
this.wave++;
|
||||
this.waveActive = true;
|
||||
Audio.playWaveStart();
|
||||
|
||||
// Calculate wave composition
|
||||
const baseCount = 5 + this.wave * 3;
|
||||
const spawnPoints = this.getSpawnPoints();
|
||||
|
||||
// Distribute enemies across spawn points
|
||||
spawnPoints.forEach(point => {
|
||||
let count = Math.ceil(baseCount / spawnPoints.length);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let type = 'biter';
|
||||
|
||||
// Higher waves get tougher enemies
|
||||
if (this.wave >= 3 && Math.random() < 0.2) type = 'spitter';
|
||||
if (this.wave >= 5 && Math.random() < 0.15) type = 'big_biter';
|
||||
if (this.wave >= 8 && Math.random() < 0.1) type = 'tank_biter';
|
||||
|
||||
// Add some randomness to spawn position
|
||||
const offsetX = (Math.random() - 0.5) * 200;
|
||||
const offsetY = (Math.random() - 0.5) * 200;
|
||||
|
||||
this.spawnQueue.push({
|
||||
type,
|
||||
x: point.x + offsetX,
|
||||
y: point.y + offsetY
|
||||
});
|
||||
this.enemiesRemaining++;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Get spawn points at map edges
|
||||
getSpawnPoints() {
|
||||
const points = [];
|
||||
const margin = 2 * CONFIG.TILE_SIZE;
|
||||
const mapW = CONFIG.MAP_WIDTH * CONFIG.TILE_SIZE;
|
||||
const mapH = CONFIG.MAP_HEIGHT * CONFIG.TILE_SIZE;
|
||||
|
||||
// Pick 1-4 spawn points based on wave number
|
||||
const numPoints = Math.min(4, 1 + Math.floor(this.wave / 3));
|
||||
|
||||
const edges = [
|
||||
{ x: margin, y: mapH / 2 }, // Left
|
||||
{ x: mapW - margin, y: mapH / 2 }, // Right
|
||||
{ x: mapW / 2, y: margin }, // Top
|
||||
{ x: mapW / 2, y: mapH - margin } // Bottom
|
||||
];
|
||||
|
||||
// Shuffle and pick
|
||||
for (let i = edges.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[edges[i], edges[j]] = [edges[j], edges[i]];
|
||||
}
|
||||
|
||||
return edges.slice(0, numPoints);
|
||||
},
|
||||
|
||||
// Spawn a single enemy
|
||||
spawnEnemy(type, x, y) {
|
||||
const enemyType = this.TYPES[type];
|
||||
this.list.push({
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
health: enemyType.maxHealth,
|
||||
maxHealth: enemyType.maxHealth,
|
||||
angle: 0,
|
||||
animTime: 0,
|
||||
attackCooldown: 0,
|
||||
xp: enemyType.xp
|
||||
});
|
||||
},
|
||||
|
||||
// Damage an enemy
|
||||
damage(enemy, amount) {
|
||||
enemy.health -= amount;
|
||||
enemy.damageFlash = 0.1;
|
||||
Audio.playEnemyHit();
|
||||
},
|
||||
|
||||
// Get enemies in radius
|
||||
getInRadius(x, y, radius) {
|
||||
return this.list.filter(e => {
|
||||
const dist = Math.sqrt((e.x - x) ** 2 + (e.y - y) ** 2);
|
||||
return dist <= radius;
|
||||
});
|
||||
},
|
||||
|
||||
// Get nearest enemy to position
|
||||
getNearest(x, y, maxRange = Infinity) {
|
||||
let nearest = null;
|
||||
let nearestDist = maxRange;
|
||||
|
||||
this.list.forEach(e => {
|
||||
const dist = Math.sqrt((e.x - x) ** 2 + (e.y - y) ** 2);
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearest = e;
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
},
|
||||
|
||||
// Get time until next wave
|
||||
getTimeUntilWave() {
|
||||
return this.waveActive ? 0 : Math.max(0, this.waveTimer);
|
||||
},
|
||||
|
||||
// Get data for saving
|
||||
getData() {
|
||||
return {
|
||||
list: this.list.map(e => ({ ...e })),
|
||||
wave: this.wave,
|
||||
waveTimer: this.waveTimer,
|
||||
waveActive: this.waveActive,
|
||||
enemiesRemaining: this.enemiesRemaining,
|
||||
spawnQueue: [...this.spawnQueue]
|
||||
};
|
||||
},
|
||||
|
||||
// Load data
|
||||
setData(data) {
|
||||
this.list = data.list?.map(e => ({ ...e })) || [];
|
||||
this.wave = data.wave || 0;
|
||||
this.waveTimer = data.waveTimer || this.timeBetweenWaves;
|
||||
this.waveActive = data.waveActive || false;
|
||||
this.enemiesRemaining = data.enemiesRemaining || 0;
|
||||
this.spawnQueue = data.spawnQueue || [];
|
||||
}
|
||||
};
|
||||
70
js/game.js
Normal file
70
js/game.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// Main Game Controller
|
||||
const game = {
|
||||
ui: UI,
|
||||
lastTime: 0,
|
||||
minimapTimer: 0,
|
||||
|
||||
// Initialize game
|
||||
init() {
|
||||
// Initialize all systems
|
||||
Audio.init();
|
||||
Resources.init();
|
||||
Terrain.init();
|
||||
Buildings.init();
|
||||
Towers.init();
|
||||
Enemies.init();
|
||||
Simulation.init();
|
||||
Renderer.init();
|
||||
Input.init();
|
||||
UI.init();
|
||||
|
||||
// Center camera on starting area
|
||||
Renderer.centerCamera(30, 30);
|
||||
|
||||
// Start game loop
|
||||
this.gameLoop(0);
|
||||
},
|
||||
|
||||
// Start new game
|
||||
newGame() {
|
||||
Resources.init();
|
||||
Terrain.init();
|
||||
Buildings.init();
|
||||
Towers.init();
|
||||
Enemies.init();
|
||||
Simulation.init();
|
||||
Renderer.centerCamera(30, 30);
|
||||
},
|
||||
|
||||
// Main game loop
|
||||
gameLoop(time) {
|
||||
const dt = Math.min((time - game.lastTime) / 1000, 0.1);
|
||||
game.lastTime = time;
|
||||
|
||||
// Update simulation
|
||||
Simulation.update(dt);
|
||||
Towers.update(dt);
|
||||
Enemies.update(dt);
|
||||
|
||||
// Update UI
|
||||
UI.updateResources();
|
||||
UI.updateToolButtons(Input.currentTool);
|
||||
UI.updateWaveIndicator();
|
||||
|
||||
// Update minimap less frequently
|
||||
game.minimapTimer += dt;
|
||||
if (game.minimapTimer > 0.2) {
|
||||
game.minimapTimer = 0;
|
||||
UI.updateMinimap();
|
||||
}
|
||||
|
||||
// Render
|
||||
Renderer.render(Input.currentTool, Input.rotation, Input.mouseWorld);
|
||||
|
||||
// Next frame
|
||||
requestAnimationFrame((t) => game.gameLoop(t));
|
||||
}
|
||||
};
|
||||
|
||||
// Start game when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => game.init());
|
||||
Reference in New Issue
Block a user