Upload files to "js"

This commit is contained in:
2026-01-13 18:32:05 +00:00
parent fddb8dbaf5
commit 2ce2f9cc7d
5 changed files with 889 additions and 0 deletions

188
js/audio.js Normal file
View 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
View 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
View 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
View 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
View 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());