Train Horde Survival Engine
by AstroGalaxy64556 lines16.7 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Train Horde Survival Engine</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
}
body {
background-color: #111;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
#game-container {
position: relative;
width: 1000px;
height: 600px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.8);
border: 2px solid #333;
}
canvas {
background-color: #1a1a1a;
display: block;
}
#ui-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
}
.hud-panel {
background: rgba(0, 0, 0, 0.65);
padding: 15px;
border-radius: 8px;
border: 1px solid #444;
backdrop-filter: blur(4px);
display: inline-block;
}
.hud-title {
font-size: 14px;
text-transform: uppercase;
color: #888;
letter-spacing: 1px;
margin-bottom: 5px;
}
.hud-value {
font-size: 20px;
font-weight: bold;
color: #00ffcc;
}
.stat-bar {
width: 200px;
height: 12px;
background: #333;
border-radius: 6px;
overflow: hidden;
margin-top: 5px;
border: 1px solid #555;
}
.stat-fill {
height: 100%;
width: 100%;
transition: width 0.1s linear;
}
#fuel-fill { background: #ffaa00; }
#wall-fill { background: #00ccff; }
#state-display {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
#state-name {
font-size: 28px;
font-weight: 800;
letter-spacing: 2px;
color: #fff;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
#state-timer {
font-size: 18px;
color: #ffcc00;
margin-top: 4px;
}
#prompt-ui {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 200, 0, 0.9);
color: #000;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
display: none;
box-shadow: 0 0 10px rgba(255, 200, 0, 0.5);
}
#controls-hint {
position: absolute;
bottom: 20px;
left: 20px;
font-size: 12px;
color: #aaa;
background: rgba(0,0,0,0.5);
padding: 8px;
border-radius: 4px;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="1000" height="600"></canvas>
<div id="ui-overlay">
<div style="display: flex; justify-content: space-between;">
<div class="hud-panel">
<div class="hud-title">Train Status</div>
<div style="font-size: 13px; margin-top: 3px;">Fuel:</div>
<div class="stat-bar"><div id="fuel-fill" class="stat-fill"></div></div>
<div style="font-size: 13px; margin-top: 6px;">Barricade HP:</div>
<div class="stat-bar"><div id="wall-fill" class="stat-fill"></div></div>
</div>
<div id="state-display" class="hud-panel">
<div id="state-name">LOBBY</div>
<div id="state-timer">Starting in 15s</div>
</div>
<div class="hud-panel" style="text-align: right;">
<div class="hud-title">Inventory</div>
<div>Coal: <span id="coal-count" class="hud-value">3</span></div>
<div>Scrap Metal: <span id="scrap-count" class="hud-value">3</span></div>
</div>
</div>
<div id="prompt-ui">Press [E] to Interact</div>
<div id="controls-hint">
<b>Controls:</b> W/A/S/D - Move | Mouse - Aim & Shoot | E - Interact (Furnace/Wall)
</div>
</div>
</div>
<script>
// --- CORE ENGINE CONFIGURATION ---
const CANVAS_WIDTH = 1000;
const CANVAS_HEIGHT = 600;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// --- GAME STATE MANAGER ---
const States = {
LOBBY: "LOBBY",
TRACK_RUN: "TRACK RUN",
STATION_REST: "STATION REST",
BOSS_HAZARD: "BOSS HAZARD",
GAME_OVER: "GAME OVER"
};
let currentState = States.LOBBY;
let stateTimer = 15;
let currentRunCount = 0;
// --- TRAIN & TRACK PHYSICS SYSTEM ---
const train = {
x: CANVAS_WIDTH / 2 - 200,
y: CANVAS_HEIGHT / 2 - 30,
width: 400,
height: 60,
speed: 0,
targetSpeed: 0,
maxSpeed: 8,
fuel: 100,
maxFuel: 100,
wallHealth: 100,
maxWallHealth: 100,
isAnchored: true
};
// Track generation variables
let trackOffset = 0;
// --- PLAYER SYSTEM ---
const player = {
x: train.x + 50,
y: train.y + 20,
radius: 12,
speed: 4,
vx: 0,
vy: 0,
coal: 3,
scrap: 3,
isOnTrain: true
};
const keys = {};
const mouse = { x: 0, y: 0, isDown: false };
// --- INTERACTION OBJECTS ---
const furnace = { x: train.x + 20, y: train.y + 10, width: 40, height: 40 };
const barricade = { x: train.x + train.width - 20, y: train.y, width: 15, height: train.height };
// --- HORDE AI SYSTEM ---
let zombies = [];
const BATCH_SIZE = 10;
let currentBatchIndex = 0;
class Zombie {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 10;
this.speed = 2.5 + Math.random() * 1;
this.hp = 50;
this.isBoarded = false;
this.relativeX = 0;
this.relativeY = 0;
}
update(train) {
if (this.hp <= 0) return;
if (!this.isBoarded) {
// Predict Lead Target Position (Train Intercept)
let targetX = train.x + train.width / 2;
let targetY = train.y + train.height / 2;
let dx = targetX - this.x;
let dy = targetY - this.y;
let dist = Math.hypot(dx, dy);
if (dist < 30 && train.wallHealth > 0) {
// Damage Wall
train.wallHealth = Math.max(0, train.wallHealth - 0.05);
} else if (dist < 30 && train.wallHealth <= 0) {
// Board Train
this.isBoarded = true;
this.relativeX = this.x - train.x;
this.relativeY = this.y - train.y;
} else {
// Move Towards Train
this.x += (dx / dist) * this.speed;
this.y += (dy / dist) * this.speed;
}
} else {
// Keep locked to relative local CFrame on train
this.x = train.x + this.relativeX;
this.y = train.y + this.relativeY;
// Attack Player locally
let dx = player.x - this.x;
let dy = player.y - this.y;
let dist = Math.hypot(dx, dy);
if (dist > 5) {
this.relativeX += (dx / dist) * 0.8;
this.relativeY += (dy / dist) * 0.8;
}
}
}
}
// --- COMBAT & RAYCAST SYSTEM ---
let bullets = [];
function fireWeapon(targetX, targetY) {
const originX = player.x;
const originY = player.y;
const angle = Math.atan2(targetY - originY, targetX - originX);
bullets.push({
x: originX,
y: originY,
vx: Math.cos(angle) * 15,
vy: Math.sin(angle) * 15,
life: 40
});
// Server-Style Raycast Hit Verification against Zombies
zombies.forEach(zombie => {
let dx = zombie.x - originX;
let dy = zombie.y - originY;
let dist = Math.hypot(dx, dy);
// Check line-of-sight proximity to beam trajectory
let dot = (dx * Math.cos(angle) + dy * Math.sin(angle));
if (dot > 0 && dot < 400) {
let projX = originX + Math.cos(angle) * dot;
let projY = originY + Math.sin(angle) * dot;
let perpDist = Math.hypot(zombie.x - projX, zombie.y - projY);
if (perpDist < zombie.radius + 5) {
zombie.hp -= 25; // Hit registered
}
}
});
}
// --- INPUT LISTENERS ---
window.addEventListener('keydown', e => keys[e.key.toLowerCase()] = true);
window.addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
canvas.addEventListener('mousedown', e => {
if (e.button === 0) fireWeapon(mouse.x, mouse.y);
});
// --- GAME LOOP & STATE MACHINES ---
let lastTime = performance.now();
function updateGameState(dt) {
stateTimer -= dt;
if (stateTimer <= 0) {
switch (currentState) {
case States.LOBBY:
currentState = States.TRACK_RUN;
stateTimer = 60;
currentRunCount++;
train.isAnchored = false;
break;
case States.TRACK_RUN:
if (currentRunCount % 3 === 0) {
currentState = States.BOSS_HAZARD;
stateTimer = 45;
} else {
currentState = States.STATION_REST;
stateTimer = 20;
train.isAnchored = true;
}
break;
case States.STATION_REST:
case States.BOSS_HAZARD:
currentState = States.TRACK_RUN;
stateTimer = 60;
currentRunCount++;
train.isAnchored = false;
break;
}
}
// Spawn Horde in Track Run & Boss phases
if ((currentState === States.TRACK_RUN || currentState === States.BOSS_HAZARD) && Math.random() < 0.05) {
if (zombies.length < 50) {
let spawnX = CANVAS_WIDTH + 20;
let spawnY = Math.random() * CANVAS_HEIGHT;
zombies.push(new Zombie(spawnX, spawnY));
}
}
}
function updatePhysics() {
// 1. Train Acceleration & Fuel Dynamics
if (!train.isAnchored && train.fuel > 0) {
train.targetSpeed = train.maxSpeed;
train.fuel = Math.max(0, train.fuel - 0.03);
} else {
train.targetSpeed = 0;
}
// Smooth Lerp Train Speed
train.speed += (train.targetSpeed - train.speed) * 0.05;
trackOffset = (trackOffset + train.speed) % 40;
// 2. Player Relative Platform Grounding System
player.vx = 0;
player.vy = 0;
if (keys['w']) player.vy -= player.speed;
if (keys['s']) player.vy += player.speed;
if (keys['a']) player.vx -= player.speed;
if (keys['d']) player.vx += player.speed;
player.x += player.vx;
player.y += player.vy;
// Check if player is on train bounding box
if (player.x >= train.x && player.x <= train.x + train.width &&
player.y >= train.y && player.y <= train.y + train.height) {
player.isOnTrain = true;
// Clamp inside train borders
player.x = Math.max(train.x + player.radius, Math.min(train.x + train.width - player.radius, player.x));
player.y = Math.max(train.y + player.radius, Math.min(train.y + train.height - player.radius, player.y));
} else {
player.isOnTrain = false;
}
// 3. Staggered Zombie Batch Updates (Performance Optimization)
let totalZombies = zombies.length;
if (totalZombies > 0) {
let endIndex = Math.min(currentBatchIndex + BATCH_SIZE, totalZombies);
for (let i = 0; i < totalZombies; i++) {
zombies[i].update(train); // Active simulation
}
currentBatchIndex = (endIndex >= totalZombies) ? 0 : endIndex;
}
// Cleanup Dead Zombies
zombies = zombies.filter(z => z.hp > 0);
// 4. Update Bullets
bullets.forEach(b => {
b.x += b.vx;
b.y += b.vy;
b.life--;
});
bullets = bullets.filter(b => b.life > 0);
// 5. Player Interaction System
const nearFurnace = Math.hypot(player.x - (furnace.x + 20), player.y - (furnace.y + 20)) < 40;
const nearWall = Math.hypot(player.x - barricade.x, player.y - (barricade.y + 30)) < 40;
const promptUI = document.getElementById('prompt-ui');
if (nearFurnace) {
promptUI.style.display = 'block';
promptUI.innerText = "Press [E] to Add Coal (+25 Fuel)";
if (keys['e']) {
if (player.coal > 0 && train.fuel < train.maxFuel) {
player.coal--;
train.fuel = Math.min(train.maxFuel, train.fuel + 25);
keys['e'] = false;
}
}
} else if (nearWall) {
promptUI.style.display = 'block';
promptUI.innerText = "Press [E] to Repair Barricade (+35 HP)";
if (keys['e']) {
if (player.scrap > 0 && train.wallHealth < train.maxWallHealth) {
player.scrap--;
train.wallHealth = Math.min(train.maxWallHealth, train.wallHealth + 35);
keys['e'] = false;
}
}
} else {
promptUI.style.display = 'none';
}
}
// --- RENDER ENGINE ---
function render() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 1. Draw Infinite Track Lines
ctx.strokeStyle = "#444";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(0, train.y - 10); ctx.lineTo(CANVAS_WIDTH, train.y - 10);
ctx.moveTo(0, train.y + train.height + 10); ctx.lineTo(CANVAS_WIDTH, train.y + train.height + 10);
ctx.stroke();
// Procedural Ties
ctx.strokeStyle = "#222";
ctx.lineWidth = 6;
for (let x = -trackOffset; x < CANVAS_WIDTH; x += 30) {
ctx.beginPath();
ctx.moveTo(x, train.y - 15);
ctx.lineTo(x, train.y + train.height + 15);
ctx.stroke();
}
// 2. Draw Train Carriages
ctx.fillStyle = "#3a3d40";
ctx.fillRect(train.x, train.y, train.width, train.height);
ctx.strokeStyle = "#666";
ctx.lineWidth = 3;
ctx.strokeRect(train.x, train.y, train.width, train.height);
// Furnace
ctx.fillStyle = train.fuel > 0 ? "#ff5500" : "#222";
ctx.fillRect(furnace.x, furnace.y, furnace.width, furnace.height);
ctx.fillStyle = "#fff";
ctx.font = "10px sans-serif";
ctx.fillText("FURNACE", furnace.x + 0, furnace.y + 24);
// Barricade / Wall
let wallAlpha = train.wallHealth / train.maxWallHealth;
ctx.fillStyle = `rgba(0, 180, 255, ${Math.max(0.2, wallAlpha)})`;
ctx.fillRect(barricade.x, barricade.y, barricade.width, barricade.height);
// 3. Draw Player
ctx.fillStyle = "#00ffcc";
ctx.beginPath();
ctx.arc(player.x, player.y, player.radius, 0, Math.PI * 2);
ctx.fill();
// Player Sight/Aim Line
ctx.strokeStyle = "rgba(0, 255, 204, 0.4)";
ctx.beginPath();
ctx.moveTo(player.x, player.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
// 4. Draw Horde Zombies
zombies.forEach(zombie => {
ctx.fillStyle = zombie.isBoarded ? "#ff0055" : "#00ff55";
ctx.beginPath();
ctx.arc(zombie.x, zombie.y, zombie.radius, 0, Math.PI * 2);
ctx.fill();
});
// 5. Draw Bullets / Raycasts
ctx.fillStyle = "#ffff00";
bullets.forEach(b => {
ctx.beginPath();
ctx.arc(b.x, b.y, 3, 0, Math.PI * 2);
ctx.fill();
});
// 6. Update HUD UI Elements
document.getElementById('state-name').innerText = currentState;
document.getElementById('state-timer').innerText = `Time Remaining: ${Math.ceil(stateTimer)}s`;
document.getElementById('fuel-fill').style.width = `${(train.fuel / train.maxFuel) * 100}%`;
document.getElementById('wall-fill').style.width = `${(train.wallHealth / train.maxWallHealth) * 100}%`;
document.getElementById('coal-count').innerText = player.coal;
document.getElementById('scrap-count').innerText = player.scrap;
}
// --- MAIN ENGINE TICK ---
function engineLoop(timestamp) {
let dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
if (!isNaN(dt)) {
updateGameState(dt);
updatePhysics();
render();
}
requestAnimationFrame(engineLoop);
}
requestAnimationFrame(engineLoop);
</script>
</body>
</html>
Game Source: Train Horde Survival Engine
Creator: AstroGalaxy64
Libraries: none
Complexity: complex (556 lines, 16.7 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: train-horde-survival-engine-astrogalaxy64" to link back to the original. Then publish at arcadelab.ai/publish.