Upload files to "js"
This commit is contained in:
92
js/terrain.js
Normal file
92
js/terrain.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Terrain Management
|
||||||
|
const Terrain = {
|
||||||
|
// Map data
|
||||||
|
tiles: [],
|
||||||
|
|
||||||
|
// Initialize terrain
|
||||||
|
init() {
|
||||||
|
this.generate();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generate new map
|
||||||
|
generate() {
|
||||||
|
this.tiles = [];
|
||||||
|
for (let y = 0; y < CONFIG.MAP_HEIGHT; y++) {
|
||||||
|
this.tiles[y] = [];
|
||||||
|
for (let x = 0; x < CONFIG.MAP_WIDTH; x++) {
|
||||||
|
this.tiles[y][x] = {
|
||||||
|
type: 'grass',
|
||||||
|
resource: null,
|
||||||
|
amount: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate resource patches
|
||||||
|
this.generatePatch(20, 20, 8, 'iron', 5000);
|
||||||
|
this.generatePatch(35, 15, 6, 'iron', 4000);
|
||||||
|
this.generatePatch(60, 30, 7, 'iron', 4500);
|
||||||
|
this.generatePatch(15, 35, 7, 'copper', 4500);
|
||||||
|
this.generatePatch(40, 45, 6, 'copper', 4000);
|
||||||
|
this.generatePatch(70, 50, 6, 'copper', 4000);
|
||||||
|
this.generatePatch(50, 25, 5, 'coal', 3000);
|
||||||
|
this.generatePatch(25, 55, 5, 'coal', 3000);
|
||||||
|
this.generatePatch(65, 15, 4, 'coal', 2500);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generate a resource patch
|
||||||
|
generatePatch(cx, cy, radius, type, amount) {
|
||||||
|
for (let dy = -radius; dy <= radius; dy++) {
|
||||||
|
for (let dx = -radius; dx <= radius; dx++) {
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (dist <= radius && Math.random() > dist / radius * 0.5) {
|
||||||
|
const x = cx + dx;
|
||||||
|
const y = cy + dy;
|
||||||
|
if (Utils.inBounds(x, y)) {
|
||||||
|
this.tiles[y][x].resource = type;
|
||||||
|
this.tiles[y][x].amount = Math.floor(amount * (1 - dist / radius * 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get tile at position
|
||||||
|
getTile(x, y) {
|
||||||
|
if (!Utils.inBounds(x, y)) return null;
|
||||||
|
return this.tiles[y][x];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mine from a tile (returns resource type or null)
|
||||||
|
mine(x, y, amount = 1) {
|
||||||
|
const tile = this.getTile(x, y);
|
||||||
|
if (!tile || !tile.resource || tile.amount <= 0) return null;
|
||||||
|
|
||||||
|
const resourceType = tile.resource;
|
||||||
|
const mined = Math.min(amount, tile.amount);
|
||||||
|
tile.amount -= mined;
|
||||||
|
|
||||||
|
if (tile.amount <= 0) {
|
||||||
|
tile.resource = null;
|
||||||
|
tile.amount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: resourceType, amount: mined };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if tile has resource
|
||||||
|
hasResource(x, y) {
|
||||||
|
const tile = this.getTile(x, y);
|
||||||
|
return tile && tile.resource && tile.amount > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get all tiles data for saving
|
||||||
|
getData() {
|
||||||
|
return this.tiles.map(row => row.map(t => ({ ...t })));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load tiles data
|
||||||
|
setData(data) {
|
||||||
|
this.tiles = data.map(row => row.map(t => ({ ...t })));
|
||||||
|
}
|
||||||
|
};
|
||||||
313
js/towers.js
Normal file
313
js/towers.js
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
// Defense Tower System
|
||||||
|
const Towers = {
|
||||||
|
list: [],
|
||||||
|
projectiles: [],
|
||||||
|
|
||||||
|
// Tower types
|
||||||
|
TYPES: {
|
||||||
|
'gun_turret': {
|
||||||
|
name: 'Gun Turret',
|
||||||
|
description: 'Rapid-fire bullets',
|
||||||
|
health: 200,
|
||||||
|
maxHealth: 200,
|
||||||
|
range: 200,
|
||||||
|
damage: 15,
|
||||||
|
fireRate: 4, // shots per second
|
||||||
|
projectileSpeed: 500,
|
||||||
|
projectileColor: '#ffcc00',
|
||||||
|
cost: { 'iron-plate': 20, 'gear': 10 },
|
||||||
|
color: '#888899',
|
||||||
|
colorDark: '#555566'
|
||||||
|
},
|
||||||
|
'flame_turret': {
|
||||||
|
name: 'Flame Turret',
|
||||||
|
description: 'Short range, high damage, burns',
|
||||||
|
health: 150,
|
||||||
|
maxHealth: 150,
|
||||||
|
range: 120,
|
||||||
|
damage: 40,
|
||||||
|
fireRate: 2,
|
||||||
|
burnDamage: 10,
|
||||||
|
burnDuration: 3,
|
||||||
|
projectileSpeed: 300,
|
||||||
|
projectileColor: '#ff6600',
|
||||||
|
cost: { 'iron-plate': 15, 'copper-plate': 20, 'coal': 50 },
|
||||||
|
color: '#cc4400',
|
||||||
|
colorDark: '#882200'
|
||||||
|
},
|
||||||
|
'laser_turret': {
|
||||||
|
name: 'Laser Turret',
|
||||||
|
description: 'Long range, high damage',
|
||||||
|
health: 100,
|
||||||
|
maxHealth: 100,
|
||||||
|
range: 350,
|
||||||
|
damage: 50,
|
||||||
|
fireRate: 1,
|
||||||
|
projectileSpeed: 1000,
|
||||||
|
projectileColor: '#ff0000',
|
||||||
|
cost: { 'iron-plate': 30, 'copper-plate': 30, 'circuit': 20 },
|
||||||
|
color: '#cc0044',
|
||||||
|
colorDark: '#880022'
|
||||||
|
},
|
||||||
|
'tesla_turret': {
|
||||||
|
name: 'Tesla Coil',
|
||||||
|
description: 'Chain lightning to multiple enemies',
|
||||||
|
health: 120,
|
||||||
|
maxHealth: 120,
|
||||||
|
range: 180,
|
||||||
|
damage: 25,
|
||||||
|
fireRate: 0.8,
|
||||||
|
chainCount: 3,
|
||||||
|
chainRange: 100,
|
||||||
|
projectileSpeed: 800,
|
||||||
|
projectileColor: '#44aaff',
|
||||||
|
cost: { 'iron-plate': 25, 'copper-plate': 50, 'circuit': 15 },
|
||||||
|
color: '#2266aa',
|
||||||
|
colorDark: '#113366'
|
||||||
|
},
|
||||||
|
'cannon_turret': {
|
||||||
|
name: 'Cannon',
|
||||||
|
description: 'Slow, explosive AoE damage',
|
||||||
|
health: 300,
|
||||||
|
maxHealth: 300,
|
||||||
|
range: 280,
|
||||||
|
damage: 100,
|
||||||
|
fireRate: 0.5,
|
||||||
|
splashRadius: 80,
|
||||||
|
projectileSpeed: 250,
|
||||||
|
projectileColor: '#333333',
|
||||||
|
cost: { 'iron-plate': 50, 'gear': 20, 'coal': 30 },
|
||||||
|
color: '#444455',
|
||||||
|
colorDark: '#222233'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
init() {
|
||||||
|
this.list = [];
|
||||||
|
this.projectiles = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if can place tower
|
||||||
|
canPlace(type, x, y) {
|
||||||
|
// Check bounds
|
||||||
|
if (!Utils.inBounds(x, y)) return false;
|
||||||
|
|
||||||
|
// Check collision with buildings
|
||||||
|
if (Buildings.getAt(x, y)) return false;
|
||||||
|
|
||||||
|
// Check collision with other towers
|
||||||
|
if (this.getAt(x, y)) return false;
|
||||||
|
|
||||||
|
// Check cost
|
||||||
|
const towerType = this.TYPES[type];
|
||||||
|
if (!Resources.canAfford(towerType.cost)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Place a tower
|
||||||
|
place(type, x, y) {
|
||||||
|
if (!this.canPlace(type, x, y)) {
|
||||||
|
Audio.playError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const towerType = this.TYPES[type];
|
||||||
|
Resources.payCost(towerType.cost);
|
||||||
|
|
||||||
|
this.list.push({
|
||||||
|
type,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
health: towerType.maxHealth,
|
||||||
|
maxHealth: towerType.maxHealth,
|
||||||
|
cooldown: 0,
|
||||||
|
angle: 0,
|
||||||
|
target: null
|
||||||
|
});
|
||||||
|
|
||||||
|
Audio.playPlace();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove a tower
|
||||||
|
remove(x, y) {
|
||||||
|
const tower = this.getAt(x, y);
|
||||||
|
if (!tower) return false;
|
||||||
|
|
||||||
|
const idx = this.list.indexOf(tower);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.list.splice(idx, 1);
|
||||||
|
// Refund half
|
||||||
|
const towerType = this.TYPES[tower.type];
|
||||||
|
Resources.refundHalf(towerType.cost);
|
||||||
|
Audio.playDelete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get tower at position
|
||||||
|
getAt(x, y) {
|
||||||
|
return this.list.find(t => t.x === x && t.y === y);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update all towers
|
||||||
|
update(dt) {
|
||||||
|
// Update towers
|
||||||
|
this.list.forEach(tower => this.updateTower(tower, dt));
|
||||||
|
|
||||||
|
// Update projectiles
|
||||||
|
for (let i = this.projectiles.length - 1; i >= 0; i--) {
|
||||||
|
const proj = this.projectiles[i];
|
||||||
|
this.updateProjectile(proj, dt);
|
||||||
|
|
||||||
|
// Remove dead projectiles
|
||||||
|
if (proj.dead) {
|
||||||
|
this.projectiles.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update single tower
|
||||||
|
updateTower(tower, dt) {
|
||||||
|
const type = this.TYPES[tower.type];
|
||||||
|
const towerX = (tower.x + 0.5) * CONFIG.TILE_SIZE;
|
||||||
|
const towerY = (tower.y + 0.5) * CONFIG.TILE_SIZE;
|
||||||
|
|
||||||
|
// Reduce cooldown
|
||||||
|
tower.cooldown = Math.max(0, tower.cooldown - dt);
|
||||||
|
|
||||||
|
// Find target
|
||||||
|
const target = Enemies.getNearest(towerX, towerY, type.range);
|
||||||
|
tower.target = target;
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
// Rotate toward target
|
||||||
|
const dx = target.x - towerX;
|
||||||
|
const dy = target.y - towerY;
|
||||||
|
tower.angle = Math.atan2(dy, dx);
|
||||||
|
|
||||||
|
// Fire if ready
|
||||||
|
if (tower.cooldown <= 0) {
|
||||||
|
tower.cooldown = 1 / type.fireRate;
|
||||||
|
this.fire(tower, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fire at target
|
||||||
|
fire(tower, target) {
|
||||||
|
const type = this.TYPES[tower.type];
|
||||||
|
const startX = (tower.x + 0.5) * CONFIG.TILE_SIZE;
|
||||||
|
const startY = (tower.y + 0.5) * CONFIG.TILE_SIZE;
|
||||||
|
|
||||||
|
this.projectiles.push({
|
||||||
|
towerType: tower.type,
|
||||||
|
x: startX,
|
||||||
|
y: startY,
|
||||||
|
targetX: target.x,
|
||||||
|
targetY: target.y,
|
||||||
|
target: target,
|
||||||
|
speed: type.projectileSpeed,
|
||||||
|
damage: type.damage,
|
||||||
|
color: type.projectileColor,
|
||||||
|
splashRadius: type.splashRadius || 0,
|
||||||
|
chainCount: type.chainCount || 0,
|
||||||
|
chainRange: type.chainRange || 0,
|
||||||
|
burnDamage: type.burnDamage || 0,
|
||||||
|
burnDuration: type.burnDuration || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
tower.firing = true;
|
||||||
|
setTimeout(() => tower.firing = false, 50);
|
||||||
|
|
||||||
|
// Sound
|
||||||
|
Audio.playShoot(tower.type);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update projectile
|
||||||
|
updateProjectile(proj, dt) {
|
||||||
|
const dx = proj.targetX - proj.x;
|
||||||
|
const dy = proj.targetY - proj.y;
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (dist < 10) {
|
||||||
|
// Hit target
|
||||||
|
this.projectileHit(proj);
|
||||||
|
proj.dead = true;
|
||||||
|
} else {
|
||||||
|
// Move toward target
|
||||||
|
const moveX = (dx / dist) * proj.speed * dt;
|
||||||
|
const moveY = (dy / dist) * proj.speed * dt;
|
||||||
|
proj.x += moveX;
|
||||||
|
proj.y += moveY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
proj.lifetime = (proj.lifetime || 0) + dt;
|
||||||
|
if (proj.lifetime > 5) proj.dead = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Projectile hits
|
||||||
|
projectileHit(proj) {
|
||||||
|
// Splash damage
|
||||||
|
if (proj.splashRadius > 0) {
|
||||||
|
const enemies = Enemies.getInRadius(proj.targetX, proj.targetY, proj.splashRadius);
|
||||||
|
enemies.forEach(e => {
|
||||||
|
Enemies.damage(e, proj.damage);
|
||||||
|
});
|
||||||
|
Audio.playExplosion();
|
||||||
|
} else if (proj.chainCount > 0) {
|
||||||
|
// Chain lightning
|
||||||
|
let targets = [proj.target];
|
||||||
|
let lastTarget = proj.target;
|
||||||
|
|
||||||
|
for (let i = 0; i < proj.chainCount; i++) {
|
||||||
|
const nearby = Enemies.getInRadius(lastTarget.x, lastTarget.y, proj.chainRange)
|
||||||
|
.filter(e => !targets.includes(e));
|
||||||
|
if (nearby.length > 0) {
|
||||||
|
const next = nearby[0];
|
||||||
|
targets.push(next);
|
||||||
|
lastTarget = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.forEach(e => {
|
||||||
|
Enemies.damage(e, proj.damage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store chain for rendering
|
||||||
|
proj.chainTargets = targets;
|
||||||
|
} else {
|
||||||
|
// Single target
|
||||||
|
if (proj.target && proj.target.health > 0) {
|
||||||
|
Enemies.damage(proj.target, proj.damage);
|
||||||
|
|
||||||
|
// Apply burn
|
||||||
|
if (proj.burnDamage > 0) {
|
||||||
|
proj.target.burning = {
|
||||||
|
damage: proj.burnDamage,
|
||||||
|
duration: proj.burnDuration
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get data for saving
|
||||||
|
getData() {
|
||||||
|
return {
|
||||||
|
list: this.list.map(t => ({ ...t, target: null })),
|
||||||
|
projectiles: [] // Don't save projectiles
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
setData(data) {
|
||||||
|
this.list = data.list?.map(t => ({ ...t })) || [];
|
||||||
|
this.projectiles = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
525
js/ui.js
Normal file
525
js/ui.js
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
// UI Management
|
||||||
|
const UI = {
|
||||||
|
tooltip: null,
|
||||||
|
recipeSelect: null,
|
||||||
|
selectedBuilding: null,
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
init() {
|
||||||
|
this.tooltip = document.getElementById('tooltip');
|
||||||
|
this.recipeSelect = document.getElementById('recipe-select');
|
||||||
|
|
||||||
|
this.createResourceBar();
|
||||||
|
this.createToolbar();
|
||||||
|
this.setupEventListeners();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create resource bar
|
||||||
|
createResourceBar() {
|
||||||
|
const container = document.getElementById('resource-bar');
|
||||||
|
container.innerHTML = CONFIG.RESOURCE_ORDER.map(res => `
|
||||||
|
<div class="resource">
|
||||||
|
<div class="resource-icon ${res}"></div>
|
||||||
|
<span>${CONFIG.RESOURCE_NAMES[res].split(' ')[0]}: <span id="res-${res}">0</span></span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create toolbar
|
||||||
|
createToolbar() {
|
||||||
|
const toolbar = document.getElementById('toolbar');
|
||||||
|
toolbar.innerHTML = `
|
||||||
|
<h3 data-category="tools">Tools</h3>
|
||||||
|
<div class="tool-category" id="cat-tools">
|
||||||
|
<button class="tool-btn active" data-tool="select">
|
||||||
|
<div class="icon select">🖱</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Select</div>
|
||||||
|
<div class="cost">View & interact</div>
|
||||||
|
<div class="hotkey">[Q]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="delete">
|
||||||
|
<div class="icon delete">✕</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Demolish</div>
|
||||||
|
<div class="cost">Remove buildings</div>
|
||||||
|
<div class="hotkey">[X]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="mine">
|
||||||
|
<div class="icon mine">⛏</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Hand Mine</div>
|
||||||
|
<div class="cost">Click on ore</div>
|
||||||
|
<div class="hotkey">[E]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 data-category="extraction">Extraction</h3>
|
||||||
|
<div class="tool-category" id="cat-extraction">
|
||||||
|
<button class="tool-btn" data-tool="miner">
|
||||||
|
<div class="icon miner">⛏</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Miner</div>
|
||||||
|
<div class="cost">10 iron plates</div>
|
||||||
|
<div class="hotkey">[1]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 data-category="logistics">Logistics</h3>
|
||||||
|
<div class="tool-category" id="cat-logistics">
|
||||||
|
<button class="tool-btn" data-tool="belt">
|
||||||
|
<div class="icon belt">→</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Belt</div>
|
||||||
|
<div class="cost">1 iron plate</div>
|
||||||
|
<div class="hotkey">[2]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="inserter">
|
||||||
|
<div class="icon inserter">⤵</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Inserter</div>
|
||||||
|
<div class="cost">2 iron, 1 gear</div>
|
||||||
|
<div class="hotkey">[3]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="chest">
|
||||||
|
<div class="icon chest">📦</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Chest</div>
|
||||||
|
<div class="cost">5 iron plates</div>
|
||||||
|
<div class="hotkey">[4]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 data-category="production">Production</h3>
|
||||||
|
<div class="tool-category" id="cat-production">
|
||||||
|
<button class="tool-btn" data-tool="furnace">
|
||||||
|
<div class="icon furnace">🔥</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Furnace</div>
|
||||||
|
<div class="cost">10 iron ore</div>
|
||||||
|
<div class="hotkey">[5]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="assembler">
|
||||||
|
<div class="icon assembler">⚙</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Assembler</div>
|
||||||
|
<div class="cost">5 iron, 3 gears</div>
|
||||||
|
<div class="hotkey">[6]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 data-category="defense">Defense</h3>
|
||||||
|
<div class="tool-category" id="cat-defense">
|
||||||
|
<button class="tool-btn" data-tool="gun_turret">
|
||||||
|
<div class="icon gun_turret">🔫</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Gun Turret</div>
|
||||||
|
<div class="cost">20 iron, 10 gear</div>
|
||||||
|
<div class="hotkey">[7]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="flame_turret">
|
||||||
|
<div class="icon flame_turret">🔥</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Flame Turret</div>
|
||||||
|
<div class="cost">15i, 20c, 50 coal</div>
|
||||||
|
<div class="hotkey">[8]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="laser_turret">
|
||||||
|
<div class="icon laser_turret">⚡</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Laser Turret</div>
|
||||||
|
<div class="cost">30i, 30c, 20 circuit</div>
|
||||||
|
<div class="hotkey">[9]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="tesla_turret">
|
||||||
|
<div class="icon tesla_turret">⚡</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Tesla Coil</div>
|
||||||
|
<div class="cost">25i, 50c, 15 circuit</div>
|
||||||
|
<div class="hotkey">[0]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn" data-tool="cannon_turret">
|
||||||
|
<div class="icon cannon_turret">💣</div>
|
||||||
|
<div class="tool-info">
|
||||||
|
<div class="name">Cannon</div>
|
||||||
|
<div class="cost">50i, 20 gear, 30 coal</div>
|
||||||
|
<div class="hotkey">[-]</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 data-category="speed">Game Speed</h3>
|
||||||
|
<div class="tool-category" id="cat-speed">
|
||||||
|
<div class="game-speed">
|
||||||
|
<button class="speed-btn" data-speed="0.5">0.5x</button>
|
||||||
|
<button class="speed-btn active" data-speed="1">1x</button>
|
||||||
|
<button class="speed-btn" data-speed="2">2x</button>
|
||||||
|
<button class="speed-btn" data-speed="4">4x</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions">
|
||||||
|
<strong>Controls:</strong><br>
|
||||||
|
<kbd>R</kbd> Rotate | <kbd>E</kbd> Mine<br>
|
||||||
|
<kbd>WASD</kbd> Pan | <kbd>Scroll</kbd> Zoom<br>
|
||||||
|
<strong>Dev:</strong> <kbd>\`</kbd> Toggle<br>
|
||||||
|
<kbd>F1</kbd> Wave <kbd>F2</kbd> Kill <kbd>F3</kbd> Res
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Category collapse handlers
|
||||||
|
toolbar.querySelectorAll('h3[data-category]').forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
Audio.playUIClick();
|
||||||
|
header.classList.toggle('collapsed');
|
||||||
|
const category = document.getElementById('cat-' + header.dataset.category);
|
||||||
|
if (category) {
|
||||||
|
category.classList.toggle('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tool button handlers
|
||||||
|
toolbar.querySelectorAll('.tool-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
Audio.playUIClick();
|
||||||
|
Input.selectTool(btn.dataset.tool);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Speed button handlers
|
||||||
|
toolbar.querySelectorAll('.speed-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
Audio.playUIClick();
|
||||||
|
Simulation.setSpeed(parseFloat(btn.dataset.speed));
|
||||||
|
toolbar.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create wave indicator
|
||||||
|
this.createWaveIndicator();
|
||||||
|
|
||||||
|
// Create minimap
|
||||||
|
this.createMinimap();
|
||||||
|
|
||||||
|
// Create sound toggle
|
||||||
|
this.createSoundToggle();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create wave indicator element
|
||||||
|
createWaveIndicator() {
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.id = 'wave-indicator';
|
||||||
|
indicator.className = 'wave-indicator peaceful';
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<div class="wave-title">Peaceful</div>
|
||||||
|
<div class="wave-info">Next wave in: --:--</div>
|
||||||
|
`;
|
||||||
|
document.querySelector('.canvas-container').appendChild(indicator);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create minimap
|
||||||
|
createMinimap() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'minimap-container';
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="minimap-header">
|
||||||
|
<span>MINIMAP</span>
|
||||||
|
</div>
|
||||||
|
<canvas id="minimap" width="150" height="150"></canvas>
|
||||||
|
`;
|
||||||
|
document.querySelector('.canvas-container').appendChild(container);
|
||||||
|
|
||||||
|
// Click to navigate
|
||||||
|
const minimap = document.getElementById('minimap');
|
||||||
|
minimap.addEventListener('click', (e) => {
|
||||||
|
const rect = minimap.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) / minimap.width;
|
||||||
|
const y = (e.clientY - rect.top) / minimap.height;
|
||||||
|
|
||||||
|
Renderer.camera.x = x * CONFIG.MAP_WIDTH * CONFIG.TILE_SIZE * Renderer.camera.zoom - Renderer.canvas.width / 2;
|
||||||
|
Renderer.camera.y = y * CONFIG.MAP_HEIGHT * CONFIG.TILE_SIZE * Renderer.camera.zoom - Renderer.canvas.height / 2;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update minimap
|
||||||
|
updateMinimap() {
|
||||||
|
const canvas = document.getElementById('minimap');
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const scale = canvas.width / CONFIG.MAP_WIDTH;
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
ctx.fillStyle = '#1a2a1a';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw resources
|
||||||
|
for (let y = 0; y < CONFIG.MAP_HEIGHT; y++) {
|
||||||
|
for (let x = 0; x < CONFIG.MAP_WIDTH; x++) {
|
||||||
|
const tile = Terrain.tiles[y]?.[x];
|
||||||
|
if (tile?.resource && tile.amount > 0) {
|
||||||
|
const colors = { iron: '#8899bb', copper: '#cc9966', coal: '#444' };
|
||||||
|
ctx.fillStyle = colors[tile.resource] || '#666';
|
||||||
|
ctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw buildings
|
||||||
|
ctx.fillStyle = '#ffaa00';
|
||||||
|
Buildings.list.forEach(b => {
|
||||||
|
const size = Utils.getBuildingSize(b.type);
|
||||||
|
ctx.fillRect(b.x * scale, b.y * scale, size.w * scale + 0.5, size.h * scale + 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw towers
|
||||||
|
ctx.fillStyle = '#4a9eff';
|
||||||
|
Towers.list.forEach(t => {
|
||||||
|
ctx.fillRect(t.x * scale, t.y * scale, scale + 0.5, scale + 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw enemies
|
||||||
|
ctx.fillStyle = '#ff4444';
|
||||||
|
Enemies.list.forEach(e => {
|
||||||
|
const ex = e.x / CONFIG.TILE_SIZE * scale;
|
||||||
|
const ey = e.y / CONFIG.TILE_SIZE * scale;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(ex, ey, 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw viewport
|
||||||
|
const viewX = Renderer.camera.x / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale;
|
||||||
|
const viewY = Renderer.camera.y / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale;
|
||||||
|
const viewW = Renderer.canvas.width / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale;
|
||||||
|
const viewH = Renderer.canvas.height / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale;
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#fff';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(viewX, viewY, viewW, viewH);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create sound toggle
|
||||||
|
createSoundToggle() {
|
||||||
|
const toggle = document.createElement('div');
|
||||||
|
toggle.className = 'sound-toggle';
|
||||||
|
toggle.id = 'sound-toggle';
|
||||||
|
toggle.innerHTML = '🔊 Sound';
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
const enabled = Audio.toggle();
|
||||||
|
toggle.innerHTML = enabled ? '🔊 Sound' : '🔇 Muted';
|
||||||
|
toggle.classList.toggle('muted', !enabled);
|
||||||
|
});
|
||||||
|
document.querySelector('.canvas-container').appendChild(toggle);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update wave indicator
|
||||||
|
updateWaveIndicator() {
|
||||||
|
const indicator = document.getElementById('wave-indicator');
|
||||||
|
if (!indicator) return;
|
||||||
|
|
||||||
|
const waveActive = Enemies.waveActive;
|
||||||
|
const timeUntil = Enemies.getTimeUntilWave();
|
||||||
|
|
||||||
|
indicator.className = 'wave-indicator ' + (waveActive ? '' : 'peaceful');
|
||||||
|
|
||||||
|
if (waveActive) {
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<div class="wave-title">⚔ WAVE ${Enemies.wave} ⚔</div>
|
||||||
|
<div class="wave-info">Enemies remaining: ${Enemies.enemiesRemaining}</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(timeUntil / 60);
|
||||||
|
const seconds = Math.floor(timeUntil % 60);
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<div class="wave-title">Wave ${Enemies.wave + 1} incoming</div>
|
||||||
|
<div class="wave-info">Time until wave: ${minutes}:${seconds.toString().padStart(2, '0')}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev mode indicator
|
||||||
|
this.updateDevModeIndicator();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show dev mode notification
|
||||||
|
showDevModeNotification() {
|
||||||
|
const existing = document.getElementById('dev-notification');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.id = 'dev-notification';
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: ${CONFIG.DEV_MODE ? 'rgba(68, 255, 68, 0.9)' : 'rgba(255, 68, 68, 0.9)'};
|
||||||
|
color: #000;
|
||||||
|
padding: 20px 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
notification.textContent = CONFIG.DEV_MODE ? '🛠 DEV MODE ON' : '🎮 DEV MODE OFF';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => notification.remove(), 1500);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update dev mode indicator
|
||||||
|
updateDevModeIndicator() {
|
||||||
|
let indicator = document.getElementById('dev-mode-indicator');
|
||||||
|
|
||||||
|
if (CONFIG.DEV_MODE) {
|
||||||
|
if (!indicator) {
|
||||||
|
indicator = document.createElement('div');
|
||||||
|
indicator.id = 'dev-mode-indicator';
|
||||||
|
indicator.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: rgba(68, 255, 68, 0.8);
|
||||||
|
color: #000;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
z-index: 9999;
|
||||||
|
`;
|
||||||
|
indicator.textContent = '🛠 DEV MODE';
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (indicator) indicator.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
setupEventListeners() {
|
||||||
|
// Menu buttons
|
||||||
|
document.getElementById('btn-new').addEventListener('click', () => this.showModal('new-modal'));
|
||||||
|
document.getElementById('btn-save').addEventListener('click', () => {
|
||||||
|
SaveLoad.updateSaveSlots();
|
||||||
|
this.showModal('save-modal');
|
||||||
|
});
|
||||||
|
document.getElementById('btn-load').addEventListener('click', () => {
|
||||||
|
SaveLoad.updateLoadSlots();
|
||||||
|
this.showModal('load-modal');
|
||||||
|
});
|
||||||
|
document.getElementById('save-new').addEventListener('click', () => {
|
||||||
|
SaveLoad.saveToNewSlot();
|
||||||
|
this.closeModal('save-modal');
|
||||||
|
});
|
||||||
|
document.getElementById('confirm-new').addEventListener('click', () => {
|
||||||
|
game.newGame();
|
||||||
|
this.closeModal('new-modal');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recipe select
|
||||||
|
document.querySelectorAll('.recipe-option').forEach(opt => {
|
||||||
|
opt.addEventListener('click', () => {
|
||||||
|
if (this.selectedBuilding && this.selectedBuilding.type === 'assembler') {
|
||||||
|
this.selectedBuilding.recipe = opt.dataset.recipe;
|
||||||
|
this.recipeSelect.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close recipe select when clicking elsewhere
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!this.recipeSelect.contains(e.target)) {
|
||||||
|
this.recipeSelect.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update resource display
|
||||||
|
updateResources() {
|
||||||
|
CONFIG.RESOURCE_ORDER.forEach(res => {
|
||||||
|
const el = document.getElementById(`res-${res}`);
|
||||||
|
if (el) {
|
||||||
|
el.textContent = Utils.formatNumber(Resources.get(res));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update tool button states
|
||||||
|
updateToolButtons(activeTool) {
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
|
||||||
|
const tool = btn.dataset.tool;
|
||||||
|
btn.classList.toggle('active', tool === activeTool);
|
||||||
|
|
||||||
|
// Disable if can't afford
|
||||||
|
if (CONFIG.COSTS[tool]) {
|
||||||
|
btn.classList.toggle('disabled', !Resources.canAfford(CONFIG.COSTS[tool]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update tooltip
|
||||||
|
updateTooltip(clientX, clientY, mouseWorld) {
|
||||||
|
const building = Buildings.getAt(mouseWorld.x, mouseWorld.y);
|
||||||
|
const tile = Terrain.getTile(mouseWorld.x, mouseWorld.y);
|
||||||
|
|
||||||
|
if (building) {
|
||||||
|
let info = `<strong style="color:#4a9eff">${building.type.toUpperCase()}</strong><br>`;
|
||||||
|
if (building.type === 'assembler') {
|
||||||
|
info += `Recipe: ${building.recipe || 'None'}<br>`;
|
||||||
|
}
|
||||||
|
const invItems = Object.entries(building.inventory).filter(([k, v]) => v > 0);
|
||||||
|
const outItems = Object.entries(building.output).filter(([k, v]) => v > 0);
|
||||||
|
if (invItems.length > 0) {
|
||||||
|
info += '<span style="color:#aaa">Input:</span> ' + invItems.map(([k, v]) => `${k}: ${v}`).join(', ') + '<br>';
|
||||||
|
}
|
||||||
|
if (outItems.length > 0) {
|
||||||
|
info += '<span style="color:#aaa">Output:</span> ' + outItems.map(([k, v]) => `${k}: ${v}`).join(', ');
|
||||||
|
}
|
||||||
|
this.tooltip.innerHTML = info;
|
||||||
|
this.tooltip.style.display = 'block';
|
||||||
|
this.tooltip.style.left = (clientX + 15) + 'px';
|
||||||
|
this.tooltip.style.top = (clientY + 15) + 'px';
|
||||||
|
} else if (tile?.resource && tile.amount > 0) {
|
||||||
|
this.tooltip.innerHTML = `<strong style="color:#4a9eff">${tile.resource.toUpperCase()} ORE</strong><br>Amount: ${Utils.formatNumber(tile.amount)}<br><span style="color:#88cc44">Click with Mine tool [E] to harvest</span>`;
|
||||||
|
this.tooltip.style.display = 'block';
|
||||||
|
this.tooltip.style.left = (clientX + 15) + 'px';
|
||||||
|
this.tooltip.style.top = (clientY + 15) + 'px';
|
||||||
|
} else {
|
||||||
|
this.tooltip.style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show recipe select
|
||||||
|
showRecipeSelect(clientX, clientY, building) {
|
||||||
|
this.selectedBuilding = building;
|
||||||
|
this.recipeSelect.style.display = 'block';
|
||||||
|
this.recipeSelect.style.left = (clientX + 10) + 'px';
|
||||||
|
this.recipeSelect.style.top = (clientY + 10) + 'px';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
showModal(id) {
|
||||||
|
document.getElementById(id).style.display = 'flex';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
closeModal(id) {
|
||||||
|
document.getElementById(id).style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
63
js/utils.js
Normal file
63
js/utils.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Utility Functions
|
||||||
|
const Utils = {
|
||||||
|
// Convert screen coordinates to world tile coordinates
|
||||||
|
screenToWorld(sx, sy, camera) {
|
||||||
|
return {
|
||||||
|
x: Math.floor((sx + camera.x) / (CONFIG.TILE_SIZE * camera.zoom)),
|
||||||
|
y: Math.floor((sy + camera.y) / (CONFIG.TILE_SIZE * camera.zoom))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Convert world tile coordinates to screen coordinates
|
||||||
|
worldToScreen(wx, wy, camera) {
|
||||||
|
return {
|
||||||
|
x: wx * CONFIG.TILE_SIZE * camera.zoom - camera.x,
|
||||||
|
y: wy * CONFIG.TILE_SIZE * camera.zoom - camera.y
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Draw a rounded rectangle
|
||||||
|
drawRoundedRect(ctx, x, y, w, h, r) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + r, y);
|
||||||
|
ctx.lineTo(x + w - r, y);
|
||||||
|
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||||||
|
ctx.lineTo(x + w, y + h - r);
|
||||||
|
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||||||
|
ctx.lineTo(x + r, y + h);
|
||||||
|
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||||||
|
ctx.lineTo(x, y + r);
|
||||||
|
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||||
|
ctx.closePath();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get building size from config
|
||||||
|
getBuildingSize(type) {
|
||||||
|
return CONFIG.BUILDING_SIZES[type] || { w: 1, h: 1 };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deep clone an object
|
||||||
|
deepClone(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format number with commas
|
||||||
|
formatNumber(num) {
|
||||||
|
return num.toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clamp a value between min and max
|
||||||
|
clamp(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Linear interpolation
|
||||||
|
lerp(a, b, t) {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if a point is within bounds
|
||||||
|
inBounds(x, y) {
|
||||||
|
return x >= 0 && x < CONFIG.MAP_WIDTH && y >= 0 && y < CONFIG.MAP_HEIGHT;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user