Upload files to "js"
This commit is contained in:
233
js/input.js
Normal file
233
js/input.js
Normal file
@@ -0,0 +1,233 @@
|
||||
// Input Handling
|
||||
const Input = {
|
||||
currentTool: 'select',
|
||||
rotation: 0,
|
||||
mouseScreen: { x: 0, y: 0 },
|
||||
mouseWorld: { x: 0, y: 0 },
|
||||
isDragging: false,
|
||||
lastMouse: { x: 0, y: 0 },
|
||||
isMouseDown: false,
|
||||
|
||||
// Initialize input handlers
|
||||
init() {
|
||||
const canvas = Renderer.canvas;
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||
canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
|
||||
canvas.addEventListener('mouseleave', () => this.onMouseLeave());
|
||||
canvas.addEventListener('wheel', (e) => this.onWheel(e));
|
||||
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
document.addEventListener('keydown', (e) => this.onKeyDown(e));
|
||||
},
|
||||
|
||||
// Mouse down handler
|
||||
onMouseDown(e) {
|
||||
const rect = Renderer.canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
|
||||
// Right click or middle click to drag
|
||||
if (e.button === 2 || e.button === 1) {
|
||||
this.isDragging = true;
|
||||
this.lastMouse = { x: e.clientX, y: e.clientY };
|
||||
Renderer.canvas.style.cursor = 'grabbing';
|
||||
return;
|
||||
}
|
||||
|
||||
this.isMouseDown = true;
|
||||
const world = Utils.screenToWorld(mx, my, Renderer.camera);
|
||||
|
||||
if (this.currentTool === 'select') {
|
||||
const building = Buildings.getAt(world.x, world.y);
|
||||
if (building && building.type === 'assembler') {
|
||||
game.ui.showRecipeSelect(e.clientX, e.clientY, building);
|
||||
}
|
||||
} else if (this.currentTool === 'delete') {
|
||||
Buildings.remove(world.x, world.y);
|
||||
Towers.remove(world.x, world.y);
|
||||
} else if (this.currentTool === 'mine') {
|
||||
// Start manual mining
|
||||
Simulation.startMining(world.x, world.y);
|
||||
} else if (Towers.TYPES[this.currentTool]) {
|
||||
// Place tower
|
||||
Towers.place(this.currentTool, world.x, world.y);
|
||||
} else {
|
||||
Buildings.place(this.currentTool, world.x, world.y, this.rotation);
|
||||
}
|
||||
},
|
||||
|
||||
// Mouse move handler
|
||||
onMouseMove(e) {
|
||||
const rect = Renderer.canvas.getBoundingClientRect();
|
||||
this.mouseScreen.x = e.clientX - rect.left;
|
||||
this.mouseScreen.y = e.clientY - rect.top;
|
||||
this.mouseWorld = Utils.screenToWorld(this.mouseScreen.x, this.mouseScreen.y, Renderer.camera);
|
||||
|
||||
if (this.isDragging) {
|
||||
Renderer.camera.x -= (e.clientX - this.lastMouse.x);
|
||||
Renderer.camera.y -= (e.clientY - this.lastMouse.y);
|
||||
this.lastMouse = { x: e.clientX, y: e.clientY };
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue mining if holding mouse and using mine tool
|
||||
if (this.isMouseDown && this.currentTool === 'mine') {
|
||||
if (!Simulation.mining.active ||
|
||||
Simulation.mining.x !== this.mouseWorld.x ||
|
||||
Simulation.mining.y !== this.mouseWorld.y) {
|
||||
Simulation.startMining(this.mouseWorld.x, this.mouseWorld.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Update tooltip
|
||||
game.ui.updateTooltip(e.clientX, e.clientY, this.mouseWorld);
|
||||
},
|
||||
|
||||
// Mouse up handler
|
||||
onMouseUp(e) {
|
||||
this.isDragging = false;
|
||||
this.isMouseDown = false;
|
||||
Renderer.canvas.style.cursor = 'crosshair';
|
||||
|
||||
// Stop mining when releasing mouse
|
||||
if (this.currentTool === 'mine') {
|
||||
Simulation.stopMining();
|
||||
}
|
||||
},
|
||||
|
||||
// Mouse leave handler
|
||||
onMouseLeave() {
|
||||
this.isDragging = false;
|
||||
this.isMouseDown = false;
|
||||
if (this.currentTool === 'mine') {
|
||||
Simulation.stopMining();
|
||||
}
|
||||
},
|
||||
|
||||
// Mouse wheel handler
|
||||
onWheel(e) {
|
||||
e.preventDefault();
|
||||
const oldZoom = Renderer.camera.zoom;
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
Renderer.camera.zoom = Utils.clamp(Renderer.camera.zoom * zoomFactor, 0.4, 2.5);
|
||||
|
||||
// Zoom toward mouse position
|
||||
const worldX = (this.mouseScreen.x + Renderer.camera.x) / oldZoom;
|
||||
const worldY = (this.mouseScreen.y + Renderer.camera.y) / oldZoom;
|
||||
Renderer.camera.x = worldX * Renderer.camera.zoom - this.mouseScreen.x;
|
||||
Renderer.camera.y = worldY * Renderer.camera.zoom - this.mouseScreen.y;
|
||||
},
|
||||
|
||||
// Key down handler
|
||||
onKeyDown(e) {
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
switch (key) {
|
||||
case 'r':
|
||||
this.rotation = (this.rotation + 1) % 4;
|
||||
break;
|
||||
case 'q':
|
||||
this.selectTool('select');
|
||||
break;
|
||||
case 'x':
|
||||
this.selectTool('delete');
|
||||
break;
|
||||
case 'e':
|
||||
this.selectTool('mine');
|
||||
break;
|
||||
case '1':
|
||||
this.selectTool('miner');
|
||||
break;
|
||||
case '2':
|
||||
this.selectTool('belt');
|
||||
break;
|
||||
case '3':
|
||||
this.selectTool('inserter');
|
||||
break;
|
||||
case '4':
|
||||
this.selectTool('chest');
|
||||
break;
|
||||
case '5':
|
||||
this.selectTool('furnace');
|
||||
break;
|
||||
case '6':
|
||||
this.selectTool('assembler');
|
||||
break;
|
||||
case '7':
|
||||
this.selectTool('gun_turret');
|
||||
break;
|
||||
case '8':
|
||||
this.selectTool('flame_turret');
|
||||
break;
|
||||
case '9':
|
||||
this.selectTool('laser_turret');
|
||||
break;
|
||||
case '0':
|
||||
this.selectTool('tesla_turret');
|
||||
break;
|
||||
case '-':
|
||||
this.selectTool('cannon_turret');
|
||||
break;
|
||||
case '`':
|
||||
case '~':
|
||||
// Toggle dev mode
|
||||
CONFIG.DEV_MODE = !CONFIG.DEV_MODE;
|
||||
game.ui.showDevModeNotification();
|
||||
Audio.playDevMode();
|
||||
break;
|
||||
case 'f1':
|
||||
// Dev: Spawn wave immediately
|
||||
if (CONFIG.DEV_MODE) {
|
||||
e.preventDefault();
|
||||
Enemies.waveTimer = 0;
|
||||
}
|
||||
break;
|
||||
case 'f2':
|
||||
// Dev: Kill all enemies
|
||||
if (CONFIG.DEV_MODE) {
|
||||
e.preventDefault();
|
||||
Enemies.list = [];
|
||||
Enemies.enemiesRemaining = 0;
|
||||
Enemies.spawnQueue = [];
|
||||
}
|
||||
break;
|
||||
case 'f3':
|
||||
// Dev: Add resources
|
||||
if (CONFIG.DEV_MODE) {
|
||||
e.preventDefault();
|
||||
Resources.add('iron', 100);
|
||||
Resources.add('copper', 100);
|
||||
Resources.add('coal', 100);
|
||||
Resources.add('iron-plate', 100);
|
||||
Resources.add('copper-plate', 100);
|
||||
Resources.add('gear', 50);
|
||||
Resources.add('circuit', 50);
|
||||
}
|
||||
break;
|
||||
case 'w':
|
||||
case 'arrowup':
|
||||
Renderer.camera.y -= 50;
|
||||
break;
|
||||
case 's':
|
||||
case 'arrowdown':
|
||||
Renderer.camera.y += 50;
|
||||
break;
|
||||
case 'a':
|
||||
case 'arrowleft':
|
||||
Renderer.camera.x -= 50;
|
||||
break;
|
||||
case 'd':
|
||||
case 'arrowright':
|
||||
Renderer.camera.x += 50;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Select a tool
|
||||
selectTool(tool) {
|
||||
this.currentTool = tool;
|
||||
Simulation.stopMining();
|
||||
game.ui.updateToolButtons(tool);
|
||||
}
|
||||
};
|
||||
631
js/renderer.js
Normal file
631
js/renderer.js
Normal file
@@ -0,0 +1,631 @@
|
||||
// Rendering System
|
||||
const Renderer = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
camera: { x: 0, y: 0, zoom: 1 },
|
||||
|
||||
// Initialize renderer
|
||||
init() {
|
||||
this.canvas = document.getElementById('game-canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
},
|
||||
|
||||
// Resize canvas to container
|
||||
resize() {
|
||||
const container = this.canvas.parentElement;
|
||||
this.canvas.width = container.clientWidth;
|
||||
this.canvas.height = container.clientHeight;
|
||||
},
|
||||
|
||||
// Center camera on position
|
||||
centerCamera(x, y) {
|
||||
this.camera.x = x * CONFIG.TILE_SIZE * this.camera.zoom - this.canvas.width / 2;
|
||||
this.camera.y = y * CONFIG.TILE_SIZE * this.camera.zoom - this.canvas.height / 2;
|
||||
},
|
||||
|
||||
// Main render function
|
||||
render(currentTool, rotation, mouseWorld) {
|
||||
this.clear();
|
||||
this.drawTerrain();
|
||||
this.drawBuildings();
|
||||
this.drawTowers();
|
||||
this.drawEnemies();
|
||||
this.drawProjectiles();
|
||||
this.drawMiningProgress();
|
||||
this.drawPlacementPreview(currentTool, rotation, mouseWorld);
|
||||
this.drawDeletePreview(currentTool, mouseWorld);
|
||||
},
|
||||
|
||||
// Clear canvas
|
||||
clear() {
|
||||
this.ctx.fillStyle = '#1a2a1a';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
},
|
||||
|
||||
// Draw terrain and resources
|
||||
drawTerrain() {
|
||||
const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
const startX = Math.floor(this.camera.x / tileScreenSize);
|
||||
const startY = Math.floor(this.camera.y / tileScreenSize);
|
||||
const endX = startX + Math.ceil(this.canvas.width / tileScreenSize) + 1;
|
||||
const endY = startY + Math.ceil(this.canvas.height / tileScreenSize) + 1;
|
||||
|
||||
for (let y = startY; y < endY; y++) {
|
||||
for (let x = startX; x < endX; x++) {
|
||||
if (!Utils.inBounds(x, y)) continue;
|
||||
|
||||
const tile = Terrain.getTile(x, y);
|
||||
const screen = Utils.worldToScreen(x, y, this.camera);
|
||||
const size = tileScreenSize;
|
||||
|
||||
// Grass with variation
|
||||
const grassVariant = ((x * 7 + y * 13) % 3);
|
||||
const grassColors = ['#2d4a2d', '#3a5a3a', '#335533'];
|
||||
this.ctx.fillStyle = grassColors[grassVariant];
|
||||
this.ctx.fillRect(screen.x, screen.y, size, size);
|
||||
|
||||
// Grid lines
|
||||
this.ctx.strokeStyle = 'rgba(0,0,0,0.2)';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.strokeRect(screen.x, screen.y, size, size);
|
||||
|
||||
// Draw resources
|
||||
if (tile.resource && tile.amount > 0) {
|
||||
this.drawResource(screen.x, screen.y, size, tile.resource, tile.amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Draw a resource tile
|
||||
drawResource(screenX, screenY, size, type, amount) {
|
||||
const padding = size * 0.1;
|
||||
const richness = Math.min(1, amount / 2000);
|
||||
const colors = CONFIG.RESOURCE_COLORS[type];
|
||||
|
||||
// Gradient fill
|
||||
const grad = this.ctx.createLinearGradient(screenX, screenY, screenX + size, screenY + size);
|
||||
grad.addColorStop(0, colors.light);
|
||||
grad.addColorStop(1, colors.dark);
|
||||
this.ctx.fillStyle = grad;
|
||||
|
||||
Utils.drawRoundedRect(this.ctx, screenX + padding, screenY + padding, size - padding * 2, size - padding * 2, 4 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
|
||||
// Richness highlight
|
||||
this.ctx.fillStyle = `rgba(255,255,255,${richness * 0.25})`;
|
||||
Utils.drawRoundedRect(this.ctx, screenX + padding, screenY + padding, size - padding * 2, size - padding * 2, 4 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
|
||||
// Texture dots
|
||||
this.ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const ox = screenX + size * 0.3 + (i % 2) * size * 0.4;
|
||||
const oy = screenY + size * 0.3 + Math.floor(i / 2) * size * 0.4;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(ox, oy, 2 * this.camera.zoom, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
},
|
||||
|
||||
// Draw all buildings
|
||||
drawBuildings() {
|
||||
const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
|
||||
Buildings.list.forEach(b => {
|
||||
const screen = Utils.worldToScreen(b.x, b.y, this.camera);
|
||||
const bSize = Utils.getBuildingSize(b.type);
|
||||
const w = bSize.w * tileScreenSize;
|
||||
const h = bSize.h * tileScreenSize;
|
||||
const pad = 3 * this.camera.zoom;
|
||||
|
||||
const style = CONFIG.BUILDING_STYLES[b.type];
|
||||
|
||||
// Shadow
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
Utils.drawRoundedRect(this.ctx, screen.x + pad + 3, screen.y + pad + 3, w - pad * 2, h - pad * 2, 6 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
|
||||
// Main body gradient
|
||||
const grad = this.ctx.createLinearGradient(screen.x, screen.y, screen.x + w, screen.y + h);
|
||||
grad.addColorStop(0, style.light);
|
||||
grad.addColorStop(1, style.dark);
|
||||
this.ctx.fillStyle = grad;
|
||||
Utils.drawRoundedRect(this.ctx, screen.x + pad, screen.y + pad, w - pad * 2, h - pad * 2, 6 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
|
||||
// Highlight
|
||||
this.ctx.fillStyle = 'rgba(255,255,255,0.15)';
|
||||
Utils.drawRoundedRect(this.ctx, screen.x + pad, screen.y + pad, w - pad * 2, (h - pad * 2) * 0.4, 6 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
|
||||
// Border
|
||||
this.ctx.strokeStyle = 'rgba(0,0,0,0.4)';
|
||||
this.ctx.lineWidth = 2 * this.camera.zoom;
|
||||
Utils.drawRoundedRect(this.ctx, screen.x + pad, screen.y + pad, w - pad * 2, h - pad * 2, 6 * this.camera.zoom);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Direction indicator
|
||||
if (['belt', 'inserter', 'miner'].includes(b.type)) {
|
||||
this.drawDirectionArrow(screen.x + w / 2, screen.y + h / 2, w, b.rotation);
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
if (b.progress > 0 && b.type !== 'belt') {
|
||||
this.drawProgressBar(screen.x, screen.y, w, h, pad, b.progress);
|
||||
}
|
||||
|
||||
// Belt items
|
||||
if (b.type === 'belt') {
|
||||
const totalItems = Buildings.getTotalInventory(b) + Buildings.getTotalOutput(b);
|
||||
if (totalItems > 0) {
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + w / 2 + 1, screen.y + h / 2 + 1, 5 * this.camera.zoom, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = '#ffcc44';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + w / 2, screen.y + h / 2, 5 * this.camera.zoom, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
// Assembler recipe icon
|
||||
if (b.type === 'assembler' && b.recipe) {
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
this.ctx.font = `bold ${16 * this.camera.zoom}px sans-serif`;
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.fillText(b.recipe === 'gear' ? '⚙' : '⚡', screen.x + w / 2 + 1, screen.y + h / 2 + 1);
|
||||
this.ctx.fillStyle = '#fff';
|
||||
this.ctx.fillText(b.recipe === 'gear' ? '⚙' : '⚡', screen.x + w / 2, screen.y + h / 2);
|
||||
}
|
||||
|
||||
// Chest contents
|
||||
if (b.type === 'chest') {
|
||||
const total = Buildings.getTotalOutput(b);
|
||||
if (total > 0) {
|
||||
this.ctx.fillStyle = '#fff';
|
||||
this.ctx.font = `bold ${10 * this.camera.zoom}px sans-serif`;
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.fillText(total > 99 ? '99+' : total, screen.x + w / 2, screen.y + h / 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Draw direction arrow
|
||||
drawDirectionArrow(cx, cy, size, rotation) {
|
||||
this.ctx.save();
|
||||
this.ctx.translate(cx, cy);
|
||||
this.ctx.rotate(rotation * Math.PI / 2);
|
||||
|
||||
// Shadow
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(size * 0.25 + 2, 2);
|
||||
this.ctx.lineTo(-size * 0.05 + 2, -size * 0.12 + 2);
|
||||
this.ctx.lineTo(-size * 0.05 + 2, size * 0.12 + 2);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fill();
|
||||
|
||||
// Arrow
|
||||
this.ctx.fillStyle = '#fff';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(size * 0.25, 0);
|
||||
this.ctx.lineTo(-size * 0.05, -size * 0.12);
|
||||
this.ctx.lineTo(-size * 0.05, size * 0.12);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.restore();
|
||||
},
|
||||
|
||||
// Draw progress bar
|
||||
drawProgressBar(x, y, w, h, pad, progress) {
|
||||
const barW = w - pad * 4;
|
||||
const barH = 6 * this.camera.zoom;
|
||||
const barX = x + pad * 2;
|
||||
const barY = y + h - pad * 2 - barH;
|
||||
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
Utils.drawRoundedRect(this.ctx, barX, barY, barW, barH, 2 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
|
||||
const progGrad = this.ctx.createLinearGradient(barX, barY, barX, barY + barH);
|
||||
progGrad.addColorStop(0, '#66ff66');
|
||||
progGrad.addColorStop(1, '#22aa22');
|
||||
this.ctx.fillStyle = progGrad;
|
||||
Utils.drawRoundedRect(this.ctx, barX + 1, barY + 1, (barW - 2) * progress, barH - 2, 2 * this.camera.zoom);
|
||||
this.ctx.fill();
|
||||
},
|
||||
|
||||
// Draw manual mining progress
|
||||
drawMiningProgress() {
|
||||
if (!Simulation.mining.active) return;
|
||||
|
||||
const screen = Utils.worldToScreen(Simulation.mining.x, Simulation.mining.y, this.camera);
|
||||
const size = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
|
||||
// Mining highlight
|
||||
this.ctx.fillStyle = 'rgba(136, 204, 68, 0.3)';
|
||||
this.ctx.fillRect(screen.x, screen.y, size, size);
|
||||
|
||||
// Progress ring
|
||||
this.ctx.strokeStyle = '#88cc44';
|
||||
this.ctx.lineWidth = 4 * this.camera.zoom;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(
|
||||
screen.x + size / 2,
|
||||
screen.y + size / 2,
|
||||
size / 3,
|
||||
-Math.PI / 2,
|
||||
-Math.PI / 2 + Simulation.mining.progress * Math.PI * 2
|
||||
);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Pickaxe icon
|
||||
this.ctx.fillStyle = '#fff';
|
||||
this.ctx.font = `${14 * this.camera.zoom}px sans-serif`;
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.fillText('⛏', screen.x + size / 2, screen.y + size / 2);
|
||||
},
|
||||
|
||||
// Draw placement preview
|
||||
drawPlacementPreview(currentTool, rotation, mouseWorld) {
|
||||
if (currentTool === 'select' || currentTool === 'delete' || currentTool === 'mine') return;
|
||||
|
||||
const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
const screen = Utils.worldToScreen(mouseWorld.x, mouseWorld.y, this.camera);
|
||||
|
||||
// Check if it's a tower
|
||||
if (Towers.TYPES[currentTool]) {
|
||||
const towerType = Towers.TYPES[currentTool];
|
||||
const canPlace = Towers.canPlace(currentTool, mouseWorld.x, mouseWorld.y);
|
||||
|
||||
this.ctx.globalAlpha = 0.6;
|
||||
|
||||
// Tower preview circle
|
||||
this.ctx.fillStyle = canPlace ? 'rgba(100, 255, 100, 0.3)' : 'rgba(255, 100, 100, 0.3)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + tileScreenSize/2, screen.y + tileScreenSize/2, tileScreenSize/2, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.strokeStyle = canPlace ? '#66ff66' : '#ff6666';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.setLineDash([5, 5]);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + tileScreenSize/2, screen.y + tileScreenSize/2, tileScreenSize/2, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Range preview
|
||||
this.ctx.strokeStyle = canPlace ? 'rgba(100, 255, 100, 0.3)' : 'rgba(255, 100, 100, 0.3)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + tileScreenSize/2, screen.y + tileScreenSize/2, towerType.range * this.camera.zoom, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
|
||||
this.ctx.setLineDash([]);
|
||||
this.ctx.globalAlpha = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CONFIG.COSTS[currentTool]) return;
|
||||
|
||||
const previewSize = Utils.getBuildingSize(currentTool);
|
||||
const w = previewSize.w * tileScreenSize;
|
||||
const h = previewSize.h * tileScreenSize;
|
||||
|
||||
const canPlace = Buildings.canPlace(currentTool, mouseWorld.x, mouseWorld.y) &&
|
||||
Resources.canAfford(CONFIG.COSTS[currentTool]);
|
||||
|
||||
this.ctx.globalAlpha = 0.6;
|
||||
this.ctx.fillStyle = canPlace ? 'rgba(100, 255, 100, 0.3)' : 'rgba(255, 100, 100, 0.3)';
|
||||
this.ctx.fillRect(screen.x, screen.y, w, h);
|
||||
|
||||
this.ctx.strokeStyle = canPlace ? '#66ff66' : '#ff6666';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.setLineDash([5, 5]);
|
||||
this.ctx.strokeRect(screen.x, screen.y, w, h);
|
||||
this.ctx.setLineDash([]);
|
||||
|
||||
// Rotation arrow preview
|
||||
if (['belt', 'inserter', 'miner'].includes(currentTool)) {
|
||||
this.ctx.save();
|
||||
this.ctx.translate(screen.x + w / 2, screen.y + h / 2);
|
||||
this.ctx.rotate(rotation * Math.PI / 2);
|
||||
this.ctx.fillStyle = canPlace ? '#66ff66' : '#ff6666';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(w * 0.3, 0);
|
||||
this.ctx.lineTo(-w * 0.1, -w * 0.15);
|
||||
this.ctx.lineTo(-w * 0.1, w * 0.15);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fill();
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
this.ctx.globalAlpha = 1;
|
||||
},
|
||||
|
||||
// Draw delete preview
|
||||
drawDeletePreview(currentTool, mouseWorld) {
|
||||
if (currentTool !== 'delete') return;
|
||||
|
||||
const building = Buildings.getAt(mouseWorld.x, mouseWorld.y);
|
||||
const tower = Towers.getAt(mouseWorld.x, mouseWorld.y);
|
||||
|
||||
if (building) {
|
||||
const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
const screen = Utils.worldToScreen(building.x, building.y, this.camera);
|
||||
const bSize = Utils.getBuildingSize(building.type);
|
||||
const w = bSize.w * tileScreenSize;
|
||||
const h = bSize.h * tileScreenSize;
|
||||
|
||||
this.drawDeleteBox(screen.x, screen.y, w, h);
|
||||
} else if (tower) {
|
||||
const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
const screen = Utils.worldToScreen(tower.x, tower.y, this.camera);
|
||||
|
||||
this.drawDeleteBox(screen.x, screen.y, tileScreenSize, tileScreenSize);
|
||||
}
|
||||
},
|
||||
|
||||
// Draw delete box helper
|
||||
drawDeleteBox(x, y, w, h) {
|
||||
this.ctx.fillStyle = 'rgba(255, 50, 50, 0.4)';
|
||||
this.ctx.fillRect(x, y, w, h);
|
||||
this.ctx.strokeStyle = '#ff3333';
|
||||
this.ctx.lineWidth = 3;
|
||||
this.ctx.strokeRect(x, y, w, h);
|
||||
|
||||
// X mark
|
||||
this.ctx.strokeStyle = '#ff3333';
|
||||
this.ctx.lineWidth = 4 * this.camera.zoom;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(x + w * 0.2, y + h * 0.2);
|
||||
this.ctx.lineTo(x + w * 0.8, y + h * 0.8);
|
||||
this.ctx.moveTo(x + w * 0.8, y + h * 0.2);
|
||||
this.ctx.lineTo(x + w * 0.2, y + h * 0.8);
|
||||
this.ctx.stroke();
|
||||
},
|
||||
|
||||
// Draw all towers
|
||||
drawTowers() {
|
||||
const tileScreenSize = CONFIG.TILE_SIZE * this.camera.zoom;
|
||||
|
||||
Towers.list.forEach(tower => {
|
||||
const type = Towers.TYPES[tower.type];
|
||||
const screen = Utils.worldToScreen(tower.x, tower.y, this.camera);
|
||||
const size = tileScreenSize;
|
||||
const pad = 3 * this.camera.zoom;
|
||||
|
||||
// Shadow
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + size/2 + 3, screen.y + size/2 + 3, size/2 - pad, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Base
|
||||
const grad = this.ctx.createRadialGradient(
|
||||
screen.x + size/2, screen.y + size/2, 0,
|
||||
screen.x + size/2, screen.y + size/2, size/2
|
||||
);
|
||||
grad.addColorStop(0, type.color);
|
||||
grad.addColorStop(1, type.colorDark);
|
||||
this.ctx.fillStyle = grad;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + size/2, screen.y + size/2, size/2 - pad, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Highlight
|
||||
this.ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + size/2, screen.y + size/3, size/4, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Border
|
||||
this.ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
||||
this.ctx.lineWidth = 2 * this.camera.zoom;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + size/2, screen.y + size/2, size/2 - pad, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
|
||||
// Turret barrel
|
||||
this.ctx.save();
|
||||
this.ctx.translate(screen.x + size/2, screen.y + size/2);
|
||||
this.ctx.rotate(tower.angle);
|
||||
|
||||
// Barrel
|
||||
this.ctx.fillStyle = tower.firing ? '#fff' : '#333';
|
||||
this.ctx.fillRect(0, -4 * this.camera.zoom, size/2.5, 8 * this.camera.zoom);
|
||||
|
||||
this.ctx.restore();
|
||||
|
||||
// Range indicator when selected
|
||||
if (tower.target) {
|
||||
this.ctx.strokeStyle = 'rgba(255,100,100,0.2)';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x + size/2, screen.y + size/2, type.range * this.camera.zoom, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// Health bar
|
||||
if (tower.health < tower.maxHealth) {
|
||||
const barW = size - pad * 2;
|
||||
const barH = 4 * this.camera.zoom;
|
||||
const barX = screen.x + pad;
|
||||
const barY = screen.y - barH - 4;
|
||||
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
this.ctx.fillRect(barX, barY, barW, barH);
|
||||
|
||||
const healthPct = tower.health / tower.maxHealth;
|
||||
this.ctx.fillStyle = healthPct > 0.5 ? '#66ff66' : healthPct > 0.25 ? '#ffcc00' : '#ff3333';
|
||||
this.ctx.fillRect(barX, barY, barW * healthPct, barH);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Draw all enemies
|
||||
drawEnemies() {
|
||||
Enemies.list.forEach(enemy => {
|
||||
const type = Enemies.TYPES[enemy.type];
|
||||
const screen = {
|
||||
x: enemy.x * this.camera.zoom - this.camera.x,
|
||||
y: enemy.y * this.camera.zoom - this.camera.y
|
||||
};
|
||||
const size = CONFIG.TILE_SIZE * this.camera.zoom * type.size;
|
||||
|
||||
// Shadow
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(screen.x + 2, screen.y + 4, size/2, size/3, 0, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Body
|
||||
this.ctx.save();
|
||||
this.ctx.translate(screen.x, screen.y);
|
||||
this.ctx.rotate(enemy.angle);
|
||||
|
||||
// Damage flash
|
||||
const color = enemy.damageFlash > 0 ? '#fff' : type.color;
|
||||
if (enemy.damageFlash > 0) enemy.damageFlash -= 0.016;
|
||||
|
||||
// Main body
|
||||
const grad = this.ctx.createRadialGradient(0, 0, 0, 0, 0, size/2);
|
||||
grad.addColorStop(0, color);
|
||||
grad.addColorStop(1, type.colorDark);
|
||||
this.ctx.fillStyle = grad;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.ellipse(0, 0, size/2, size/3, 0, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Eyes
|
||||
this.ctx.fillStyle = '#ff0000';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(size/4, -size/8, size/10, 0, Math.PI * 2);
|
||||
this.ctx.arc(size/4, size/8, size/10, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Legs animation
|
||||
this.ctx.strokeStyle = type.colorDark;
|
||||
this.ctx.lineWidth = 2 * this.camera.zoom;
|
||||
const legOffset = Math.sin(enemy.animTime) * 5;
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const angle = (i - 1) * 0.5;
|
||||
const offset = i === 1 ? legOffset : -legOffset;
|
||||
|
||||
// Left leg
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(-size/6, -size/4);
|
||||
this.ctx.lineTo(-size/3 + offset, -size/2 - Math.abs(offset));
|
||||
this.ctx.stroke();
|
||||
|
||||
// Right leg
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(-size/6, size/4);
|
||||
this.ctx.lineTo(-size/3 - offset, size/2 + Math.abs(offset));
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
this.ctx.restore();
|
||||
|
||||
// Health bar
|
||||
if (enemy.health < enemy.maxHealth) {
|
||||
const barW = size;
|
||||
const barH = 4 * this.camera.zoom;
|
||||
const barX = screen.x - size/2;
|
||||
const barY = screen.y - size/2 - barH - 4;
|
||||
|
||||
this.ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
this.ctx.fillRect(barX, barY, barW, barH);
|
||||
|
||||
const healthPct = enemy.health / enemy.maxHealth;
|
||||
this.ctx.fillStyle = healthPct > 0.5 ? '#66ff66' : healthPct > 0.25 ? '#ffcc00' : '#ff3333';
|
||||
this.ctx.fillRect(barX, barY, barW * healthPct, barH);
|
||||
}
|
||||
|
||||
// Burning effect
|
||||
if (enemy.burning && enemy.burning.duration > 0) {
|
||||
this.ctx.fillStyle = `rgba(255, 100, 0, ${0.3 + Math.sin(Date.now() / 100) * 0.2})`;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x, screen.y, size/2, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Draw projectiles
|
||||
drawProjectiles() {
|
||||
Towers.projectiles.forEach(proj => {
|
||||
const screen = {
|
||||
x: proj.x * this.camera.zoom - this.camera.x,
|
||||
y: proj.y * this.camera.zoom - this.camera.y
|
||||
};
|
||||
|
||||
// Projectile glow
|
||||
this.ctx.fillStyle = proj.color;
|
||||
this.ctx.shadowColor = proj.color;
|
||||
this.ctx.shadowBlur = 10;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(screen.x, screen.y, 4 * this.camera.zoom, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
this.ctx.shadowBlur = 0;
|
||||
|
||||
// Trail
|
||||
const dx = proj.targetX - proj.x;
|
||||
const dy = proj.targetY - proj.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist > 0) {
|
||||
const trailX = screen.x - (dx / dist) * 15 * this.camera.zoom;
|
||||
const trailY = screen.y - (dy / dist) * 15 * this.camera.zoom;
|
||||
|
||||
this.ctx.strokeStyle = proj.color;
|
||||
this.ctx.lineWidth = 2 * this.camera.zoom;
|
||||
this.ctx.globalAlpha = 0.5;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(screen.x, screen.y);
|
||||
this.ctx.lineTo(trailX, trailY);
|
||||
this.ctx.stroke();
|
||||
this.ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Chain lightning effect
|
||||
if (proj.chainTargets && proj.chainTargets.length > 1) {
|
||||
this.ctx.strokeStyle = proj.color;
|
||||
this.ctx.lineWidth = 2 * this.camera.zoom;
|
||||
this.ctx.globalAlpha = 0.8;
|
||||
|
||||
for (let i = 0; i < proj.chainTargets.length - 1; i++) {
|
||||
const t1 = proj.chainTargets[i];
|
||||
const t2 = proj.chainTargets[i + 1];
|
||||
const s1 = {
|
||||
x: t1.x * this.camera.zoom - this.camera.x,
|
||||
y: t1.y * this.camera.zoom - this.camera.y
|
||||
};
|
||||
const s2 = {
|
||||
x: t2.x * this.camera.zoom - this.camera.x,
|
||||
y: t2.y * this.camera.zoom - this.camera.y
|
||||
};
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(s1.x, s1.y);
|
||||
// Jagged line for lightning effect
|
||||
const midX = (s1.x + s2.x) / 2 + (Math.random() - 0.5) * 20;
|
||||
const midY = (s1.y + s2.y) / 2 + (Math.random() - 0.5) * 20;
|
||||
this.ctx.lineTo(midX, midY);
|
||||
this.ctx.lineTo(s2.x, s2.y);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
this.ctx.globalAlpha = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
66
js/resources.js
Normal file
66
js/resources.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// Resource Management
|
||||
const Resources = {
|
||||
// Current resource counts
|
||||
inventory: {},
|
||||
|
||||
// Initialize resources
|
||||
init() {
|
||||
this.inventory = Utils.deepClone(CONFIG.STARTING_RESOURCES);
|
||||
},
|
||||
|
||||
// Get resource count
|
||||
get(type) {
|
||||
return this.inventory[type] || 0;
|
||||
},
|
||||
|
||||
// Add resources
|
||||
add(type, amount) {
|
||||
this.inventory[type] = (this.inventory[type] || 0) + amount;
|
||||
},
|
||||
|
||||
// Remove resources (returns false if not enough)
|
||||
remove(type, amount) {
|
||||
if (this.get(type) < amount) return false;
|
||||
this.inventory[type] -= amount;
|
||||
return true;
|
||||
},
|
||||
|
||||
// Check if player can afford a cost
|
||||
canAfford(costObj) {
|
||||
if (CONFIG.DEV_MODE) return true;
|
||||
if (!costObj) return true;
|
||||
for (const [res, amount] of Object.entries(costObj)) {
|
||||
if (this.get(res) < amount) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// Pay a cost (deduct resources)
|
||||
payCost(costObj) {
|
||||
if (CONFIG.DEV_MODE) return true;
|
||||
if (!costObj) return true;
|
||||
if (!this.canAfford(costObj)) return false;
|
||||
for (const [res, amount] of Object.entries(costObj)) {
|
||||
this.remove(res, amount);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// Refund half cost (for demolishing)
|
||||
refundHalf(costObj) {
|
||||
if (!costObj) return;
|
||||
for (const [res, amount] of Object.entries(costObj)) {
|
||||
this.add(res, Math.floor(amount / 2));
|
||||
}
|
||||
},
|
||||
|
||||
// Get all resources as object
|
||||
getAll() {
|
||||
return Utils.deepClone(this.inventory);
|
||||
},
|
||||
|
||||
// Set all resources from object
|
||||
setAll(data) {
|
||||
this.inventory = Utils.deepClone(data);
|
||||
}
|
||||
};
|
||||
116
js/saveload.js
Normal file
116
js/saveload.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Save/Load System
|
||||
const SaveLoad = {
|
||||
STORAGE_KEY: 'minifactory_saves',
|
||||
|
||||
// Get all saves
|
||||
getSaves() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '{}');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
// Set all saves
|
||||
setSaves(saves) {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(saves));
|
||||
},
|
||||
|
||||
// Save game to slot
|
||||
saveGame(slotId) {
|
||||
const saves = this.getSaves();
|
||||
saves[slotId] = {
|
||||
date: new Date().toISOString(),
|
||||
resources: Resources.getAll(),
|
||||
terrain: Terrain.getData(),
|
||||
buildings: Buildings.getData(),
|
||||
towers: Towers.getData(),
|
||||
enemies: Enemies.getData(),
|
||||
camera: { ...Renderer.camera },
|
||||
simulation: Simulation.getData()
|
||||
};
|
||||
this.setSaves(saves);
|
||||
},
|
||||
|
||||
// Load game from slot
|
||||
loadGame(slotId) {
|
||||
const saves = this.getSaves();
|
||||
const save = saves[slotId];
|
||||
if (!save) return false;
|
||||
|
||||
Resources.setAll(save.resources);
|
||||
Terrain.setData(save.terrain);
|
||||
Buildings.setData(save.buildings);
|
||||
if (save.towers) Towers.setData(save.towers);
|
||||
if (save.enemies) Enemies.setData(save.enemies);
|
||||
Renderer.camera = { ...save.camera };
|
||||
if (save.simulation) {
|
||||
Simulation.setData(save.simulation);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// Delete a save
|
||||
deleteSave(slotId) {
|
||||
const saves = this.getSaves();
|
||||
delete saves[slotId];
|
||||
this.setSaves(saves);
|
||||
},
|
||||
|
||||
// Save to new slot
|
||||
saveToNewSlot() {
|
||||
const slotId = 'save_' + Date.now();
|
||||
this.saveGame(slotId);
|
||||
},
|
||||
|
||||
// Update save slots UI
|
||||
updateSaveSlots() {
|
||||
const container = document.getElementById('save-slots');
|
||||
const saves = this.getSaves();
|
||||
const slots = Object.entries(saves);
|
||||
|
||||
if (slots.length === 0) {
|
||||
container.innerHTML = '<div class="no-saves">No saved games</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = slots.map(([id, save]) => `
|
||||
<div class="save-slot">
|
||||
<div class="save-slot-info">
|
||||
<div class="save-slot-name">Save ${new Date(save.date).toLocaleDateString()}</div>
|
||||
<div class="save-slot-date">${new Date(save.date).toLocaleTimeString()}</div>
|
||||
</div>
|
||||
<div class="save-slot-actions">
|
||||
<button class="save-slot-btn" onclick="SaveLoad.saveGame('${id}'); game.ui.closeModal('save-modal');">Overwrite</button>
|
||||
<button class="save-slot-btn delete" onclick="SaveLoad.deleteSave('${id}'); SaveLoad.updateSaveSlots();">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
},
|
||||
|
||||
// Update load slots UI
|
||||
updateLoadSlots() {
|
||||
const container = document.getElementById('load-slots');
|
||||
const saves = this.getSaves();
|
||||
const slots = Object.entries(saves);
|
||||
|
||||
if (slots.length === 0) {
|
||||
container.innerHTML = '<div class="no-saves">No saved games</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = slots.map(([id, save]) => `
|
||||
<div class="save-slot">
|
||||
<div class="save-slot-info">
|
||||
<div class="save-slot-name">Save ${new Date(save.date).toLocaleDateString()}</div>
|
||||
<div class="save-slot-date">${new Date(save.date).toLocaleTimeString()}</div>
|
||||
</div>
|
||||
<div class="save-slot-actions">
|
||||
<button class="save-slot-btn" onclick="SaveLoad.loadGame('${id}'); game.ui.closeModal('load-modal');">Load</button>
|
||||
<button class="save-slot-btn delete" onclick="SaveLoad.deleteSave('${id}'); SaveLoad.updateLoadSlots();">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
};
|
||||
279
js/simulation.js
Normal file
279
js/simulation.js
Normal file
@@ -0,0 +1,279 @@
|
||||
// Game Simulation / Logic
|
||||
const Simulation = {
|
||||
gameTime: 0,
|
||||
gameSpeed: 1,
|
||||
|
||||
// Manual mining state
|
||||
mining: {
|
||||
active: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
progress: 0
|
||||
},
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
this.gameTime = 0;
|
||||
this.gameSpeed = 1;
|
||||
this.mining = { active: false, x: 0, y: 0, progress: 0 };
|
||||
},
|
||||
|
||||
// Main update function
|
||||
update(dt) {
|
||||
dt *= this.gameSpeed;
|
||||
this.gameTime += dt;
|
||||
|
||||
this.updateManualMining(dt);
|
||||
this.updateMiners(dt);
|
||||
this.updateFurnaces(dt);
|
||||
this.updateAssemblers(dt);
|
||||
this.updateInserters(dt);
|
||||
this.updateBelts(dt);
|
||||
this.updateChests(dt);
|
||||
},
|
||||
|
||||
// Manual mining update
|
||||
updateManualMining(dt) {
|
||||
if (!this.mining.active) return;
|
||||
|
||||
const tile = Terrain.getTile(this.mining.x, this.mining.y);
|
||||
if (!tile || !tile.resource || tile.amount <= 0) {
|
||||
this.mining.active = false;
|
||||
this.mining.progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mining.progress += dt / CONFIG.MANUAL_MINE_RATE;
|
||||
|
||||
// Play mine sound periodically
|
||||
this.mining.soundTimer = (this.mining.soundTimer || 0) + dt;
|
||||
if (this.mining.soundTimer > 0.3) {
|
||||
this.mining.soundTimer = 0;
|
||||
Audio.playMine();
|
||||
}
|
||||
|
||||
if (this.mining.progress >= 1) {
|
||||
this.mining.progress = 0;
|
||||
const result = Terrain.mine(this.mining.x, this.mining.y, CONFIG.MANUAL_MINE_AMOUNT);
|
||||
if (result) {
|
||||
Resources.add(result.type, result.amount);
|
||||
Audio.playMineComplete();
|
||||
}
|
||||
|
||||
// Check if resource depleted
|
||||
if (!Terrain.hasResource(this.mining.x, this.mining.y)) {
|
||||
this.mining.active = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Start manual mining at position
|
||||
startMining(x, y) {
|
||||
if (Terrain.hasResource(x, y) && !Buildings.getAt(x, y)) {
|
||||
this.mining.active = true;
|
||||
this.mining.x = x;
|
||||
this.mining.y = y;
|
||||
this.mining.progress = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// Stop manual mining
|
||||
stopMining() {
|
||||
this.mining.active = false;
|
||||
this.mining.progress = 0;
|
||||
},
|
||||
|
||||
// Update miners
|
||||
updateMiners(dt) {
|
||||
Buildings.getByType('miner').forEach(miner => {
|
||||
const size = Utils.getBuildingSize('miner');
|
||||
let resourceType = null;
|
||||
let hasResource = false;
|
||||
|
||||
// Find resource under miner
|
||||
for (let dy = 0; dy < size.h && !hasResource; dy++) {
|
||||
for (let dx = 0; dx < size.w && !hasResource; dx++) {
|
||||
const tile = Terrain.getTile(miner.x + dx, miner.y + dy);
|
||||
if (tile?.resource && tile.amount > 0) {
|
||||
resourceType = tile.resource;
|
||||
hasResource = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasResource && resourceType) {
|
||||
miner.progress += dt * CONFIG.SPEEDS.miner;
|
||||
if (miner.progress >= 1) {
|
||||
miner.progress = 0;
|
||||
Buildings.addToOutput(miner, resourceType);
|
||||
|
||||
// Deplete resource
|
||||
for (let dy = 0; dy < size.h; dy++) {
|
||||
for (let dx = 0; dx < size.w; dx++) {
|
||||
const result = Terrain.mine(miner.x + dx, miner.y + dy, 1);
|
||||
if (result) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update furnaces
|
||||
updateFurnaces(dt) {
|
||||
Buildings.getByType('furnace').forEach(furnace => {
|
||||
const hasCoal = Buildings.getInventoryCount(furnace, 'coal') > 0;
|
||||
const hasIron = Buildings.getInventoryCount(furnace, 'iron') > 0;
|
||||
const hasCopper = Buildings.getInventoryCount(furnace, 'copper') > 0;
|
||||
|
||||
if (hasCoal && (hasIron || hasCopper)) {
|
||||
furnace.progress += dt * CONFIG.SPEEDS.furnace;
|
||||
if (furnace.progress >= 1) {
|
||||
furnace.progress = 0;
|
||||
Buildings.removeFromInventory(furnace, 'coal');
|
||||
|
||||
if (hasIron) {
|
||||
Buildings.removeFromInventory(furnace, 'iron');
|
||||
Buildings.addToOutput(furnace, 'iron-plate');
|
||||
} else if (hasCopper) {
|
||||
Buildings.removeFromInventory(furnace, 'copper');
|
||||
Buildings.addToOutput(furnace, 'copper-plate');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update assemblers
|
||||
updateAssemblers(dt) {
|
||||
Buildings.getByType('assembler').forEach(asm => {
|
||||
if (!asm.recipe) return;
|
||||
|
||||
const recipe = CONFIG.RECIPES[asm.recipe];
|
||||
if (!recipe) return;
|
||||
|
||||
// Check if can craft
|
||||
let canCraft = true;
|
||||
for (const [item, amount] of Object.entries(recipe.inputs)) {
|
||||
if (Buildings.getInventoryCount(asm, item) < amount) {
|
||||
canCraft = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canCraft) {
|
||||
asm.progress += dt * CONFIG.SPEEDS.assembler;
|
||||
if (asm.progress >= 1) {
|
||||
asm.progress = 0;
|
||||
|
||||
// Consume inputs
|
||||
for (const [item, amount] of Object.entries(recipe.inputs)) {
|
||||
Buildings.removeFromInventory(asm, item, amount);
|
||||
}
|
||||
|
||||
// Produce output
|
||||
Buildings.addToOutput(asm, recipe.output, recipe.outputCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update inserters
|
||||
updateInserters(dt) {
|
||||
Buildings.getByType('inserter').forEach(ins => {
|
||||
const dir = CONFIG.DIR[ins.rotation];
|
||||
const pickupX = ins.x - dir.x;
|
||||
const pickupY = ins.y - dir.y;
|
||||
const dropX = ins.x + dir.x;
|
||||
const dropY = ins.y + dir.y;
|
||||
|
||||
const source = Buildings.getAt(pickupX, pickupY);
|
||||
const dest = Buildings.getAt(dropX, dropY);
|
||||
|
||||
if (!source) return;
|
||||
|
||||
// Find item to transfer from source output
|
||||
for (const [item, count] of Object.entries(source.output || {})) {
|
||||
if (count > 0) {
|
||||
ins.progress += dt * CONFIG.SPEEDS.inserter;
|
||||
if (ins.progress >= 1) {
|
||||
ins.progress = 0;
|
||||
Buildings.removeFromOutput(source, item);
|
||||
|
||||
if (dest) {
|
||||
// Drop into building
|
||||
Buildings.addToInventory(dest, item);
|
||||
} else if (source.type === 'chest') {
|
||||
// Only chests can output to player inventory
|
||||
Resources.add(item, 1);
|
||||
}
|
||||
// If no dest and not chest, item is lost
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update belts
|
||||
updateBelts(dt) {
|
||||
Buildings.getByType('belt').forEach(belt => {
|
||||
const dir = CONFIG.DIR[belt.rotation];
|
||||
const nextX = belt.x + dir.x;
|
||||
const nextY = belt.y + dir.y;
|
||||
const nextBuilding = Buildings.getAt(nextX, nextY);
|
||||
|
||||
// Move items from output to next building
|
||||
for (const [item, count] of Object.entries(belt.output || {})) {
|
||||
if (count > 0 && nextBuilding) {
|
||||
Buildings.removeFromOutput(belt, item);
|
||||
Buildings.addToInventory(nextBuilding, item);
|
||||
}
|
||||
}
|
||||
|
||||
// Move items through belt (inventory -> output)
|
||||
belt.progress = (belt.progress || 0) + dt * CONFIG.SPEEDS.belt;
|
||||
if (belt.progress >= 1) {
|
||||
belt.progress = 0;
|
||||
for (const [item, count] of Object.entries(belt.inventory || {})) {
|
||||
if (count > 0) {
|
||||
Buildings.removeFromInventory(belt, item);
|
||||
Buildings.addToOutput(belt, item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Update chests (move inventory to output for inserters)
|
||||
updateChests(dt) {
|
||||
Buildings.getByType('chest').forEach(chest => {
|
||||
for (const [item, count] of Object.entries(chest.inventory)) {
|
||||
if (count > 0) {
|
||||
chest.output[item] = (chest.output[item] || 0) + count;
|
||||
chest.inventory[item] = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Set game speed
|
||||
setSpeed(speed) {
|
||||
this.gameSpeed = speed;
|
||||
},
|
||||
|
||||
// Get state for saving
|
||||
getData() {
|
||||
return {
|
||||
gameTime: this.gameTime
|
||||
};
|
||||
},
|
||||
|
||||
// Load state
|
||||
setData(data) {
|
||||
this.gameTime = data.gameTime || 0;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user