norfolk / index.html
Altaire's picture
Add 1 files
01a783f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Altaire - Battle AI Monsters in Norfolk!</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* ZX Spectrum inspired color palette */
:root {
--zx-black: #000000;
--zx-blue: #0000d0;
--zx-red: #d00000;
--zx-magenta: #d000d0;
--zx-green: #00d000;
--zx-cyan: #00d0d0;
--zx-yellow: #d0d000;
--zx-white: #d0d0d0;
--zx-bright-blue: #0000ff;
--zx-bright-red: #ff0000;
--zx-bright-green: #00ff00;
}
body {
margin: 0;
padding: 0;
background-color: var(--zx-blue);
font-family: 'Courier New', monospace;
color: var(--zx-white);
overflow: hidden;
image-rendering: pixelated;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
border: 8px solid var(--zx-black);
background-color: var(--zx-black);
overflow: hidden;
}
#loading-screen {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--zx-blue);
color: var(--zx-white);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
}
#spectrum-loading {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--zx-black);
z-index: 101;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#loading-border {
width: 80%;
height: 20px;
border: 2px solid var(--zx-white);
margin-top: 20px;
position: relative;
overflow: hidden;
}
#loading-stripes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
to right,
var(--zx-bright-green) 0%,
var(--zx-bright-green) 20%,
var(--zx-black) 20%,
var(--zx-black) 40%
);
background-size: 40px 100%;
animation: stripeMove 0.5s linear infinite;
}
@keyframes stripeMove {
0% { background-position: 0 0; }
100% { background-position: 40px 0; }
}
#loading-progress {
height: 100%;
width: 0;
background-color: var(--zx-bright-green);
transition: width 0.3s;
position: relative;
z-index: 2;
}
#game-screen {
position: relative;
width: 100%;
height: 100%;
display: none;
}
#title-screen {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--zx-black);
color: var(--zx-white);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
#title {
font-size: 72px;
color: var(--zx-bright-red);
text-shadow: 0 0 10px var(--zx-red);
margin-bottom: 30px;
transform: rotate(-5deg);
animation: flicker 0.5s infinite alternate;
}
@keyframes flicker {
0% { opacity: 1; }
20% { opacity: 0.8; }
40% { opacity: 0.7; }
60% { opacity: 0.9; }
100% { opacity: 1; }
}
#press-start {
font-size: 24px;
color: var(--zx-white);
margin-top: 40px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
#menu {
margin-top: 30px;
}
.menu-item {
margin: 10px 0;
padding: 8px 20px;
background-color: var(--zx-blue);
color: var(--zx-white);
cursor: pointer;
border: 2px solid var(--zx-cyan);
transition: all 0.2s;
font-size: 20px;
}
.menu-item:hover {
background-color: var(--zx-bright-blue);
transform: scale(1.05);
}
#game-area {
position: relative;
width: 100%;
height: 100%;
background-color: var(--zx-black);
}
#player {
position: absolute;
width: 32px;
height: 32px;
background-color: var(--zx-bright-green);
z-index: 5;
}
.monster {
position: absolute;
width: 32px;
height: 32px;
z-index: 4;
animation: monsterFloat 2s infinite ease-in-out;
}
@keyframes monsterFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
.projectile {
position: absolute;
width: 8px;
height: 8px;
background-color: var(--zx-yellow);
z-index: 3;
}
#hud {
position: absolute;
top: 10px;
left: 10px;
padding: 5px 10px;
background-color: rgba(0, 0, 0, 0.7);
border: 2px solid var(--zx-white);
z-index: 10;
font-size: 18px;
}
#stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.stat {
margin-right: 20px;
color: var(--zx-white);
}
#health-bar {
width: 200px;
height: 15px;
background-color: var(--zx-red);
margin-top: 5px;
}
#health-fill {
height: 100%;
width: 100%;
background-color: var(--zx-bright-green);
transition: width 0.3s;
}
#location-display {
position: absolute;
bottom: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 5px 10px;
border: 2px solid var(--zx-white);
font-size: 18px;
}
#controls {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
padding: 5px 10px;
border: 2px solid var(--zx-white);
font-size: 16px;
}
#game-over {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 20;
}
#game-over h1 {
color: var(--zx-bright-red);
font-size: 72px;
margin-bottom: 20px;
}
#btn-retry {
margin-top: 20px;
padding: 10px 20px;
background-color: var(--zx-blue);
color: var(--zx-white);
border: 2px solid var(--zx-cyan);
cursor: pointer;
font-size: 20px;
}
#btn-retry:hover {
background-color: var(--zx-bright-blue);
}
#victory-screen {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 20;
}
#victory-screen h1 {
color: var(--zx-bright-green);
font-size: 72px;
margin-bottom: 20px;
animation: pulse 1s infinite;
}
#norfolk-map {
width: 80%;
max-width: 800px;
height: 60vh;
max-height: 600px;
background-color: var(--zx-blue);
border: 2px solid var(--zx-white);
position: relative;
margin: 20px 0;
}
.location-marker {
position: absolute;
width: 16px;
height: 16px;
background-color: var(--zx-red);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.completed-marker {
background-color: var(--zx-bright-green);
}
.active-marker {
background-color: var(--zx-yellow);
box-shadow: 0 0 10px var(--zx-yellow);
animation: pulse 0.5s infinite;
}
#locations-list {
margin-top: 20px;
text-align: left;
max-height: 150px;
overflow-y: auto;
width: 80%;
max-width: 600px;
}
.location-item {
padding: 8px 15px;
margin: 5px 0;
background-color: rgba(0, 0, 0, 0.5);
border-left: 5px solid var(--zx-blue);
font-size: 18px;
}
.location-item.completed {
border-left-color: var(--zx-bright-green);
}
.location-item.active {
border-left-color: var(--zx-yellow);
animation: active-border 1s infinite;
}
@keyframes active-border {
0% { border-left-color: var(--zx-yellow); }
50% { border-left-color: var(--zx-bright-red); }
100% { border-left-color: var(--zx-yellow); }
}
#next-location-btn {
margin-top: 20px;
padding: 10px 20px;
background-color: var(--zx-blue);
color: var(--zx-white);
border: 2px solid var(--zx-cyan);
cursor: pointer;
font-size: 20px;
}
#next-location-btn:hover {
background-color: var(--zx-bright-blue);
}
#final-boss {
position: absolute;
width: 64px;
height: 64px;
background-color: var(--zx-bright-red);
z-index: 4;
animation: bossGlow 1s infinite alternate;
display: none;
}
@keyframes bossGlow {
from { box-shadow: 0 0 15px var(--zx-bright-red); }
to { box-shadow: 0 0 30px var(--zx-bright-red); }
}
#cheats {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
padding: 5px 10px;
border: 2px solid var(--zx-red);
z-index: 30;
display: none;
}
#cheats input {
background-color: var(--zx-black);
color: var(--zx-white);
border: 1px solid var(--zx-red);
padding: 5px 10px;
font-family: 'Courier New', monospace;
font-size: 16px;
}
#cheats button {
background-color: var(--zx-red);
color: var(--zx-white);
border: none;
padding: 5px 10px;
cursor: pointer;
margin-left: 5px;
font-family: 'Courier New', monospace;
font-size: 16px;
}
/* ZX Spectrum style scanlines effect */
.scanlines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
to bottom,
transparent 0%,
transparent 1px,
rgba(0, 0, 0, 0.3) 2px,
rgba(0, 0, 0, 0.3) 3px
);
pointer-events: none;
z-index: 5;
}
/* Pixel grid overlay */
.pixel-grid {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(to right, rgba(192, 192, 192, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(192, 192, 192, 0.1) 1px, transparent 1px);
background-size: 4px 4px;
pointer-events: none;
z-index: 6;
}
/* CRT curvature effect */
.crt-effect::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) 45%,
rgba(0, 0, 0, 0.3) 46%,
rgba(0, 0, 0, 0.3) 54%,
rgba(0, 0, 0, 0) 55%,
rgba(0, 0, 0, 0) 100%
);
pointer-events: none;
z-index: 7;
}
/* Spectrum loading screen elements */
#loading-header {
font-family: 'Courier New', monospace;
text-align: center;
margin-bottom: 20px;
}
#loading-header h1 {
color: var(--zx-yellow);
font-size: 28px;
margin-bottom: 5px;
}
#loading-header h2 {
color: var(--zx-white);
font-size: 16px;
margin-top: 0;
}
#loading-info {
position: absolute;
bottom: 30px;
left: 0;
width: 100%;
text-align: center;
font-family: 'Courier New', monospace;
color: var(--zx-white);
}
#loading-details {
position: absolute;
top: 30%;
left: 20px;
color: var(--zx-white);
font-family: 'Courier New', monospace;
font-size: 14px;
}
#loading-message {
position: absolute;
bottom: 60px;
width: 100%;
text-align: center;
color: var(--zx-white);
font-family: 'Courier New', monospace;
font-size: 16px;
}
#spectrum-progress-bar {
position: relative;
width: 80%;
max-width: 400px;
height: 30px;
background: var(--zx-blue);
border: 2px solid var(--zx-white);
margin: 20px 0;
}
#spectrum-progress {
height: 100%;
width: 0;
background: var(--zx-bright-green);
transition: width 0.3s;
}
#spectrum-stripes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(to right,
var(--zx-bright-green) 0px,
var(--zx-bright-green) 4px,
var(--zx-blue) 4px,
var(--zx-blue) 8px);
}
</style>
</head>
<body>
<div id="game-container" class="crt-effect">
<!-- Spectrum loading screen -->
<div id="spectrum-loading">
<div id="loading-header">
<h1>ALT<span style="color: var(--zx-bright-red);">AI</span>RE</h1>
<h2>Battle AI Monsters in Norfolk</h2>
</div>
<div id="loading-details">
<div>Loading Altaire...</div>
<div>By AI Developer</div>
<div>2023</div>
<div>ZX Spectrum 48K</div>
</div>
<div id="spectrum-progress-bar">
<div id="spectrum-progress"></div>
<div id="spectrum-stripes"></div>
</div>
<div id="loading-message">Searching for Altaire...</div>
<div id="loading-info">Press any key to break</div>
</div>
<div id="loading-screen" style="display: none;">
<h1>ALT<span style="color: var(--zx-bright-red);">AI</span>RE</h1>
<p>Initializing digital defenses...</p>
<div id="loading-border">
<div id="loading-stripes"></div>
<div id="loading-progress"></div>
</div>
</div>
<div id="game-screen">
<div id="title-screen">
<div id="title">ALT<span style="color: var(--zx-bright-red);">AI</span>RE</div>
<p>BATTLE AI MONSTERS IN NORFOLK</p>
<div id="menu">
<div class="menu-item" id="start-game">START GAME</div>
<div class="menu-item" id="locations">NORFOLK LOCATIONS</div>
<div class="menu-item" id="instructions">INSTRUCTIONS</div>
</div>
<div id="press-start">PRESS START TO DEFEND NORFOLK</div>
</div>
<div id="game-area">
<div id="hud">
<div id="stats">
<div class="stat">HEALTH: <span id="health-value">100%</span></div>
<div class="stat">SCORE: <span id="score-value">0</span></div>
<div class="stat">LEVEL: <span id="level-value">1</span></div>
<div class="stat">MONSTERS: <span id="monsters-left">5</span></div>
</div>
<div id="health-bar">
<div id="health-fill"></div>
</div>
</div>
<div id="player"></div>
<div id="final-boss"></div>
<div id="location-display">
CURRENT LOCATION: <span id="location-name">GREAT YARMOUTH</span>
</div>
<div id="controls">
WASD: MOVE | SPACE: SHOOT | ESC: MENU | `: CHEATS
</div>
<div id="cheats">
<input type="text" id="cheat-input" placeholder="Enter cheat code">
<button id="submit-cheat">SUBMIT</button>
</div>
</div>
<div id="game-over">
<h1>GAME OVER</h1>
<p>THE AI HAVE OVERRUN NORFOLK!</p>
<p>YOUR FINAL SCORE: <span id="final-score">0</span></p>
<div id="btn-retry">TRY AGAIN</div>
</div>
<div id="victory-screen">
<h1>VICTORY!</h1>
<p>YOU'VE SAVED NORFOLK FROM THE AI MENACE!</p>
<div id="norfolk-map"></div>
<div id="locations-list"></div>
<p>TOTAL SCORE: <span id="total-score">0</span></p>
<div id="next-location-btn" style="display: none;">NEXT LOCATION</div>
</div>
</div>
<div class="scanlines"></div>
<div class="pixel-grid"></div>
</div>
<script>
// Game state
const gameState = {
health: 100,
score: 0,
level: 1,
monstersKilled: 0,
monstersSpawned: 0,
gameActive: false,
playerX: 0,
playerY: 0,
playerWidth: 32,
playerHeight: 32,
keys: {},
projectiles: [],
monsters: [],
currentLocation: 0,
locationsCompleted: [],
cheatCodes: {
'NORFOLK': () => { gameState.health = 100; updateHealth(); },
'YARMOUTH': () => { gameState.score += 1000; updateScore(); },
'KINGSLYNN': () => { spawnMonsters(3); },
'SUFFOLK': () => { document.getElementById('game-over').style.display = 'none'; gameState.gameActive = true; }
}
};
// Norfolk locations with positions for the map
const norfolkLocations = [
{ name: 'GREAT YARMOUTH', x: 80, y: 240, monsters: 5, boss: false },
{ name: 'NORWICH', x: 120, y: 160, monsters: 8, boss: false },
{ name: 'CROMER', x: 60, y: 80, monsters: 6, boss: false },
{ name: 'HOLT', x: 100, y: 60, monsters: 7, boss: false },
{ name: 'FAKENHAM', x: 150, y: 100, monsters: 6, boss: false },
{ name: 'DEREHAM', x: 180, y: 140, monsters: 7, boss: false },
{ name: 'ATTLEBOROUGH', x: 210, y: 180, monsters: 8, boss: false },
{ name: 'WYMONDHAM', x: 190, y: 200, monsters: 7, boss: false },
{ name: 'LOWESTOFT', x: 100, y: 230, monsters: 6, boss: false },
{ name: 'BECCLES', x: 130, y: 250, monsters: 5, boss: false },
{ name: 'KING\'S LYNN', x: 50, y: 150, monsters: 10, boss: true }
];
// DOM elements
const gameContainer = document.getElementById('game-container');
const spectrumLoading = document.getElementById('spectrum-loading');
const spectrumProgress = document.getElementById('spectrum-progress');
const loadingScreen = document.getElementById('loading-screen');
const loadingProgress = document.getElementById('loading-progress');
const loadingMessage = document.getElementById('loading-message');
const gameScreen = document.getElementById('game-screen');
const titleScreen = document.getElementById('title-screen');
const gameArea = document.getElementById('game-area');
const player = document.getElementById('player');
const finalBoss = document.getElementById('final-boss');
const hud = document.getElementById('hud');
const healthValue = document.getElementById('health-value');
const healthFill = document.getElementById('health-fill');
const scoreValue = document.getElementById('score-value');
const levelValue = document.getElementById('level-value');
const monstersLeft = document.getElementById('monsters-left');
const locationName = document.getElementById('location-name');
const gameOverScreen = document.getElementById('game-over');
const finalScore = document.getElementById('final-score');
const btnRetry = document.getElementById('btn-retry');
const victoryScreen = document.getElementById('victory-screen');
const totalScore = document.getElementById('total-score');
const norfolkMap = document.getElementById('norfolk-map');
const locationsList = document.getElementById('locations-list');
const nextLocationBtn = document.getElementById('next-location-btn');
const cheatInput = document.getElementById('cheat-input');
const submitCheat = document.getElementById('submit-cheat');
const cheatsPanel = document.getElementById('cheats');
// Menu buttons
document.getElementById('start-game').addEventListener('click', startGame);
document.getElementById('locations').addEventListener('click', showLocations);
document.getElementById('instructions').addEventListener('click', showInstructions);
btnRetry.addEventListener('click', restartGame);
nextLocationBtn.addEventListener('click', nextLocation);
submitCheat.addEventListener('click', submitCheatCode);
// Fix for loading screen hanging
document.addEventListener('keydown', (e) => {
// Bypass loading screen on any key press
if (spectrumLoading.style.display !== 'none') {
spectrumLoading.style.display = 'none';
loadingScreen.style.display = 'flex';
simulateLoading();
}
if (e.key === 'Escape') {
if (titleScreen.style.display === 'flex') {
resumeGame();
} else {
pauseGame();
}
}
if (e.key === '`' || e.key === '~') {
cheatsPanel.style.display = cheatsPanel.style.display === 'none' ? 'block' : 'none';
if (cheatsPanel.style.display === 'block') {
cheatInput.focus();
}
}
gameState.keys[e.key.toUpperCase()] = true;
});
document.addEventListener('keyup', (e) => {
gameState.keys[e.key.toUpperCase()] = false;
});
// Simulate ZX Spectrum tape loading
function simulateTapeLoading() {
const loadingMessages = [
"Searching for Altaire",
"Found Altaire",
"Reading block 1 of 10",
"Reading block 2 of 10",
"Reading block 3 of 10",
"Reading block 4 of 10",
"Reading block 5 of 10",
"Reading block 6 of 10",
"Reading block 7 of 10",
"Reading block 8 of 10",
"Reading block 9 of 10",
"Reading block 10 of 10",
"Loading complete"
];
let progress = 0;
let messageIndex = 0;
const interval = setInterval(() => {
progress += Math.random() * 10;
if (progress >= 100) progress = 100;
spectrumProgress.style.width = `${progress}%`;
loadingMessage.textContent = `${loadingMessages[messageIndex]} ...`;
// Update message every 10% progress
if (progress >= messageIndex * (100 / (loadingMessages.length - 1))) {
if (messageIndex < loadingMessages.length - 1) {
messageIndex++;
}
}
// Random tape "glitches"
if (Math.random() < 0.05) {
loadingMessage.textContent = "*** Tape loading error ***";
setTimeout(() => {
loadingMessage.textContent = `${loadingMessages[messageIndex]} ...`;
}, 500);
}
if (progress === 100) {
clearInterval(interval);
setTimeout(() => {
// Show modern loading screen
spectrumLoading.style.display = 'none';
loadingScreen.style.display = 'flex';
simulateLoading();
}, 1000);
}
}, 300);
}
function simulateLoading() {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 10;
if (progress > 100) progress = 100;
loadingProgress.style.width = `${progress}%`;
if (progress === 100) {
clearInterval(interval);
setTimeout(() => {
loadingScreen.style.display = 'none';
gameScreen.style.display = 'block';
}, 500);
}
}, 200);
}
// Start the loading simulation
simulateTapeLoading();
function showMainMenu() {
titleScreen.innerHTML = `
<div id="title">ALT<span style="color: var(--zx-bright-red);">AI</span>RE</div>
<p>BATTLE AI MONSTERS IN NORFOLK</p>
<div id="menu">
<div class="menu-item" id="start-game">START GAME</div>
<div class="menu-item" id="locations">NORFOLK LOCATIONS</div>
<div class="menu-item" id="instructions">INSTRUCTIONS</div>
</div>
<div id="press-start">PRESS START TO DEFEND NORFOLK</div>
`;
// Re-attach event listeners
document.getElementById('start-game').addEventListener('click', startGame);
document.getElementById('locations').addEventListener('click', showLocations);
document.getElementById('instructions').addEventListener('click', showInstructions);
}
function startGame() {
titleScreen.style.display = 'none';
gameState.gameActive = true;
// Reset game state
gameState.health = 100;
gameState.score = 0;
gameState.level = 1;
gameState.monstersKilled = 0;
gameState.monstersSpawned = 0;
gameState.currentLocation = 0;
gameState.locationsCompleted = [];
gameState.projectiles = [];
gameState.monsters = [];
// Clear any existing monsters
document.querySelectorAll('.monster').forEach(m => m.remove());
document.querySelectorAll('.projectile').forEach(p => p.remove());
// Update UI
updateHealth();
updateScore();
levelValue.textContent = gameState.level;
// Position player
gameState.playerX = (gameArea.clientWidth - gameState.playerWidth) / 2;
gameState.playerY = gameArea.clientHeight - gameState.playerHeight - 50;
player.style.left = `${gameState.playerX}px`;
player.style.top = `${gameState.playerY}px`;
// Set initial location
startLocation();
// Start game loop
requestAnimationFrame(gameLoop);
}
function startLocation() {
const location = norfolkLocations[gameState.currentLocation];
locationName.textContent = location.name;
const totalMonsters = location.monsters;
monstersLeft.textContent = totalMonsters;
gameState.monstersKilled = 0;
gameState.monstersSpawned = 0;
// Spawn initial monsters
if (location.boss) {
// Spawn the final boss
finalBoss.style.display = 'block';
finalBoss.style.left = `${(gameArea.clientWidth - 64) / 2}px`;
finalBoss.style.top = '50px';
gameState.monsters.push({
element: finalBoss,
x: (gameArea.clientWidth - 64) / 2,
y: 50,
width: 64,
height: 64,
health: 20,
speed: 2,
isBoss: true
});
gameState.monstersSpawned = 1;
} else {
spawnMonsters(2);
}
}
function spawnMonsters(count) {
for (let i = 0; i < count; i++) {
if (gameState.monstersSpawned >= norfolkLocations[gameState.currentLocation].monsters) {
return;
}
gameState.monstersSpawned++;
const monster = document.createElement('div');
monster.className = 'monster';
const monsterSize = 24 + Math.random() * 16;
const monsterX = Math.random() * (gameArea.clientWidth - monsterSize);
const monsterY = Math.random() * (gameArea.clientHeight / 2 - monsterSize);
monster.style.width = `${monsterSize}px`;
monster.style.height = `${monsterSize}px`;
monster.style.left = `${monsterX}px`;
monster.style.top = `${monsterY}px`;
// Random monster color
const colors = ['var(--zx-red)', 'var(--zx-magenta)', 'var(--zx-cyan)', 'var(--zx-yellow)'];
monster.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
gameArea.appendChild(monster);
gameState.monsters.push({
element: monster,
x: monsterX,
y: monsterY,
width: monsterSize,
height: monsterSize,
health: 2,
speedX: (Math.random() - 0.5) * 4,
speedY: Math.random() * 2 + 1,
isBoss: false
});
monstersLeft.textContent = norfolkLocations[gameState.currentLocation].monsters - gameState.monstersKilled;
}
}
function gameLoop() {
if (!gameState.gameActive) return;
// Player movement
const moveSpeed = 5;
if (gameState.keys['W'] || gameState.keys['ARROWUP']) {
gameState.playerY = Math.max(0, gameState.playerY - moveSpeed);
}
if (gameState.keys['S'] || gameState.keys['ARROWDOWN']) {
gameState.playerY = Math.min(gameArea.clientHeight - gameState.playerHeight, gameState.playerY + moveSpeed);
}
if (gameState.keys['A'] || gameState.keys['ARROWLEFT']) {
gameState.playerX = Math.max(0, gameState.playerX - moveSpeed);
}
if (gameState.keys['D'] || gameState.keys['ARROWRIGHT']) {
gameState.playerX = Math.min(gameArea.clientWidth - gameState.playerWidth, gameState.playerX + moveSpeed);
}
// Update player position
player.style.left = `${gameState.playerX}px`;
player.style.top = `${gameState.playerY}px`;
// Shooting
if (gameState.keys[' ']) {
if (gameState.lastShot === undefined || Date.now() - gameState.lastShot > 300) {
shoot();
gameState.lastShot = Date.now();
}
}
// Update projectiles
updateProjectiles();
// Update monsters
updateMonsters();
// Check for collisions
checkCollisions();
// Spawn more monsters if needed
if (gameState.monsters.length < 3 && gameState.monstersSpawned < norfolkLocations[gameState.currentLocation].monsters) {
spawnMonsters(1);
}
requestAnimationFrame(gameLoop);
}
function shoot() {
const projectile = document.createElement('div');
projectile.className = 'projectile';
const projectileX = gameState.playerX + gameState.playerWidth / 2 - 4;
const projectileY = gameState.playerY;
projectile.style.left = `${projectileX}px`;
projectile.style.top = `${projectileY}px`;
gameArea.appendChild(projectile);
gameState.projectiles.push({
element: projectile,
x: projectileX,
y: projectileY,
width: 8,
height: 8,
speed: 8
});
}
function updateProjectiles() {
for (let i = gameState.projectiles.length - 1; i >= 0; i--) {
const projectile = gameState.projectiles[i];
projectile.y -= projectile.speed;
projectile.element.style.top = `${projectile.y}px`;
// Remove if out of bounds
if (projectile.y < 0) {
gameArea.removeChild(projectile.element);
gameState.projectiles.splice(i, 1);
}
}
}
function updateMonsters() {
for (let i = gameState.monsters.length - 1; i >= 0; i--) {
const monster = gameState.monsters[i];
if (!monster.isBoss) {
// Regular monster movement
monster.x += monster.speedX;
monster.y += monster.speedY;
// Bounce off walls
if (monster.x <= 0 || monster.x + monster.width >= gameArea.clientWidth) {
monster.speedX *= -1;
}
// Reverse direction when hitting bottom
if (monster.y + monster.height >= gameArea.clientHeight) {
monster.speedY *= -1;
}
monster.element.style.left = `${monster.x}px`;
monster.element.style.top = `${monster.y}px`;
} else {
// Boss movement pattern
if (monster.moveDirection === undefined) {
monster.moveDirection = Math.random() < 0.5 ? 1 : -1;
monster.moveCounter = Math.random() * 100 + 50;
}
monster.x += monster.speed * monster.moveDirection;
monster.moveCounter--;
if (monster.moveCounter <= 0 ||
monster.x <= 0 ||
monster.x + monster.width >= gameArea.clientWidth) {
monster.moveDirection *= -1;
monster.moveCounter = Math.random() * 100 + 50;
// Boss occasionally shoots
if (Math.random() < 0.2) {
bossShoot(monster);
}
}
monster.element.style.left = `${monster.x}px`;
}
}
}
function bossShoot(boss) {
for (let i = 0; i < 3; i++) {
setTimeout(() => {
const projectile = document.createElement('div');
projectile.className = 'projectile';
projectile.style.backgroundColor = 'var(--zx-bright-red)';
const projectileX = boss.x + boss.width / 2 - 4;
const projectileY = boss.y + boss.height;
projectile.style.left = `${projectileX}px`;
projectile.style.top = `${projectileY}px`;
gameArea.appendChild(projectile);
gameState.projectiles.push({
element: projectile,
x: projectileX,
y: projectileY,
width: 8,
height: 8,
speed: -5, // Moves downward
fromMonster: true
});
}, i * 300);
}
}
function checkCollisions() {
// Check projectile to monster collisions
for (let i = gameState.projectiles.length - 1; i >= 0; i--) {
const projectile = gameState.projectiles[i];
if (projectile.fromMonster) {
// Monsters shooting at player
if (checkCollision(projectile, {
x: gameState.playerX,
y: gameState.playerY,
width: gameState.playerWidth,
height: gameState.playerHeight
})) {
gameArea.removeChild(projectile.element);
gameState.projectiles.splice(i, 1);
takeDamage(10);
continue;
}
} else {
// Player shooting at monsters
for (let j = gameState.monsters.length - 1; j >= 0; j--) {
const monster = gameState.monsters[j];
if (checkCollision(projectile, monster)) {
gameArea.removeChild(projectile.element);
gameState.projectiles.splice(i, 1);
monster.health--;
if (monster.health <= 0) {
gameArea.removeChild(monster.element);
gameState.monsters.splice(j, 1);
gameState.monstersKilled++;
gameState.score += monster.isBoss ? 500 : 100;
updateScore();
// Show explosion effect for boss
if (monster.isBoss) {
const explosion = document.createElement('div');
explosion.style.position = 'absolute';
explosion.style.left = `${monster.x}px`;
explosion.style.top = `${monster.y}px`;
explosion.style.width = `${monster.width}px`;
explosion.style.height = `${monster.height}px`;
explosion.style.backgroundColor = 'var(--zx-bright-red)';
explosion.style.zIndex = '100';
explosion.style.borderRadius = '50%';
explosion.style.animation = 'flicker 0.1s 10 alternate';
gameArea.appendChild(explosion);
setTimeout(() => {
gameArea.removeChild(explosion);
}, 1000);
}
}
break;
}
}
}
}
// Check monster to player collisions
for (let i = gameState.monsters.length - 1; i >= 0; i--) {
const monster = gameState.monsters[i];
if (!monster.isBoss && checkCollision({
x: gameState.playerX,
y: gameState.playerY,
width: gameState.playerWidth,
height: gameState.playerHeight
}, monster)) {
takeDamage(5);
// Push monster away
monster.speedX = (monster.x < gameState.playerX) ? -3 : 3;
monster.speedY = -2;
}
}
// Update monsters left display
monstersLeft.textContent = norfolkLocations[gameState.currentLocation].monsters - gameState.monstersKilled;
// Check if location is cleared
if (gameState.monstersKilled >= norfolkLocations[gameState.currentLocation].monsters) {
locationCleared();
}
}
function checkCollision(obj1, obj2) {
return obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y;
}
function takeDamage(amount) {
gameState.health -= amount;
updateHealth();
// Flash player
player.style.backgroundColor = 'var(--zx-bright-red)';
setTimeout(() => {
player.style.backgroundColor = 'var(--zx-bright-green)';
}, 100);
if (gameState.health <= 0) {
gameOver();
}
}
function updateHealth() {
const healthPercent = Math.max(0, gameState.health);
healthValue.textContent = `${healthPercent}%`;
healthFill.style.width = `${healthPercent}%`;
// Change color based on health
if (healthPercent > 60) {
healthFill.style.backgroundColor = 'var(--zx-bright-green)';
} else if (healthPercent > 30) {
healthFill.style.backgroundColor = 'var(--zx-yellow)';
} else {
healthFill.style.backgroundColor = 'var(--zx-bright-red)';
}
}
function updateScore() {
scoreValue.textContent = gameState.score;
}
function locationCleared() {
gameState.gameActive = false;
gameState.locationsCompleted.push(gameState.currentLocation);
// Flash background
gameArea.style.backgroundColor = 'var(--zx-bright-green)';
setTimeout(() => {
gameArea.style.backgroundColor = 'var(--zx-black)';
}, 100);
setTimeout(() => {
gameArea.style.backgroundColor = 'var(--zx-bright-green)';
}, 200);
setTimeout(() => {
gameArea.style.backgroundColor = 'var(--zx-black)';
}, 300);
if (gameState.currentLocation === norfolkLocations.length - 1) {
// Final victory
victory();
} else {
// Show next location button
nextLocationBtn.style.display = 'block';
}
}
function nextLocation() {
gameState.currentLocation++;
gameState.level++;
levelValue.textContent = gameState.level;
// Reset player position
gameState.playerX = (gameArea.clientWidth - gameState.playerWidth) / 2;
gameState.playerY = gameArea.clientHeight - gameState.playerHeight - 50;
player.style.left = `${gameState.playerX}px`;
player.style.top = `${gameState.playerY}px`;
// Hide victory screen and show game
victoryScreen.style.display = 'none';
nextLocationBtn.style.display = 'none';
// Restore 25% health or to 100%, whichever is smaller
gameState.health = Math.min(100, gameState.health + 25);
updateHealth();
// Start new location
startLocation();
// Resume game
gameState.gameActive = true;
requestAnimationFrame(gameLoop);
}
function gameOver() {
gameState.gameActive = false;
gameOverScreen.style.display = 'flex';
finalScore.textContent = gameState.score;
}
function victory() {
gameState.gameActive = false;
victoryScreen.style.display = 'flex';
totalScore.textContent = gameState.score;
nextLocationBtn.style.display = 'none';
// Create Norfolk map with completion markers
norfolkMap.innerHTML = '';
// Draw Norfolk coastline approximation
const coastline = document.createElement('div');
coastline.style.position = 'absolute';
coastline.style.backgroundColor = 'var(--zx-green)';
coastline.style.width = '80%';
coastline.style.height = '80%';
coastline.style.top = '10%';
coastline.style.left = '10%';
coastline.style.borderRadius = '0 120px 120px 0';
norfolkMap.appendChild(coastline);
// Add location markers
for (let i = 0; i < norfolkLocations.length; i++) {
const marker = document.createElement('div');
marker.className = 'location-marker';
if (gameState.locationsCompleted.includes(i)) {
marker.classList.add('completed-marker');
}
if (i === gameState.currentLocation) {
marker.classList.add('active-marker');
}
marker.style.left = `${norfolkLocations[i].x * (norfolkMap.clientWidth / 400)}px`;
marker.style.top = `${norfolkLocations[i].y * (norfolkMap.clientHeight / 300)}px`;
marker.title = norfolkLocations[i].name;
norfolkMap.appendChild(marker);
}
// Populate locations list
locationsList.innerHTML = '';
for (let i = 0; i < norfolkLocations.length; i++) {
const item = document.createElement('div');
item.className = 'location-item';
if (gameState.locationsCompleted.includes(i)) {
item.classList.add('completed');
item.innerHTML = `<i class="fas fa-check"></i> ${norfolkLocations[i].name} (CLEARED)`;
} else if (i === gameState.currentLocation) {
item.classList.add('active');
item.innerHTML = `<i class="fas fa-skull-crossbones"></i> ${norfolkLocations[i].name} (CURRENT)`;
} else {
item.innerHTML = `<i class="fas fa-lock"></i> ${norfolkLocations[i].name}`;
}
locationsList.appendChild(item);
}
}
function restartGame() {
gameOverScreen.style.display = 'none';
startGame();
}
function pauseGame() {
gameState.gameActive = false;
titleScreen.style.display = 'flex';
document.getElementById('start-game').textContent = 'RESUME GAME';
}
function resumeGame() {
titleScreen.style.display = 'none';
gameState.gameActive = true;
requestAnimationFrame(gameLoop);
}
function showLocations() {
titleScreen.innerHTML = `
<h2 style="font-size: 36px;">NORFOLK LOCATIONS</h2>
<div style="max-width: 800px; margin: 20px auto; display: flex; flex-wrap: wrap; justify-content: center;">
${norfolkLocations.map((loc, i) => `
<div style="margin: 10px; padding: 10px; background: var(--zx-blue); width: 200px; border: 2px solid var(--zx-cyan);">
<div style="font-size: 18px; font-weight: bold;">${loc.name}</div>
<div>${loc.monsters} monsters${loc.boss ? ' + FINAL BOSS' : ''}</div>
<div style="width: 100%; height: 5px; background: var(--zx-red); margin-top: 5px;">
<div style="height: 100%; width: ${(loc.monsters / 10) * 100}%; background: var(--zx-bright-green);"></div>
</div>
</div>
`).join('')}
</div>
<div class="menu-item" id="back-to-menu" style="margin-top: 20px;">BACK</div>
`;
document.getElementById('back-to-menu').addEventListener('click', () => {
showMainMenu();
});
}
function showInstructions() {
titleScreen.innerHTML = `
<h2 style="font-size: 36px;">HOW TO PLAY</h2>
<div style="max-width: 800px; text-align: left; margin: 20px auto; font-size: 18px;">
<p><strong>OBJECTIVE:</strong> Defeat all AI monsters invading Norfolk's locations!</p>
<div style="display: flex; flex-wrap: wrap; justify-content: space-around; margin: 30px 0;">
<div style="width: 150px; margin: 15px; text-align: center;">
<div style="width: 80px; height: 80px; background: var(--zx-bright-green); margin: 0 auto 10px;"></div>
<p>PLAYER SHIP</p>
<p>Move with WASD or Arrow Keys</p>
<p>Press SPACE to shoot</p>
</div>
<div style="width: 150px; margin: 15px; text-align: center;">
<div style="width: 60px; height: 60px; background: var(--zx-red); margin: 0 auto 10px; border-radius: 50%;"></div>
<p>REGULAR MONSTERS</p>
<p>2 hits to defeat</p>
<p>Avoid contact</p>
</div>
<div style="width: 150px; margin: 15px; text-align: center;">
<div style="width: 64px; height: 64px; background: var(--zx-bright-red); margin: 0 auto 10px; animation: bossGlow 1s infinite alternate;"></div>
<p>FINAL BOSS</p>
<p>20 hits to defeat</p>
<p>Shoots projectiles</p>
</div>
</div>
<p><strong>CONTROLS:</strong></p>
<ul>
<li>WASD or Arrow Keys - Move your ship</li>
<li>SPACE - Fire projectiles</li>
<li>ESC - Pause game/open menu</li>
<li>` - Toggle cheat console</li>
</ul>
<p><strong>GAMEPLAY:</strong></p>
<ul>
<li>Clear all monsters in each Norfolk location</li>
<li>Defeat 10 locations to reach the final boss</li>
<li>Health regenerates between locations</li>
<li>Watch out for enemy projectiles</li>
</ul>
</div>
<div class="menu-item" id="back-to-menu" style="margin-top: 20px;">BACK TO MAIN MENU</div>
`;
document.getElementById('back-to-menu').addEventListener('click', () => {
showMainMenu();
});
}
function submitCheatCode() {
const code = cheatInput.value.toUpperCase();
if (gameState.cheatCodes[code]) {
gameState.cheatCodes[code]();
cheatInput.value = '';
cheatsPanel.style.display = 'none';
} else {
cheatInput.value = '';
alert('Invalid cheat code!');
}
}
</script>
</body>
</html>