Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgtdga7wkx0jsyjhiPlain Text
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Zombie Survival FPS - Hardcore Open World</title>
<style>
* {
box-sizing: border-box;
user-select: none;
}
body {
margin: 0;
overflow: hidden;
background-color: #000;
font-family: 'Courier New', Courier, monospace;
color: #fff;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: 5;
}
/* HUD Top */
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px;
font-size: 20px;
color: #00ff66;
text-shadow: 0 0 6px #00ff66;
}
.hud-stats {
display: flex;
flex-direction: column;
gap: 6px;
}
.coins-display {
color: #ffd700;
text-shadow: 0 0 8px #ffaa00;
font-weight: bold;
font-size: 24px;
}
.wood-display {
color: #e3a857;
text-shadow: 0 0 8px #8b4513;
font-weight: bold;
font-size: 22px;
}
/* Crosshair */
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 22px;
transform: translate(-50%, -50%);
pointer-events: none;
}
#crosshair::before, #crosshair::after {
content: '';
position: absolute;
background: #00ff66;
box-shadow: 0 0 4px #00ff66;
}
#crosshair::before {
top: 10px; left: 0; width: 22px; height: 2px;
}
#crosshair::after {
left: 10px; top: 0; width: 2px; height: 22px;
}
#action-prompt {
position: absolute;
top: 56%;
left: 50%;
transform: translate(-50%, -50%);
color: #00ffff;
font-size: 18px;
font-weight: bold;
text-shadow: 0 0 8px #00ffff;
display: none;
pointer-events: none;
background: rgba(0,0,0,0.75);
padding: 8px 16px;
border-radius: 6px;
border: 1px solid #00ffff;
}
/* HUD Bottom */
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 20px;
color: #fff;
}
.bar-container {
width: 260px;
height: 24px;
background: #111;
border: 2px solid #555;
border-radius: 4px;
overflow: hidden;
margin-bottom: 5px;
}
#health-bar {
width: 100%;
height: 100%;
background: #ff1111;
box-shadow: 0 0 10px #ff1111;
transition: width 0.15s ease-out;
}
#ammo-display {
font-size: 32px;
color: #ffff00;
text-shadow: 0 0 6px #ffff00;
}
#weapon-name {
font-size: 18px;
color: #aaa;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Rear Radar Sensor */
#radar-container {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
width: 140px;
height: 140px;
background: rgba(0, 20, 0, 0.85);
border: 2px solid #00ff66;
border-radius: 50%;
display: none;
box-shadow: 0 0 12px rgba(0, 255, 102, 0.4);
pointer-events: none;
}
#radar-canvas {
width: 100%;
height: 100%;
border-radius: 50%;
}
#rear-warning {
position: absolute;
top: 168px;
left: 50%;
transform: translateX(-50%);
color: #ff0000;
font-size: 16px;
font-weight: bold;
display: none;
text-shadow: 0 0 8px #ff0000;
animation: warningBlink 0.35s infinite alternate;
}
@keyframes warningBlink {
from { opacity: 0.2; }
to { opacity: 1; }
}
/* Overlay Screens */
#blocker {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.88);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
z-index: 10;
pointer-events: auto;
cursor: pointer;
}
#shop-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(5, 10, 18, 0.96);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 20;
color: #fff;
pointer-events: auto;
padding: 20px;
overflow-y: auto;
}
.shop-header {
font-size: 36px;
color: #ffd700;
text-shadow: 0 0 12px #ff9900;
margin-bottom: 4px;
font-weight: bold;
letter-spacing: 2px;
}
.shop-grid {
display: grid;
grid-template-columns: repeat(3, 240px);
gap: 14px;
max-width: 800px;
margin-bottom: 18px;
}
.shop-card {
background: #0f1724;
border: 2px solid #23354d;
border-radius: 8px;
padding: 14px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transition: transform 0.1s, border-color 0.2s;
}
.shop-card:hover {
border-color: #ffd700;
transform: translateY(-2px);
}
.shop-card h3 {
margin: 2px 0 6px 0;
color: #00ccff;
font-size: 17px;
}
.shop-card p {
font-size: 12px;
color: #9aa8bc;
margin: 0 0 10px 0;
height: 32px;
line-height: 1.3;
}
.shop-btn {
background: #ffd700;
color: #000;
border: none;
padding: 9px 14px;
font-weight: bold;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
width: 100%;
transition: background 0.15s;
}
.shop-btn:hover { background: #ffffff; }
.shop-btn:disabled { background: #333d4d; color: #6d7d93; cursor: not-allowed; }
.shop-footer {
display: flex;
gap: 20px;
align-items: center;
}
.close-shop-btn {
padding: 12px 28px;
background: #cc1111;
color: white;
border: none;
font-size: 16px;
cursor: pointer;
border-radius: 6px;
font-weight: bold;
}
.close-shop-btn:hover { background: #ff2222; }
.reset-btn {
padding: 12px 20px;
background: #331111;
color: #ff7777;
border: 1px solid #882222;
font-size: 14px;
cursor: pointer;
border-radius: 6px;
font-weight: bold;
}
.reset-btn:hover { background: #aa0000; color: #fff; }
h1 { margin: 0; font-size: 46px; color: #ff1111; text-transform: uppercase; letter-spacing: 4px; text-shadow: 0 0 14px #ff0000; }
p { font-size: 16px; color: #ccc; }
#damage-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
box-shadow: inset 0 0 0 0 rgba(255,0,0,0);
pointer-events: none;
transition: box-shadow 0.1s;
}
.damage-active {
box-shadow: inset 0 0 120px 60px rgba(255,0,0,0.7) !important;
}
#ammo-message {
position: absolute;
top: 62%;
left: 50%;
transform: translate(-50%, -50%);
color: #ff2222;
font-size: 26px;
font-weight: bold;
display: none;
text-shadow: 2px 2px 0 #000;
}
.controls-hint {
position: absolute;
bottom: 80px;
left: 20px;
font-size: 13px;
color: #bbb;
pointer-events: none;
line-height: 1.6;
background: rgba(0,0,0,0.65);
padding: 10px 14px;
border-radius: 6px;
border: 1px solid #333;
}
#toast-msg {
position: absolute;
top: 12%;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 255, 200, 0.95);
color: #000;
padding: 10px 24px;
font-weight: bold;
border-radius: 20px;
display: none;
z-index: 30;
box-shadow: 0 0 20px rgba(0,255,200,0.6);
font-size: 15px;
}
</style>
<!-- Load Three.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<!-- Game HUD -->
<div id="ui-layer">
<div class="hud-top">
<div class="hud-stats">
<div id="score">SCORE: 0</div>
<div id="coins" class="coins-display">COINS: $0</div>
<div id="wood" class="wood-display">🪵 WOOD: 0</div>
</div>
<div id="wave">WAVE: 1</div>
</div>
<div id="radar-container">
<canvas id="radar-canvas" width="140" height="140"></canvas>
<div id="rear-warning">⚠️ BEHIND YOU!</div>
</div>
<div id="action-prompt">PRESS [E] TO INTERACT</div>
<div id="ammo-message">RELOAD! (R)</div>
<div id="crosshair"></div>
<div class="controls-hint">
<b>[E]</b> Chop Trees / Loot Crates<br>
<b>[C]</b> Build Wall (15 Wood)<br>
<b>[F]</b> Flashlight | <b>[B]</b> Shop<br>
<b>[1-4]</b> Gun Swap | <b>[R]</b> Reload
</div>
<div class="hud-bottom">
<div>
<div class="bar-container">
<div id="health-bar"></div>
</div>
<div style="color:#ff2222; font-weight:bold;">HEALTH <span id="health-num">100/100</span></div>
</div>
<div style="text-align: right;">
<div id="weapon-name">PISTOL</div>
<div id="ammo-display">12 / 12</div>
<div style="color:#ffff00; font-weight:bold;">AMMO</div>
</div>
</div>
</div>
<div id="damage-overlay"></div>
<div id="toast-msg">ACCOUNT RESET SUCCESSFUL</div>
<!-- Start / Pause Overlay -->
<div id="blocker">
<div id="instructions" style="text-align: center;">
<h1 id="title-text">HARDCORE ZOMBIE SECTOR</h1>
<p style="margin-top: 15px; font-weight: bold; color: #00ff66;">CLICK TO PLAY</p>
<p style="font-size: 14px; color: #aaa; margin-top:15px; line-height: 1.6;">
WASD to Move • MOUSE to Aim • CLICK to Fire<br>
<b>E</b> Chop Trees / Loot Buildings • <b>C</b> Build Barricades • <b>F</b> Mega Flashlight<br>
<b>B</b> Shop Upgrades (Health up to 500, Radar, Guns)
</p>
</div>
</div>
<!-- Arsenal Shop Overlay -->
<div id="shop-overlay">
<div class="shop-header">ARSENAL & SURVIVAL UPGRADES</div>
<div style="color: #ffd700; margin-bottom: 14px; font-size: 20px; font-weight: bold;" id="shop-coin-count">COINS: $5000 | WOOD: 0</div>
<div class="shop-grid">
<!-- Shotgun -->
<div class="shop-card">
<h3>Shotgun</h3>
<p>Devastating heavy close-range burst firing 8 pellets.</p>
<button class="shop-btn" id="buy-shotgun" onclick="buyItem('shotgun')">Buy $150</button>
</div>
<!-- Assault Rifle -->
<div class="shop-card">
<h3>Assault Rifle</h3>
<p>High-speed automatic rifle with 30-round magazine.</p>
<button class="shop-btn" id="buy-rifle" onclick="buyItem('rifle')">Buy $350</button>
</div>
<!-- Plasma Cannon -->
<div class="shop-card">
<h3>Plasma Cannon</h3>
<p>Extreme power laser energy cannon for giant bosses.</p>
<button class="shop-btn" id="buy-plasma" onclick="buyItem('plasma')">Buy $750</button>
</div>
<!-- Mega Flashlight -->
<div class="shop-card">
<h3>Ultra Flashlight</h3>
<p>High-powered mega beam searchlight (Press F).</p>
<button class="shop-btn" id="buy-flashlight" onclick="buyItem('flashlight')">Buy $50</button>
</div>
<!-- Rear Radar Sensor -->
<div class="shop-card">
<h3>Rear Sensor Radar</h3>
<p>360° Radar & warning alerts for zombies behind you.</p>
<button class="shop-btn" id="buy-rear-sensor" onclick="buyItem('rear_sensor')">Buy $125</button>
</div>
<!-- Health Boost up to 500 -->
<div class="shop-card">
<h3>Max Health Boost</h3>
<p>+50 Max HP Upgrade (Up to 500 Max HP!). Heals full.</p>
<button class="shop-btn" id="buy-health" onclick="buyItem('health')">Buy $100</button>
</div>
<!-- Speed Boost -->
<div class="shop-card">
<h3>Sprint Speed</h3>
<p>Permanent +25% player sprint movement speed.</p>
<button class="shop-btn" id="buy-speed" onclick="buyItem('speed')">Buy $120</button>
</div>
<!-- Damage Amp -->
<div class="shop-card">
<h3>Damage Amp</h3>
<p>Permanent +50% extra firepower for all guns.</p>
<button class="shop-btn" id="buy-damage" onclick="buyItem('damage')">Buy $200</button>
</div>
<!-- Wood Pack -->
<div class="shop-card">
<h3>Wood Crate (+30)</h3>
<p>Instant lumber supply for building defensive walls.</p>
<button class="shop-btn" id="buy-wood" onclick="buyItem('wood_pack')">Buy $50</button>
</div>
</div>
<div class="shop-footer">
<button class="close-shop-btn" onclick="toggleShop(false)">RESUME GAME (B)</button>
<button class="reset-btn" onclick="resetAccountData()">⚠️ RESET ACCOUNT</button>
</div>
</div>
<script>
// Synthesized Web Audio API Sound Generator
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.connect(gainNode);
gainNode.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'shoot_pistol') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(650, now);
osc.frequency.exponentialRampToValueAtTime(80, now + 0.12);
gainNode.gain.setValueAtTime(0.3, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
osc.start(now); osc.stop(now + 0.12);
} else if (type === 'shoot_shotgun') {
osc.type = 'square'; osc.frequency.setValueAtTime(320, now);
osc.frequency.exponentialRampToValueAtTime(40, now + 0.25);
gainNode.gain.setValueAtTime(0.5, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
osc.start(now); osc.stop(now + 0.25);
} else if (type === 'shoot_rifle') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(850, now);
osc.frequency.exponentialRampToValueAtTime(110, now + 0.08);
gainNode.gain.setValueAtTime(0.25, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.08);
osc.start(now); osc.stop(now + 0.08);
} else if (type === 'shoot_plasma') {
osc.type = 'sine'; osc.frequency.setValueAtTime(1300, now);
osc.frequency.exponentialRampToValueAtTime(180, now + 0.2);
gainNode.gain.setValueAtTime(0.4, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now); osc.stop(now + 0.2);
} else if (type === 'chop') {
osc.type = 'square'; osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(60, now + 0.14);
gainNode.gain.setValueAtTime(0.35, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.14);
osc.start(now); osc.stop(now + 0.14);
} else if (type === 'build') {
osc.type = 'triangle'; osc.frequency.setValueAtTime(220, now);
osc.frequency.setValueAtTime(330, now + 0.1);
gainNode.gain.setValueAtTime(0.3, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now); osc.stop(now + 0.2);
} else if (type === 'empty') {
osc.type = 'square'; osc.frequency.setValueAtTime(800, now);
gainNode.gain.setValueAtTime(0.1, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.05);
osc.start(now); osc.stop(now + 0.05);
} else if (type === 'reload') {
osc.type = 'sine'; osc.frequency.setValueAtTime(300, now);
osc.frequency.linearRampToValueAtTime(600, now + 0.25);
gainNode.gain.setValueAtTime(0.2, now); gainNode.gain.linearRampToValueAtTime(0, now + 0.35);
osc.start(now); osc.stop(now + 0.35);
} else if (type === 'hit') {
osc.type = 'square'; osc.frequency.setValueAtTime(140, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.2);
gainNode.gain.setValueAtTime(0.35, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
osc.start(now); osc.stop(now + 0.2);
} else if (type === 'zombie_die') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(190, now);
osc.frequency.linearRampToValueAtTime(30, now + 0.35);
gainNode.gain.setValueAtTime(0.3, now); gainNode.gain.linearRampToValueAtTime(0, now + 0.35);
osc.start(now); osc.stop(now + 0.35);
} else if (type === 'coin') {
osc.type = 'sine'; osc.frequency.setValueAtTime(987.77, now);
osc.frequency.setValueAtTime(1318.51, now + 0.08);
gainNode.gain.setValueAtTime(0.2, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
osc.start(now); osc.stop(now + 0.25);
} else if (type === 'buy') {
osc.type = 'triangle'; osc.frequency.setValueAtTime(523.25, now);
osc.frequency.setValueAtTime(659.25, now + 0.08);
osc.frequency.setValueAtTime(783.99, now + 0.16);
gainNode.gain.setValueAtTime(0.25, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
} else if (type === 'loot') {
osc.type = 'sine'; osc.frequency.setValueAtTime(440, now);
osc.frequency.setValueAtTime(880, now + 0.12);
gainNode.gain.setValueAtTime(0.3, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
}
}
let camera, scene, renderer;
let raycaster;
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false, canJump = false, isRunning = false;
let prevTime = performance.now();
const velocity = new THREE.Vector3();
const direction = new THREE.Vector3();
let isGameActive = false;
let isShopOpen = false;
let score = 0;
let coins = 5000; // Updated starting balance to $5,000
let wood = 20;
let wave = 1;
let health = 100;
let maxHealth = 100;
let speedMultiplier = 1.0;
let damageMultiplier = 1.0;
// Flashlight
let flashlightSpot;
let hasFlashlight = false;
let flashlightOn = false;
// Rear Sensor / Radar
let hasRearSensor = false;
// Weapon Configuration
const WEAPONS = {
pistol: { name: "Pistol", damage: 65, fireRate: 200, maxAmmo: 12, pelletCount: 1, spread: 0.02, sound: 'shoot_pistol', unlocked: true },
shotgun: { name: "Shotgun", damage: 45, fireRate: 650, maxAmmo: 6, pelletCount: 8, spread: 0.12, sound: 'shoot_shotgun', unlocked: false },
rifle: { name: "Assault Rifle", damage: 55, fireRate: 90, maxAmmo: 30, pelletCount: 1, spread: 0.035, sound: 'shoot_rifle', unlocked: false },
plasma: { name: "Plasma Cannon", damage: 320, fireRate: 400, maxAmmo: 8, pelletCount: 1, spread: 0.01, sound: 'shoot_plasma', unlocked: false }
};
let currentWeaponKey = 'pistol';
let ammoMap = { pistol: 12, shotgun: 6, rifle: 30, plasma: 8 };
let isShooting = false;
let lastShotTime = 0;
let isReloading = false;
// Weapon Meshes
let weaponGroup;
let muzzleFlashMesh;
let gunMeshes = {};
// World Collections
let zombies = [];
let trees = [];
let barricades = [];
let particles = [];
let buildings = [];
let buildingBounds = []; // Bounding boxes for Safe Zones
let lootCrates = [];
let targetedTree = null;
let targetedCrate = null;
let lastSpawnTime = 0;
let spawnRate = 1200; // Slightly reduced spawn rate for easier gameplay
let baseZombieSpeed = 16; // Reduced zombie base speed
let playerBaseSpeed = 44;
// Lighting & Theme elements
let hemiLight, dirLight;
init();
loadAccountData();
animate();
function init() {
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1500);
camera.position.y = 10;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0d14);
scene.fog = new THREE.FogExp2(0x0a0d14, 0.0018);
// Lights
hemiLight = new THREE.HemisphereLight(0x555566, 0x111122, 0.5);
scene.add(hemiLight);
dirLight = new THREE.DirectionalLight(0xff7733, 0.4);
dirLight.position.set(60, 100, 40);
scene.add(dirLight);
// Mega Flashlight attached to Camera
flashlightSpot = new THREE.SpotLight(0xffffff, 0, 450, Math.PI / 3.5, 0.35, 1);
flashlightSpot.position.set(0, 0, 0);
flashlightSpot.target.position.set(0, 0, -1);
camera.add(flashlightSpot);
camera.add(flashlightSpot.target);
// Control Listeners
const canvas = document.body;
const blocker = document.getElementById('blocker');
blocker.addEventListener('click', function () {
if (!isShopOpen) canvas.requestPointerLock();
});
document.addEventListener('pointerlockchange', function () {
if (document.pointerLockElement === canvas) {
isGameActive = true;
blocker.style.display = 'none';
if (health <= 0) resetGame();
} else {
if (!isShopOpen) {
isGameActive = false;
blocker.style.display = 'flex';
}
}
});
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousemove', onMouseMove);
raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, 0, -1), 0, 150);
// Expanded Terrain Floor (7,500 x 7,500 Grid)
const gridSize = 512;
const gridCanvas = document.createElement('canvas');
gridCanvas.width = gridSize; gridCanvas.height = gridSize;
const ctx = gridCanvas.getContext('2d');
ctx.fillStyle = '#111822'; ctx.fillRect(0, 0, gridSize, gridSize);
ctx.strokeStyle = '#1e2c3e'; ctx.lineWidth = 4; ctx.strokeRect(0, 0, gridSize, gridSize);
const gridTexture = new THREE.CanvasTexture(gridCanvas);
gridTexture.wrapS = THREE.RepeatWrapping; gridTexture.wrapT = THREE.RepeatWrapping;
gridTexture.repeat.set(250, 250);
const floorGeometry = new THREE.PlaneGeometry(7500, 7500);
const floorMaterial = new THREE.MeshStandardMaterial({ map: gridTexture, roughness: 0.85 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = - Math.PI / 2;
scene.add(floor);
// World Objects Generation (Scaled for 7,500x7,500 world)
generateTrees(250);
generateBuildings(25);
createWeapons();
// Spawn initial zombies immediately so player isn't alone
for (let i = 0; i < 8; i++) {
spawnZombie();
}
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize);
updateThemeForWave();
}
function generateTrees(count) {
const trunkGeo = new THREE.CylinderGeometry(1.0, 1.5, 14, 8);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a2e18, roughness: 0.9 });
const leavesGeo = new THREE.ConeGeometry(7, 18, 8);
const leavesMat = new THREE.MeshStandardMaterial({ color: 0x1b4d24, roughness: 0.7 });
for (let i = 0; i < count; i++) {
const treeGroup = new THREE.Group();
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
trunk.position.y = 7;
treeGroup.add(trunk);
const leaves = new THREE.Mesh(leavesGeo, leavesMat);
leaves.position.y = 18;
treeGroup.add(leaves);
const angle = Math.random() * Math.PI * 2;
const dist = 35 + Math.random() * 1200;
treeGroup.position.set(Math.cos(angle) * dist, 0, Math.sin(angle) * dist);
scene.add(treeGroup);
trees.push({ group: treeGroup, trunk: trunk, health: 3, maxHealth: 3 });
}
}
function generateBuildings(count) {
const wallMat = new THREE.MeshStandardMaterial({ color: 0x222a36, roughness: 0.85, metalness: 0.2 });
const roofMat = new THREE.MeshStandardMaterial({ color: 0x151c24, roughness: 0.9 });
const crateMat = new THREE.MeshStandardMaterial({ color: 0xb8860b, roughness: 0.4, metalness: 0.5 });
const glowMat = new THREE.MeshBasicMaterial({ color: 0x00ffcc });
for (let b = 0; b < count; b++) {
const bGroup = new THREE.Group();
const angle = (b / count) * Math.PI * 2 + (Math.random() * 0.3);
const dist = 100 + Math.random() * 1000;
const bx = Math.cos(angle) * dist;
const bz = Math.sin(angle) * dist;
bGroup.position.set(bx, 0, bz);
const rotY = Math.random() * Math.PI * 2;
bGroup.rotation.y = rotY;
const width = 24 + Math.random() * 10;
const depth = 24 + Math.random() * 10;
const height = 12;
const backWall = new THREE.Mesh(new THREE.BoxGeometry(width, height, 1), wallMat);
backWall.position.set(0, height / 2, -depth / 2);
bGroup.add(backWall);
const leftWall = new THREE.Mesh(new THREE.BoxGeometry(1, height, depth), wallMat);
leftWall.position.set(-width / 2, height / 2, 0);
bGroup.add(leftWall);
const rightWall = new THREE.Mesh(new THREE.BoxGeometry(1, height, depth), wallMat);
rightWall.position.set(width / 2, height / 2, 0);
bGroup.add(rightWall);
const frontLeft = new THREE.Mesh(new THREE.BoxGeometry(width * 0.35, height, 1), wallMat);
frontLeft.position.set(-width * 0.32, height / 2, depth / 2);
bGroup.add(frontLeft);
const frontRight = new THREE.Mesh(new THREE.BoxGeometry(width * 0.35, height, 1), wallMat);
frontRight.position.set(width * 0.32, height / 2, depth / 2);
bGroup.add(frontRight);
const roof = new THREE.Mesh(new THREE.BoxGeometry(width + 2, 0.8, depth + 2), roofMat);
roof.position.set(0, height, 0);
bGroup.add(roof);
scene.add(bGroup);
buildings.push(bGroup);
// Safe zone boundary recording
buildingBounds.push({
minX: bx - width / 2 - 2,
maxX: bx + width / 2 + 2,
minZ: bz - depth / 2 - 2,
maxZ: bz + depth / 2 + 2
});
// Spawn Loot Crates inside
const crateCount = 1 + Math.floor(Math.random() * 2);
for (let c = 0; c < crateCount; c++) {
const crateGroup = new THREE.Group();
const crateBox = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.4, 2.4), crateMat);
crateBox.position.y = 1.2;
crateGroup.add(crateBox);
const glowBand = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.35, 2.5), glowMat);
glowBand.position.y = 1.6;
crateGroup.add(glowBand);
const cx = (Math.random() - 0.5) * (width - 6);
const cz = (Math.random() - 0.5) * (depth - 6);
const worldPos = new THREE.Vector3(cx, 0, cz).applyAxisAngle(new THREE.Vector3(0, 1, 0), rotY).add(bGroup.position);
crateGroup.position.copy(worldPos);
scene.add(crateGroup);
lootCrates.push({ group: crateGroup, box: crateBox, glow: glowBand, looted: false });
}
}
}
// Helper check if position is inside an abandoned building safe zone
function isPositionInsideBuilding(pos) {
for (let b of buildingBounds) {
if (pos.x >= b.minX && pos.x <= b.maxX && pos.z >= b.minZ && pos.z <= b.maxZ) {
return true;
}
}
return false;
}
// Dynamic Dusk/Twilight theme switch every 5 levels
function updateThemeForWave() {
const isTwilightMode = Math.floor((wave - 1) / 5) % 2 === 1;
if (isTwilightMode) {
// Brighter Dusk Twilight atmosphere (dusk sky, twilight ambient glow)
const twilightBg = new THREE.Color(0x1a2138);
scene.background = twilightBg;
scene.fog.color = twilightBg;
hemiLight.color.setHex(0x9a88bb);
hemiLight.groundColor.setHex(0x332244);
hemiLight.intensity = 0.85; // Brighter visibility
dirLight.color.setHex(0xffaa66);
dirLight.intensity = 0.65;
} else {
// Standard Night atmosphere
const nightBg = new THREE.Color(0x0a0d14);
scene.background = nightBg;
scene.fog.color = nightBg;
hemiLight.color.setHex(0x555566);
hemiLight.groundColor.setHex(0x111122);
hemiLight.intensity = 0.5;
dirLight.color.setHex(0xff7733);
dirLight.intensity = 0.4;
}
}
function generateBuildings(count) {
const wallMat = new THREE.MeshStandardMaterial({ color: 0x222a36, roughness: 0.85, metalness: 0.2 });
const roofMat = new THREE.MeshStandardMaterial({ color: 0x151c24, roughness: 0.9 });
const crateMat = new THREE.MeshStandardMaterial({ color: 0xb8860b, roughness: 0.4, metalness: 0.5 });
const glowMat = new THREE.MeshBasicMaterial({ color: 0x00ffcc });
for (let b = 0; b < count; b++) {
const bGroup = new THREE.Group();
const angle = (b / count) * Math.PI * 2 + (Math.random() * 0.3);
const dist = 80 + Math.random() * 320;
const bx = Math.cos(angle) * dist;
const bz = Math.sin(angle) * dist;
bGroup.position.set(bx, 0, bz);
bGroup.rotation.y = Math.random() * Math.PI * 2;
const width = 20 + Math.random() * 8;
const depth = 20 + Math.random() * 8;
const height = 12;
const backWall = new THREE.Mesh(new THREE.BoxGeometry(width, height, 1), wallMat);
backWall.position.set(0, height / 2, -depth / 2);
bGroup.add(backWall);
const leftWall = new THREE.Mesh(new THREE.BoxGeometry(1, height, depth), wallMat);
leftWall.position.set(-width / 2, height / 2, 0);
bGroup.add(leftWall);
const rightWall = new THREE.Mesh(new THREE.BoxGeometry(1, height, depth), wallMat);
rightWall.position.set(width / 2, height / 2, 0);
bGroup.add(rightWall);
const frontLeft = new THREE.Mesh(new THREE.BoxGeometry(width * 0.35, height, 1), wallMat);
frontLeft.position.set(-width * 0.32, height / 2, depth / 2);
bGroup.add(frontLeft);
const frontRight = new THREE.Mesh(new THREE.BoxGeometry(width * 0.35, height, 1), wallMat);
frontRight.position.set(width * 0.32, height / 2, depth / 2);
bGroup.add(frontRight);
const roof = new THREE.Mesh(new THREE.BoxGeometry(width + 2, 0.8, depth + 2), roofMat);
roof.position.set(0, height, 0);
bGroup.add(roof);
scene.add(bGroup);
buildings.push(bGroup);
// Spawn Loot Crates inside
const crateCount = 1 + Math.floor(Math.random() * 2);
for (let c = 0; c < crateCount; c++) {
const crateGroup = new THREE.Group();
const crateBox = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.4, 2.4), crateMat);
crateBox.position.y = 1.2;
crateGroup.add(crateBox);
const glowBand = new THREE.Mesh(new THREE.BoxGeometry(2.5, 0.35, 2.5), glowMat);
glowBand.position.y = 1.6;
crateGroup.add(glowBand);
const cx = (Math.random() - 0.5) * (width - 6);
const cz = (Math.random() - 0.5) * (depth - 6);
const worldPos = new THREE.Vector3(cx, 0, cz).applyAxisAngle(new THREE.Vector3(0, 1, 0), bGroup.rotation.y).add(bGroup.position);
crateGroup.position.copy(worldPos);
scene.add(crateGroup);
lootCrates.push({ group: crateGroup, box: crateBox, glow: glowBand, looted: false });
}
}
}
function createWeapons() {
weaponGroup = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.3, metalness: 0.8 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.5 });
const energyMat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
// 1. Pistol
const pistolGroup = new THREE.Group();
const pBody = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.5, 1.2), bodyMat);
pBody.position.set(0.4, -0.4, -1.0);
const pHandle = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.6, 0.3), darkMat);
pHandle.position.set(0.4, -0.7, -0.7);
pHandle.rotation.x = -0.2;
pistolGroup.add(pBody, pHandle);
gunMeshes['pistol'] = pistolGroup;
// 2. Shotgun
const shotgunGroup = new THREE.Group();
const sBody = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.5, 2.2), bodyMat);
sBody.position.set(0.4, -0.4, -1.3);
const sBarrel = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 2.0), darkMat);
sBarrel.rotation.x = Math.PI / 2;
sBarrel.position.set(0.4, -0.3, -2.0);
shotgunGroup.add(sBody, sBarrel);
gunMeshes['shotgun'] = shotgunGroup;
// 3. Rifle
const rifleGroup = new THREE.Group();
const rBody = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.55, 2.4), bodyMat);
rBody.position.set(0.4, -0.4, -1.4);
const rMag = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.8, 0.4), darkMat);
rMag.position.set(0.4, -0.9, -1.2);
rMag.rotation.x = 0.2;
rifleGroup.add(rBody, rMag);
gunMeshes['rifle'] = rifleGroup;
// 4. Plasma
const plasmaGroup = new THREE.Group();
const plBody = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 2.5), bodyMat);
plBody.position.set(0.4, -0.4, -1.4);
const plCore = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 1.5), energyMat);
plCore.rotation.x = Math.PI / 2;
plCore.position.set(0.4, -0.4, -1.5);
plasmaGroup.add(plBody, plCore);
gunMeshes['plasma'] = plasmaGroup;
// Attach all to weapon group
Object.keys(gunMeshes).forEach(key => {
gunMeshes[key].visible = (key === currentWeaponKey);
weaponGroup.add(gunMeshes[key]);
});
// Muzzle Flash
const flashGeo = new THREE.PlaneGeometry(1.2, 1.2);
const flashMat = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0, side: THREE.DoubleSide });
muzzleFlashMesh = new THREE.Mesh(flashGeo, flashMat);
muzzleFlashMesh.position.set(0.4, -0.35, -2.8);
weaponGroup.add(muzzleFlashMesh);
camera.add(weaponGroup);
scene.add(camera);
}
function onKeyDown(event) {
if (isShopOpen && event.code !== 'KeyB') return;
switch (event.code) {
case 'KeyW': moveForward = true; break;
case 'KeyA': moveLeft = true; break;
case 'KeyS': moveBackward = true; break;
case 'KeyD': moveRight = true; break;
case 'Space': if (canJump) velocity.y += 320; canJump = false; break;
case 'ShiftLeft': isRunning = true; break;
case 'KeyR': reload(); break;
case 'KeyF': toggleFlashlight(); break;
case 'KeyB': toggleShop(); break;
case 'KeyE': interact(); break;
case 'KeyC': buildBarricade(); break;
case 'Digit1': switchWeapon('pistol'); break;
case 'Digit2': switchWeapon('shotgun'); break;
case 'Digit3': switchWeapon('rifle'); break;
case 'Digit4': switchWeapon('plasma'); break;
}
}
function onKeyUp(event) {
switch (event.code) {
case 'KeyW': moveForward = false; break;
case 'KeyA': moveLeft = false; break;
case 'KeyS': moveBackward = false; break;
case 'KeyD': moveRight = false; break;
case 'ShiftLeft': isRunning = false; break;
}
}
function onMouseDown(event) {
if (isGameActive && !isShopOpen && event.button === 0) {
isShooting = true;
}
}
function onMouseUp(event) {
if (event.button === 0) isShooting = false;
}
function onMouseMove(event) {
if (!isGameActive || isShopOpen) return;
const movementX = event.movementX || 0;
const movementY = event.movementY || 0;
const euler = new THREE.Euler(0, 0, 0, 'YXZ');
euler.setFromQuaternion(camera.quaternion);
euler.y -= movementX * 0.0022;
euler.x -= movementY * 0.0022;
euler.x = Math.max(-Math.PI / 2.05, Math.min(Math.PI / 2.05, euler.x));
camera.quaternion.setFromEuler(euler);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function interact() {
if (targetedTree) {
chopTree();
} else if (targetedCrate) {
lootCrate();
}
}
function chopTree() {
if (!targetedTree) return;
targetedTree.health--;
playSound('chop');
createParticle(targetedTree.group.position.clone().add(new THREE.Vector3(0, 5, 0)), 0x8b4513);
if (targetedTree.health <= 0) {
wood += 15;
playSound('coin');
showToast("+15 WOOD CHOPPED!");
// Tree falls/respawns elsewhere
scene.remove(targetedTree.group);
trees = trees.filter(t => t !== targetedTree);
targetedTree = null;
document.getElementById('action-prompt').style.display = 'none';
// Spawn replacement tree far away
setTimeout(() => generateTrees(1), 15000);
}
updateHUD();
saveAccountData();
}
function buildBarricade() {
if (wood < 15) {
showToast("NOT ENOUGH WOOD! NEED 15 WOOD");
return;
}
wood -= 15;
playSound('build');
const wallGeo = new THREE.BoxGeometry(10, 6, 1.5);
const wallMat = new THREE.MeshStandardMaterial({ color: 0x8b4513, roughness: 0.9 });
const wallMesh = new THREE.Mesh(wallGeo, wallMat);
// Position wall directly in front of camera direction
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
forward.y = 0; forward.normalize();
wallMesh.position.copy(camera.position).addScaledVector(forward, 8);
wallMesh.position.y = 3;
wallMesh.quaternion.copy(camera.quaternion);
wallMesh.rotation.x = 0; wallMesh.rotation.z = 0;
scene.add(wallMesh);
barricades.push({ mesh: wallMesh, health: 250, maxHealth: 250 });
showToast("BARRICADE BUILT!");
updateHUD();
saveAccountData();
}
function lootCrate() {
if (!targetedCrate || targetedCrate.looted) return;
targetedCrate.looted = true;
playSound('loot');
const lootType = Math.floor(Math.random() * 4);
if (lootType === 0) {
const gainedCoins = 60 + Math.floor(Math.random() * 120);
coins += gainedCoins;
showToast(`LOOTED CRATE: +$${gainedCoins} COINS!`);
} else if (lootType === 1) {
const gainedWood = 25 + Math.floor(Math.random() * 30);
wood += gainedWood;
showToast(`LOOTED CRATE: +${gainedWood} WOOD!`);
} else if (lootType === 2) {
health = Math.min(maxHealth, health + 50);
showToast(`LOOTED CRATE: +50 HEALTH RESTORED!`);
} else {
Object.keys(WEAPONS).forEach(k => ammoMap[k] = WEAPONS[k].maxAmmo);
showToast(`LOOTED CRATE: ALL AMMO REFILLED!`);
}
targetedCrate.box.material = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.9 });
targetedCrate.glow.material = new THREE.MeshBasicMaterial({ color: 0x111111 });
createParticle(targetedCrate.group.position.clone().add(new THREE.Vector3(0, 2, 0)), 0x00ffcc);
targetedCrate = null;
document.getElementById('action-prompt').style.display = 'none';
saveAccountData();
updateHUD();
}
function switchWeapon(key) {
if (!WEAPONS[key] || !WEAPONS[key].unlocked || isReloading) return;
currentWeaponKey = key;
Object.keys(gunMeshes).forEach(k => gunMeshes[k].visible = (k === key));
updateHUD();
}
function toggleFlashlight() {
if (!hasFlashlight) {
showToast("BUY FLASHLIGHT IN SHOP (B)");
return;
}
flashlightOn = !flashlightOn;
flashlightSpot.intensity = flashlightOn ? 5.0 : 0;
playSound('buy');
}
function reload() {
const w = WEAPONS[currentWeaponKey];
if (ammoMap[currentWeaponKey] === w.maxAmmo || isReloading) return;
isReloading = true;
playSound('reload');
const msg = document.getElementById('ammo-message');
msg.innerText = "RELOADING...";
msg.style.display = 'block';
msg.style.color = '#ffff00';
setTimeout(() => {
ammoMap[currentWeaponKey] = w.maxAmmo;
isReloading = false;
msg.style.display = 'none';
updateHUD();
}, 1400);
}
function shoot(now) {
const w = WEAPONS[currentWeaponKey];
if (now - lastShotTime < w.fireRate || isReloading) return;
if (ammoMap[currentWeaponKey] <= 0) {
playSound('empty');
const msg = document.getElementById('ammo-message');
msg.innerText = "RELOAD! (R)";
msg.style.display = 'block';
msg.style.color = '#ff2222';
lastShotTime = now;
return;
}
lastShotTime = now;
ammoMap[currentWeaponKey]--;
updateHUD();
playSound(w.sound);
// Recoil
weaponGroup.position.z += 0.25;
weaponGroup.rotation.x += 0.12;
// Flash
muzzleFlashMesh.opacity = 1;
muzzleFlashMesh.rotation.z = Math.random() * Math.PI;
// Pellets
for (let i = 0; i < w.pelletCount; i++) {
const spreadVec = new THREE.Vector3(
(Math.random() - 0.5) * w.spread,
(Math.random() - 0.5) * w.spread,
-1
).normalize().applyQuaternion(camera.quaternion);
raycaster.set(camera.position, spreadVec);
const zombieMeshes = zombies.map(z => z.mesh);
const intersects = raycaster.intersectObjects(zombieMeshes);
if (intersects.length > 0) {
const hitObj = intersects[0].object;
const zombie = zombies.find(z => z.mesh === hitObj);
if (zombie) {
createParticle(intersects[0].point, 0xff0000);
const finalDmg = w.damage * damageMultiplier;
zombie.health -= finalDmg;
if (zombie.health <= 0) {
killZombie(zombie);
} else {
const pushDir = hitObj.position.clone().sub(camera.position).normalize();
hitObj.position.add(pushDir.multiplyScalar(1.2));
}
}
} else {
const floorIntersect = raycaster.intersectObject(scene.children.find(c => c.geometry && c.geometry.type === "PlaneGeometry"));
if (floorIntersect.length > 0) {
createParticle(floorIntersect[0].point, 0xffff00);
}
}
}
}
function createParticle(pos, color) {
const geo = new THREE.BoxGeometry(0.25, 0.25, 0.25);
const mat = new THREE.MeshBasicMaterial({ color: color });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
scene.add(mesh);
const dir = new THREE.Vector3((Math.random() - 0.5), Math.random(), (Math.random() - 0.5)).normalize();
particles.push({ mesh: mesh, dir: dir, life: 1.0 });
}
function spawnZombie() {
const angle = Math.random() * Math.PI * 2;
const dist = 55 + Math.random() * 75;
const x = camera.position.x + Math.cos(angle) * dist;
const z = camera.position.z + Math.sin(angle) * dist;
// Spawn type checks
const isEvery5Wave = (wave % 5 === 0);
const isMegaBoss = isEvery5Wave && (Math.random() < 0.4 || zombies.length === 0);
const isBoss = !isMegaBoss && Math.random() < 0.18;
const isRainbow = !isMegaBoss && !isBoss && Math.random() < 0.20; // Shiny Rainbow Mini Zombie
const isFast = !isMegaBoss && !isBoss && !isRainbow && Math.random() < 0.25;
let scale = isMegaBoss ? 7.5 : (isBoss ? 4.2 : (isRainbow ? 1.4 : (isFast ? 1.8 : 2.4)));
let height = 3.5 * scale;
let width = 1.8 * scale;
let zColor = isMegaBoss ? 0xff00ff : (isBoss ? 0xcc1111 : (isRainbow ? 0xffffff : (isFast ? 0xdddd22 : 0x118833)));
// Eased difficulty scaling
let zHealth = (isMegaBoss ? 1500 : (isBoss ? 500 : (isRainbow ? 120 : 130))) + (wave * 35);
let zDamage = (isMegaBoss ? 25 : (isBoss ? 16 : 8)) + (wave * 2);
let zSpeed = isFast ? baseZombieSpeed * 1.35 : (isMegaBoss ? baseZombieSpeed * 0.65 : (isBoss ? baseZombieSpeed * 0.8 : baseZombieSpeed));
const geometry = new THREE.BoxGeometry(width, height, width);
const material = new THREE.MeshStandardMaterial({
color: zColor,
roughness: isRainbow ? 0.2 : 0.6,
metalness: isRainbow ? 0.8 : 0.1
});
const zombieMesh = new THREE.Mesh(geometry, material);
zombieMesh.position.set(x, height / 2, z);
const eyeGeo = new THREE.BoxGeometry(0.35 * scale, 0.35 * scale, 0.35 * scale);
const eyeMat = new THREE.MeshBasicMaterial({ color: isMegaBoss ? 0x00ffff : (isBoss ? 0xffff00 : 0xff0000) });
const eye1 = new THREE.Mesh(eyeGeo, eyeMat);
const eye2 = new THREE.Mesh(eyeGeo, eyeMat);
eye1.position.set(width * 0.25, height * 0.35, width * 0.5);
eye2.position.set(-width * 0.25, height * 0.35, width * 0.5);
zombieMesh.add(eye1, eye2);
scene.add(zombieMesh);
zombies.push({
mesh: zombieMesh,
health: zHealth,
maxHealth: zHealth,
damage: zDamage,
speed: zSpeed,
isBoss: isBoss || isMegaBoss,
isMegaBoss: isMegaBoss,
isRainbow: isRainbow,
value: isMegaBoss ? 2500 : (isBoss ? 1000 : (isRainbow ? 650 : 35)) // High rewards for bosses and rainbow mini-zombies
});
}
function killZombie(zombie) {
scene.remove(zombie.mesh);
zombies = zombies.filter(z => z !== zombie);
score += zombie.value * 10;
coins += zombie.value;
playSound('zombie_die');
playSound('coin');
if (zombie.isMegaBoss) {
showToast(`🏆 MEGA BOSS DEFEATED! +$${zombie.value} COINS!`);
} else if (zombie.isBoss) {
showToast(`💥 BOSS ELIMINATED! +$${zombie.value} COINS!`);
} else if (zombie.isRainbow) {
showToast(`🌈 SHINY RAINBOW ZOMBIE KILLED! +$${zombie.value} COINS!`);
}
if (score >= wave * 1500) {
wave++;
spawnRate = Math.max(250, spawnRate - 80);
health = Math.min(maxHealth, health + 50);
updateThemeForWave();
showToast(`WAVE ${wave} STARTED! ENVIRONMENT SHIFTED.`);
}
saveAccountData();
updateHUD();
updateShopUI();
}
function takeDamage(amount) {
health -= amount;
playSound('hit');
const overlay = document.getElementById('damage-overlay');
overlay.classList.add('damage-active');
setTimeout(() => overlay.classList.remove('damage-active'), 200);
if (health <= 0) {
health = 0;
gameOver();
}
updateHUD();
}
function gameOver() {
isGameActive = false;
document.exitPointerLock();
document.getElementById('blocker').style.display = 'flex';
document.getElementById('title-text').innerText = "YOU DIED";
document.getElementById('title-text').style.color = "#888";
document.getElementById('instructions').querySelector('p').innerText = "FINAL SCORE: " + score + " - CLICK TO RESTART";
}
function resetGame() {
health = maxHealth;
score = 0;
wave = 1;
Object.keys(WEAPONS).forEach(k => ammoMap[k] = WEAPONS[k].maxAmmo);
zombies.forEach(z => scene.remove(z.mesh));
zombies = [];
// Spawn fresh wave of zombies on restart
for (let i = 0; i < 8; i++) {
spawnZombie();
}
updateHUD();
document.getElementById('title-text').innerText = "HARDCORE ZOMBIE SECTOR";
document.getElementById('title-text').style.color = "#ff1111";
}
function toggleShop(forceState) {
if (forceState !== undefined) {
isShopOpen = forceState;
} else {
isShopOpen = !isShopOpen;
}
const shopEl = document.getElementById('shop-overlay');
if (isShopOpen) {
shopEl.style.display = 'flex';
document.exitPointerLock();
updateShopUI();
} else {
shopEl.style.display = 'none';
if (isGameActive) document.body.requestPointerLock();
}
}
function buyItem(item) {
if (item === 'shotgun' && coins >= 150) {
coins -= 150; WEAPONS.shotgun.unlocked = true; switchWeapon('shotgun'); playSound('buy'); showToast("UNLOCKED SHOTGUN!");
} else if (item === 'rifle' && coins >= 350) {
coins -= 350; WEAPONS.rifle.unlocked = true; switchWeapon('rifle'); playSound('buy'); showToast("UNLOCKED ASSAULT RIFLE!");
} else if (item === 'plasma' && coins >= 750) {
coins -= 750; WEAPONS.plasma.unlocked = true; switchWeapon('plasma'); playSound('buy'); showToast("UNLOCKED PLASMA CANNON!");
} else if (item === 'flashlight' && coins >= 50) {
coins -= 50; hasFlashlight = true; toggleFlashlight(); showToast("ULTRA FLASHLIGHT UNLOCKED (F)!");
} else if (item === 'rear_sensor' && coins >= 125) {
coins -= 125; hasRearSensor = true; document.getElementById('radar-container').style.display = 'block'; playSound('buy'); showToast("REAR RADAR SENSOR ACTIVE!");
} else if (item === 'health' && coins >= 100) {
if (maxHealth >= 500) {
showToast("MAX HEALTH ALREADY AT LIMIT (500 HP)!"); return;
}
coins -= 100; maxHealth += 50; health = maxHealth; playSound('buy'); showToast(`MAX HEALTH UPGRADED TO ${maxHealth} HP!`);
} else if (item === 'speed' && coins >= 120) {
coins -= 120; speedMultiplier += 0.25; playSound('buy'); showToast("SPRINT SPEED +25%!");
} else if (item === 'damage' && coins >= 200) {
coins -= 200; damageMultiplier += 0.50; playSound('buy'); showToast("DAMAGE FIREPOWER +50%!");
} else if (item === 'wood_pack' && coins >= 50) {
coins -= 50; wood += 30; playSound('buy'); showToast("+30 WOOD SUPPLIES!");
} else {
showToast("NOT ENOUGH COINS!"); return;
}
saveAccountData();
updateHUD();
updateShopUI();
}
function updateShopUI() {
document.getElementById('shop-coin-count').innerText = `COINS: $${coins} | WOOD: ${wood}`;
document.getElementById('buy-shotgun').disabled = WEAPONS.shotgun.unlocked || coins < 150;
if (WEAPONS.shotgun.unlocked) document.getElementById('buy-shotgun').innerText = "UNLOCKED";
document.getElementById('buy-rifle').disabled = WEAPONS.rifle.unlocked || coins < 350;
if (WEAPONS.rifle.unlocked) document.getElementById('buy-rifle').innerText = "UNLOCKED";
document.getElementById('buy-plasma').disabled = WEAPONS.plasma.unlocked || coins < 750;
if (WEAPONS.plasma.unlocked) document.getElementById('buy-plasma').innerText = "UNLOCKED";
document.getElementById('buy-flashlight').disabled = hasFlashlight || coins < 50;
if (hasFlashlight) document.getElementById('buy-flashlight').innerText = "UNLOCKED";
document.getElementById('buy-rear-sensor').disabled = hasRearSensor || coins < 125;
if (hasRearSensor) document.getElementById('buy-rear-sensor').innerText = "UNLOCKED";
document.getElementById('buy-health').disabled = maxHealth >= 500 || coins < 100;
if (maxHealth >= 500) document.getElementById('buy-health').innerText = "MAX (500 HP)";
}
function updateHUD() {
document.getElementById('score').innerText = `SCORE: ${score}`;
document.getElementById('coins').innerText = `COINS: $${coins}`;
document.getElementById('wood').innerText = `🪵 WOOD: ${wood}`;
document.getElementById('wave').innerText = `WAVE: ${wave}`;
const w = WEAPONS[currentWeaponKey];
document.getElementById('weapon-name').innerText = w.name;
document.getElementById('ammo-display').innerText = `${ammoMap[currentWeaponKey]} / ${w.maxAmmo}`;
const healthPct = Math.max(0, (health / maxHealth) * 100);
document.getElementById('health-bar').style.width = `${healthPct}%`;
document.getElementById('health-num').innerText = `${Math.ceil(health)}/${maxHealth}`;
}
function renderRadar() {
if (!hasRearSensor) return;
const canvas = document.getElementById('radar-canvas');
const ctx = canvas.getContext('2d');
const cx = 70, cy = 70, radius = 65;
ctx.clearRect(0, 0, 140, 140);
// Background circle
ctx.fillStyle = 'rgba(0, 30, 0, 0.7)';
ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.fill();
// Radar Grid
ctx.strokeStyle = 'rgba(0, 255, 102, 0.3)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(cx, cy, radius * 0.5, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx, cy - radius); ctx.lineTo(cx, cy + radius); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx - radius, cy); ctx.lineTo(cx + radius, cy); ctx.stroke();
// Player Center Triangle
ctx.fillStyle = '#00ff66';
ctx.beginPath();
ctx.moveTo(cx, cy - 6); ctx.lineTo(cx - 5, cy + 5); ctx.lineTo(cx + 5, cy + 5);
ctx.closePath(); ctx.fill();
// Player Forward Quaternion Vector
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const pAngle = Math.atan2(forward.x, forward.z);
let rearThreat = false;
// Plot Zombies
zombies.forEach(z => {
const relX = z.mesh.position.x - camera.position.x;
const relZ = z.mesh.position.z - camera.position.z;
const dist = Math.sqrt(relX * relX + relZ * relZ);
if (dist < 120) {
const zAngle = Math.atan2(relX, relZ);
const diffAngle = zAngle - pAngle;
const mapDist = (dist / 120) * radius;
const mapX = cx + Math.sin(diffAngle) * mapDist;
const mapY = cy - Math.cos(diffAngle) * mapDist;
ctx.fillStyle = z.isBoss ? '#ffff00' : '#ff0000';
ctx.beginPath();
ctx.arc(mapX, mapY, z.isBoss ? 5 : 3, 0, Math.PI * 2);
ctx.fill();
// Check if behind player (Angle > 110 degrees) and within 45 units
const deg = Math.abs(diffAngle * (180 / Math.PI));
if ((deg > 110 && deg < 250) && dist < 45) {
rearThreat = true;
}
}
});
document.getElementById('rear-warning').style.display = rearThreat ? 'block' : 'none';
}
function saveAccountData() {
const data = {
coins: coins,
wood: wood,
maxHealth: maxHealth,
speedMultiplier: speedMultiplier,
damageMultiplier: damageMultiplier,
hasFlashlight: hasFlashlight,
hasRearSensor: hasRearSensor,
unlockedWeapons: {
shotgun: WEAPONS.shotgun.unlocked,
rifle: WEAPONS.rifle.unlocked,
plasma: WEAPONS.plasma.unlocked
}
};
localStorage.setItem('zombie_fps_account', JSON.stringify(data));
}
function loadAccountData() {
const raw = localStorage.getItem('zombie_fps_account');
if (!raw) return;
try {
const data = JSON.parse(raw);
coins = data.coins || 0;
wood = data.wood || 20;
maxHealth = data.maxHealth || 100;
health = maxHealth;
speedMultiplier = data.speedMultiplier || 1.0;
damageMultiplier = data.damageMultiplier || 1.0;
hasFlashlight = !!data.hasFlashlight;
hasRearSensor = !!data.hasRearSensor;
if (data.unlockedWeapons) {
WEAPONS.shotgun.unlocked = !!data.unlockedWeapons.shotgun;
WEAPONS.rifle.unlocked = !!data.unlockedWeapons.rifle;
WEAPONS.plasma.unlocked = !!data.unlockedWeapons.plasma;
}
if (hasRearSensor) {
document.getElementById('radar-container').style.display = 'block';
}
updateHUD();
} catch (e) {
console.error("Failed to load save data", e);
}
}
function resetAccountData() {
localStorage.removeItem('zombie_fps_account');
coins = 5000; // Reset with $5,000 deposit
wood = 20;
maxHealth = 100;
health = 100;
wave = 1;
speedMultiplier = 1.0;
damageMultiplier = 1.0;
hasFlashlight = false;
hasRearSensor = false;
flashlightSpot.intensity = 0;
flashlightOn = false;
document.getElementById('radar-container').style.display = 'none';
WEAPONS.shotgun.unlocked = false;
WEAPONS.rifle.unlocked = false;
WEAPONS.plasma.unlocked = false;
switchWeapon('pistol');
updateThemeForWave();
showToast("ACCOUNT RESET! $5,000 CASH DEPOSITED.");
saveAccountData();
updateHUD();
updateShopUI();
}
function showToast(msg) {
const toast = document.getElementById('toast-msg');
toast.innerText = msg;
toast.style.display = 'block';
setTimeout(() => toast.style.display = 'none', 2600);
}
function animate() {
requestAnimationFrame(animate);
if (isGameActive && !isShopOpen) {
const time = performance.now();
const delta = (time - prevTime) / 1000;
// Automatic enemy spawner loop
if (time - lastSpawnTime > spawnRate && zombies.length < 25) {
spawnZombie();
lastSpawnTime = time;
}
// Physics movement
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
velocity.y -= 9.8 * 100.0 * delta;
direction.z = Number(moveForward) - Number(moveBackward);
direction.x = Number(moveRight) - Number(moveLeft);
direction.normalize();
const currentSpeed = (isRunning ? playerBaseSpeed * 1.7 : playerBaseSpeed) * speedMultiplier;
if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta * (currentSpeed / 40);
if (moveLeft || moveRight) velocity.x += direction.x * 400.0 * delta * (currentSpeed / 40);
camera.translateX(velocity.x * delta);
camera.translateZ(velocity.z * delta);
camera.position.y += (velocity.y * delta);
if (camera.position.y < 10) {
velocity.y = 0; camera.position.y = 10; canJump = true;
}
// Raycast target check for trees and loot crates
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const treeTrunks = trees.map(t => t.trunk);
const crateBoxes = lootCrates.filter(c => !c.looted).map(c => c.box);
const treeHits = raycaster.intersectObjects(treeTrunks);
const crateHits = raycaster.intersectObjects(crateBoxes);
const promptEl = document.getElementById('action-prompt');
if (treeHits.length > 0 && treeHits[0].distance < 18) {
const hitTrunk = treeHits[0].object;
targetedTree = trees.find(t => t.trunk === hitTrunk);
targetedCrate = null;
promptEl.innerText = "PRESS [E] TO CHOP TREE";
promptEl.style.display = 'block';
} else if (crateHits.length > 0 && crateHits[0].distance < 18) {
const hitBox = crateHits[0].object;
targetedCrate = lootCrates.find(c => c.box === hitBox);
targetedTree = null;
promptEl.innerText = "PRESS [E] TO LOOT CRATE";
promptEl.style.display = 'block';
} else {
targetedTree = null;
targetedCrate = null;
promptEl.style.display = 'none';
}
// Weapon recoil recovery & sway
weaponGroup.position.z += (0 - weaponGroup.position.z) * 12 * delta;
weaponGroup.rotation.x += (0 - weaponGroup.rotation.x) * 12 * delta;
if (muzzleFlashMesh.opacity > 0) muzzleFlashMesh.opacity -= delta * 18;
// Zombie AI - Persistent tracking & Structure Safe Zones
const isPlayerInsideBuilding = isPositionInsideBuilding(camera.position);
zombies.forEach(z => {
const zObj = z.mesh;
// Animate Rainbow Mini Zombies
if (z.isRainbow && zObj.material) {
const hue = (time * 0.001) % 1;
zObj.material.color.setHSL(hue, 1.0, 0.5);
}
let targetBarricade = null;
let minDist = 999;
barricades.forEach(b => {
const bDist = zObj.position.distanceTo(b.mesh.position);
if (bDist < 6 && bDist < minDist) {
minDist = bDist;
targetBarricade = b;
}
});
if (targetBarricade) {
targetBarricade.health -= delta * 30;
if (targetBarricade.health <= 0) {
scene.remove(targetBarricade.mesh);
barricades = barricades.filter(b => b !== targetBarricade);
playSound('chop');
}
} else if (!isPlayerInsideBuilding) {
// Only pursue player if player is NOT inside an abandoned building structure (Safe Zone)
zObj.lookAt(camera.position.x, zObj.position.y, camera.position.z);
const dist = zObj.position.distanceTo(camera.position);
if (dist > (z.isMegaBoss ? 8.0 : (z.isBoss ? 5.5 : 3.8))) {
zObj.translateZ(z.speed * delta);
} else {
if (Math.random() < 0.05) takeDamage(z.damage);
}
} else {
// Zombies stop at structure perimeters when player is safely inside
const distToPlayer = zObj.position.distanceTo(camera.position);
if (distToPlayer > 30) {
zObj.lookAt(camera.position.x, zObj.position.y, camera.position.z);
zObj.translateZ(z.speed * d elta * 0.3);
}
}
});
// Particles loop
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.mesh.position.addScaledVector(p.dir, delta * 12);
p.life -= delta * 2.5;
p.mesh.scale.setScalar(p.life);
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
renderRadar();
prevTime = time;
}
renderer.render(scene, camera);
}
</script>
</body>
</html>72,872 chars · codepastes.com