| <!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> |
| |
| :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; |
| } |
| |
| |
| .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 { |
| 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-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; |
| } |
| |
| |
| #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"> |
| |
| <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> |
| |
| 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; } |
| } |
| }; |
| |
| |
| 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 } |
| ]; |
| |
| |
| 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'); |
| |
| |
| 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); |
| |
| |
| document.addEventListener('keydown', (e) => { |
| |
| 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; |
| }); |
| |
| |
| 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]} ...`; |
| |
| |
| if (progress >= messageIndex * (100 / (loadingMessages.length - 1))) { |
| if (messageIndex < loadingMessages.length - 1) { |
| messageIndex++; |
| } |
| } |
| |
| |
| if (Math.random() < 0.05) { |
| loadingMessage.textContent = "*** Tape loading error ***"; |
| setTimeout(() => { |
| loadingMessage.textContent = `${loadingMessages[messageIndex]} ...`; |
| }, 500); |
| } |
| |
| if (progress === 100) { |
| clearInterval(interval); |
| setTimeout(() => { |
| |
| 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); |
| } |
| |
| |
| 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> |
| `; |
| |
| |
| 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; |
| |
| |
| gameState.health = 100; |
| gameState.score = 0; |
| gameState.level = 1; |
| gameState.monstersKilled = 0; |
| gameState.monstersSpawned = 0; |
| gameState.currentLocation = 0; |
| gameState.locationsCompleted = []; |
| gameState.projectiles = []; |
| gameState.monsters = []; |
| |
| |
| document.querySelectorAll('.monster').forEach(m => m.remove()); |
| document.querySelectorAll('.projectile').forEach(p => p.remove()); |
| |
| |
| updateHealth(); |
| updateScore(); |
| levelValue.textContent = gameState.level; |
| |
| |
| 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`; |
| |
| |
| startLocation(); |
| |
| |
| 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; |
| |
| |
| if (location.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`; |
| |
| |
| 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; |
| |
| |
| 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); |
| } |
| |
| |
| player.style.left = `${gameState.playerX}px`; |
| player.style.top = `${gameState.playerY}px`; |
| |
| |
| if (gameState.keys[' ']) { |
| if (gameState.lastShot === undefined || Date.now() - gameState.lastShot > 300) { |
| shoot(); |
| gameState.lastShot = Date.now(); |
| } |
| } |
| |
| |
| updateProjectiles(); |
| |
| |
| updateMonsters(); |
| |
| |
| checkCollisions(); |
| |
| |
| 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`; |
| |
| |
| 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) { |
| |
| monster.x += monster.speedX; |
| monster.y += monster.speedY; |
| |
| |
| if (monster.x <= 0 || monster.x + monster.width >= gameArea.clientWidth) { |
| monster.speedX *= -1; |
| } |
| |
| |
| 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 { |
| |
| 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; |
| |
| |
| 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, |
| fromMonster: true |
| }); |
| }, i * 300); |
| } |
| } |
| |
| function checkCollisions() { |
| |
| for (let i = gameState.projectiles.length - 1; i >= 0; i--) { |
| const projectile = gameState.projectiles[i]; |
| |
| if (projectile.fromMonster) { |
| |
| 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 { |
| |
| 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(); |
| |
| |
| 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; |
| } |
| } |
| } |
| } |
| |
| |
| 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); |
| |
| |
| monster.speedX = (monster.x < gameState.playerX) ? -3 : 3; |
| monster.speedY = -2; |
| } |
| } |
| |
| |
| monstersLeft.textContent = norfolkLocations[gameState.currentLocation].monsters - gameState.monstersKilled; |
| |
| |
| 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(); |
| |
| |
| 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}%`; |
| |
| |
| 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); |
| |
| |
| 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) { |
| |
| victory(); |
| } else { |
| |
| nextLocationBtn.style.display = 'block'; |
| } |
| } |
| |
| function nextLocation() { |
| gameState.currentLocation++; |
| gameState.level++; |
| levelValue.textContent = gameState.level; |
| |
| |
| 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`; |
| |
| |
| victoryScreen.style.display = 'none'; |
| nextLocationBtn.style.display = 'none'; |
| |
| |
| gameState.health = Math.min(100, gameState.health + 25); |
| updateHealth(); |
| |
| |
| startLocation(); |
| |
| |
| 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'; |
| |
| |
| norfolkMap.innerHTML = ''; |
| |
| |
| 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); |
| |
| |
| 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); |
| } |
| |
| |
| 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> |