<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
<title>Super Star Trek: TOS Console</title>
<meta name=”theme-color” content=”#e6b800″>
<meta name=”mobile-web-app-capable” content=”yes”>
<meta name=”apple-mobile-web-app-capable” content=”yes”>
<meta name=”apple-mobile-web-app-status-bar-style” content=”black-translucent”>
<meta name=”apple-mobile-web-app-title” content=”Star Trek”>
<link rel=”apple-touch-icon” href=”data:image/svg+xml;charset=utf-8,%3Csvg xmlns=’http://www.w3.org/2000/svg’ viewBox=’0 0 512 512’%3E%3Crect width=’512′ height=’512′ fill=’%23222’/%3E%3Cpath d=’M256 100 L400 400 L256 320 L112 400 Z’ fill=’%23e6b800’/%3E%3C/svg%3E”>
<link rel=”manifest” href=”data:application/json;charset=utf-8,%7B%22name%22%3A%22Super%20Star%20Trek%22%2C%22short_name%22%3A%22Star%20Trek%22%2C%22start_url%22%3A%22.%22%2C%22display%22%3A%22standalone%22%2C%22background_color%22%3A%22%23222222%22%2C%22theme_color%22%3A%22%23e6b800%22%2C%22icons%22%3A%5B%7B%22src%22%3A%22data%3Aimage%2Fsvg%2Bxml%3Bcharset%3Dutf-8%2C%253Csvg%20xmlns%3D’http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg’%20viewBox%3D’0%200%20512%20512’%253E%253Crect%20width%3D’512’%20height%3D’512’%20fill%3D’%2523222’%2F%253E%253Cpath%20d%3D’M256%20100%20L400%20400%20L256%20320%20L112%20400%20Z’%20fill%3D’%2523e6b800’%2F%253E%253C%2Fsvg%253E%22%2C%22sizes%22%3A%22512×512%22%2C%22type%22%3A%22image%2Fsvg%2Bxml%22%2C%22purpose%22%3A%22any%20maskable%22%7D%5D%7D”>
<style>
@import url(‘https://fonts.googleapis.com/css2?family=Oswald:wght@500;700&family=VT323&display=swap’);
:root {
–tos-gold: #e6b800;
–tos-blue: #0077cc;
–tos-red: #d32f2f;
–tos-green: #388e3c;
–tos-orange: #f57c00;
–tos-purple: #7b1fa2;
–tos-light-blue: #03a9f4;
–console-bg: #2a2d34;
–panel-bg: #1a1c20;
–bezel: #5a5f68;
–font-main: ‘Oswald’, sans-serif;
–font-screen: ‘VT323’, monospace;
}
html, body {
background-color: var(–console-bg);
color: #ffffff;
font-family: var(–font-main);
text-transform: uppercase;
margin: 0; padding: 0;
width: 100vw; height: 100vh; height: 100dvh;
overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none;
}
* { box-sizing: border-box; }
h1, h2, p { margin: 0; padding: 0; text-transform: uppercase; }
.font-screen { font-family: var(–font-screen); text-transform: none; }
.text-center { text-align: center; }
.tos-btn {
cursor: pointer; border: 3px solid #000;
border-top-color: rgba(255,255,255,0.4); border-left-color: rgba(255,255,255,0.4);
border-bottom-color: rgba(0,0,0,0.6); border-right-color: rgba(0,0,0,0.6);
color: white; font-family: var(–font-main); font-weight: 700;
display: flex; flex-direction: column; align-items: center; justify-content: center;
border-radius: 4px; box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
transition: transform 0.05s, border-color 0.05s; padding: 2px; text-align: center;
overflow: hidden; line-height: 1.1;
}
.tos-btn:active, .btn-active {
border-bottom-color: rgba(255,255,255,0.4); border-right-color: rgba(255,255,255,0.4);
border-top-color: rgba(0,0,0,0.6); border-left-color: rgba(0,0,0,0.6);
transform: translate(2px, 2px); box-shadow: 0px 0px 2px rgba(0,0,0,0.5);
}
.btn-active { filter: brightness(1.3); }
.btn-gold { background-color: var(–tos-gold); color: #000; }
.btn-blue { background-color: var(–tos-blue); }
.btn-red { background-color: var(–tos-red); }
.btn-green { background-color: var(–tos-green); }
.btn-orange { background-color: var(–tos-orange); }
.btn-purple { background-color: var(–tos-purple); }
.indicator {
width: 16px; height: 16px; border-radius: 50%; border: 2px solid #000;
box-shadow: inset -2px -2px 4px rgba(0,0,0,0.5), inset 2px 2px 4px rgba(255,255,255,0.5);
}
.ind-red { background-color: #880000; }
.ind-red.on { background-color: #ff3333; box-shadow: 0 0 10px #ff3333, inset -2px -2px 4px rgba(0,0,0,0.5); }
.ind-yellow { background-color: #888800; }
.ind-yellow.on { background-color: #ffff33; box-shadow: 0 0 10px #ffff33, inset -2px -2px 4px rgba(0,0,0,0.5); }
.ind-green { background-color: #006600; }
.ind-green.on { background-color: #33ff33; box-shadow: 0 0 10px #33ff33, inset -2px -2px 4px rgba(0,0,0,0.5); }
#interface-container {
display: grid; grid-template-columns: 65px 1fr; grid-template-rows: 45px 1fr 150px;
height: 100%; width: 100%; gap: 6px; padding: 6px;
background: linear-gradient(to bottom, #3a3f47, #1a1c20);
}
#top-bar {
grid-column: 1 / 3; background-color: var(–panel-bg); border: 3px solid #111; border-radius: 6px;
display: flex; align-items: center; justify-content: space-between; padding: 0 10px;
box-shadow: inset 0 0 10px #000;
}
.header-title { font-size: clamp(1rem, 4vw, 1.5rem); color: var(–tos-gold); letter-spacing: 1px; }
.header-lights { display: flex; gap: 8px; }
.header-right { display: flex; align-items: center; gap: 8px; }
.header-date { font-size: clamp(0.9rem, 3vw, 1.2rem); color: #fff; }
#btn-quit { display: none; padding: 2px 6px; font-size: 0.75rem; height: 24px; border-width: 2px; }
#side-panel { grid-column: 1 / 2; display: flex; flex-direction: column; gap: 6px; }
.stat-box {
flex: 1; background-color: var(–panel-bg); border: 3px solid #111; border-radius: 4px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
box-shadow: inset 0 0 8px #000;
}
.stat-label { font-size: clamp(0.5rem, 2vw, 0.65rem); color: #aaa; margin-bottom: -4px; text-align: center; line-height: 1; }
.stat-val { font-size: clamp(1rem, 4vw, 1.2rem); font-weight: bold; color: var(–tos-light-blue); line-height: 1; margin-top: 2px;}
.stat-val.warning { color: var(–tos-red); animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.2; } }
#main-view {
grid-column: 2 / 3; background-color: #050a15; border: 8px solid var(–bezel);
border-radius: 15px 15px 5px 5px; box-shadow: 0 0 0 3px #111, inset 0 0 20px #000;
display: flex; flex-direction: column; overflow: hidden; position: relative;
}
.red-alert-pulse { animation: redAlertCRT 2s infinite; }
@keyframes redAlertCRT {
0% { box-shadow: inset 0 0 0px rgba(255, 0, 0, 0); }
50% { box-shadow: inset 0 0 80px rgba(255, 0, 0, 0.7); }
100% { box-shadow: inset 0 0 0px rgba(255, 0, 0, 0); }
}
#canvas-container { flex: 1; min-height: 0; width: 100%; position: relative; }
canvas { display: block; width: 100%; height: 100%; cursor: crosshair; touch-action: none; position: absolute; top:0; left:0;}
#console-log {
height: 70px; background: rgba(0, 10, 0, 0.9); border-top: 2px solid #333; color: #33ff33;
padding: 4px 6px; overflow-y: auto; font-size: clamp(0.9rem, 3.5vw, 1.1rem);
display: flex; flex-direction: column-reverse; z-index: 10;
}
.log-entry { margin-bottom: 2px; line-height: 1; }
.log-alert { color: #ff3333; }
.log-info { color: #55aaff; }
.log-success { color: #ffff33; }
.log-chatter { color: #ff99ff; }
.log-computer { color: #ffffff; background: #0055ff; padding: 0 4px; display: inline-block;}
#bottom-bar {
grid-column: 1 / 3; background-color: var(–panel-bg); border: 3px solid #111; border-radius: 6px;
padding: 6px; display: grid; grid-template-columns: repeat(3, 1fr) 1.2fr;
grid-template-rows: repeat(2, 1fr); gap: 6px; box-shadow: inset 0 0 10px #000;
}
.btn-label { font-size: clamp(0.8rem, 3.5vw, 1.2rem); line-height: 1; }
.btn-sub { font-size: clamp(0.5rem, 2vw, 0.65rem); margin-top: 2px; opacity: 0.8; letter-spacing: 0.5px; }
.btn-shield { grid-column: 4 / 5; grid-row: 1 / 3; }
.overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(0,0,0,0.85); z-index: 20; padding: 10px; backdrop-filter: blur(2px);
}
.modal {
background: var(–panel-bg); border: 4px solid var(–bezel); border-radius: 6px;
padding: 12px; text-align: center; z-index: 30; width: 100%; max-width: 350px;
box-shadow: 0 0 20px #000;
}
.modal-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-bottom: 0.75rem; }
.modal-btn { padding: 4px 0; font-size: clamp(0.75rem, 3vw, 1rem); white-space: nowrap; line-height: 1.2;}
.modal-title { font-size: clamp(1.2rem, 5vw, 1.5rem); color: var(–tos-gold); margin-bottom: 0.5rem; line-height: 1;}
.modal-label { grid-column: span 4; text-align: left; font-size: clamp(0.65rem, 2.5vw, 0.8rem); color: #ccc; margin-top: 0.5rem; margin-bottom: 0; }
.modal-cancel { width: 100%; padding: 6px; font-size: clamp(1rem, 4vw, 1.2rem); }
.btn-pill { border-radius: 9999px; padding: 8px 20px; font-size: clamp(1.2rem, 5vw, 1.5rem); margin-top: 1rem; }
.hidden { display: none !important; }
@media (min-width: 768px) {
#interface-container { grid-template-columns: 100px 1fr; grid-template-rows: 60px 1fr 100px; padding: 15px; gap: 15px; }
.header-title { font-size: 2rem; }
.header-lights { gap: 12px; }
.indicator { width: 20px; height: 20px; }
.stat-label { font-size: 0.8rem; }
.stat-val { font-size: 1.8rem; }
#main-view { border-width: 12px; }
#console-log { height: 100px; font-size: 1.3rem; }
#bottom-bar { grid-template-columns: repeat(6, 1fr) 2fr; grid-template-rows: 1fr; gap: 10px; padding: 10px;}
.btn-shield { grid-column: auto; grid-row: auto; }
.btn-label { font-size: 1.5rem; }
.btn-sub { font-size: 0.8rem; margin-top: 4px;}
.tos-btn { border-width: 4px; padding: 6px; }
.modal-grid { gap: 8px; }
.modal-btn { padding: 8px 0; font-size: 1.1rem; }
.modal-label { margin-bottom: -4px; }
#btn-quit { font-size: 1rem; padding: 4px 10px; height: auto;}
}
</style>
</head>
<body>
<div id=”interface-container”>
<div id=”top-bar”>
<div class=”header-lights”>
<div id=”ind-1″ class=”indicator ind-red”></div>
<div id=”ind-2″ class=”indicator ind-yellow”></div>
<div id=”ind-3″ class=”indicator ind-green on”></div>
</div>
<div class=”header-title”>STARFLEET TACTICAL</div>
<div class=”header-right”>
<div class=”header-date”>SD: <span id=”ui-stardate”>3000.0</span></div>
<button id=”btn-quit” class=”tos-btn btn-red”>QUIT</button>
</div>
</div>
<div id=”side-panel”>
<div class=”stat-box”>
<span class=”stat-label”>ENERGY</span>
<span id=”ui-energy” class=”stat-val”>3000</span>
</div>
<div class=”stat-box”>
<span class=”stat-label”>SHIELDS</span>
<span id=”ui-shields” class=”stat-val”>0</span>
</div>
<div class=”stat-box”>
<span class=”stat-label”>TORPS</span>
<span id=”ui-torps” class=”stat-val” style=”color:var(–tos-orange)”>10</span>
</div>
<div class=”stat-box”>
<span class=”stat-label”>KLINGONS</span>
<span id=”ui-klingons” class=”stat-val” style=”color:var(–tos-red)”>0</span>
</div>
</div>
<div id=”main-view”>
<div id=”canvas-container”>
<canvas id=”sensorCanvas”></canvas>
<div id=”start-screen” class=”overlay”>
<h1 style=”font-size: clamp(2rem, 8vw, 3rem); color: var(–tos-gold); text-shadow: 2px 2px 0 #000; margin-bottom: 0.5rem; line-height: 1;”>STAR TREK</h1>
<div class=”font-screen” style=”color: #ccc; text-align: center; max-width: 320px; font-size: clamp(1rem, 4vw, 1.2rem); margin-bottom: 1rem; line-height: 1.2;”>
<p>The Klingon Empire has invaded.</p>
<p>You have <span style=”color: #fff; font-weight:bold;”>30 stardates</span> to destroy them.</p>
</div>
<button id=”btn-start” class=”tos-btn btn-gold btn-pill”>ENGAGE</button>
</div>
<div id=”game-over” class=”overlay hidden”>
<h1 id=”go-title” style=”font-size: clamp(2rem, 8vw, 3rem); color: var(–tos-red); text-shadow: 2px 2px 0 #000; text-align: center; line-height: 1;”>MISSION FAILED</h1>
<p id=”go-reason” class=”font-screen” style=”color: #fff; font-size: clamp(1.1rem, 4vw, 1.4rem); margin: 1rem 0; text-align:center;”>The Enterprise was destroyed.</p>
<button id=”btn-restart” class=”tos-btn btn-gold btn-pill”>RETRY</button>
</div>
<div id=”input-modal” class=”overlay hidden”>
<div class=”modal”>
<h2 id=”modal-title” class=”modal-title”>ALLOCATE POWER</h2>
<div id=”modal-buttons” class=”modal-grid”></div>
<button class=”tos-btn btn-orange modal-cancel” onclick=”closeModal()”>CANCEL</button>
</div>
</div>
</div>
<div id=”console-log” class=”font-screen”></div>
</div>
<div id=”bottom-bar”>
<button id=”btn-srs” class=”tos-btn btn-blue btn-active”>
<span class=”btn-label”>SRS</span><span class=”btn-sub”>SENSORS</span>
</button>
<button id=”btn-lrs” class=”tos-btn btn-blue”>
<span class=”btn-label”>LRS</span><span class=”btn-sub”>GALAXY</span>
</button>
<button id=”btn-imp” class=”tos-btn btn-gold”>
<span class=”btn-label”>IMP</span><span class=”btn-sub”>MOVE</span>
</button>
<button id=”btn-wrp” class=”tos-btn btn-purple”>
<span class=”btn-label”>WRP</span><span class=”btn-sub”>WARP</span>
</button>
<button id=”btn-pha” class=”tos-btn btn-red text-white”>
<span class=”btn-label”>PHA</span><span class=”btn-sub”>PHASERS</span>
</button>
<button id=”btn-tor” class=”tos-btn btn-orange”>
<span class=”btn-label”>TOR</span><span class=”btn-sub”>TORPEDO</span>
</button>
<button id=”btn-shd” class=”tos-btn btn-green btn-shield”>
<span class=”btn-label”>SHD</span><span class=”btn-sub”>SHIELDS</span>
</button>
</div>
</div>
<script>
// — WEB AUDIO API SOUND ENGINE —
const sfx = {
ctx: null, masterGain: null, redAlertLoop: null,
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.masterGain.connect(this.ctx.destination);
}
if (this.ctx.state === ‘suspended’) this.ctx.resume();
this.masterGain.gain.setValueAtTime(1, this.ctx.currentTime); // Ensure unmuted
},
playOsc(type, fStart, fEnd, dur, vol=0.1) {
if(!this.ctx || this.ctx.state === ‘suspended’) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type; osc.connect(gain); gain.connect(this.masterGain);
osc.frequency.setValueAtTime(fStart, t);
if(fEnd) osc.frequency.exponentialRampToValueAtTime(fEnd, t + dur);
gain.gain.setValueAtTime(vol, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + dur);
osc.start(t); osc.stop(t + dur);
},
click() { this.playOsc(‘sine’, 1200, 800, 0.05, 0.05); },
error() { this.playOsc(‘sawtooth’, 150, 100, 0.3, 0.1); },
phaser() { this.playOsc(‘sawtooth’, 800, 100, 0.3, 0.1); },
torpedo() { this.playOsc(‘square’, 200, 40, 0.6, 0.15); },
warp() { this.playOsc(‘sine’, 50, 2000, 1.5, 0.1); },
decloak() { this.playOsc(‘sine’, 1500, 200, 1.2, 0.1); },
purr() {
if(!this.ctx || this.ctx.state === ‘suspended’) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain();
osc.type = ‘triangle’; osc.frequency.value = 60;
osc.connect(gain); gain.connect(this.masterGain);
gain.gain.setValueAtTime(0, t); gain.gain.linearRampToValueAtTime(0.2, t + 0.2); gain.gain.linearRampToValueAtTime(0, t + 0.6);
osc.frequency.setValueAtTime(60, t); osc.frequency.exponentialRampToValueAtTime(40, t + 0.6);
osc.start(t); osc.stop(t + 0.6);
},
noiseBurst() {
if(!this.ctx || this.ctx.state === ‘suspended’) return;
const bufferSize = this.ctx.sampleRate * 0.4;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 – 1;
const noise = this.ctx.createBufferSource(); noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter(); filter.type = ‘highpass’; filter.frequency.value = 1000;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.4, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.4);
noise.connect(filter); filter.connect(gain); gain.connect(this.masterGain);
noise.start();
},
explosion() {
if(!this.ctx || this.ctx.state === ‘suspended’) return;
const bufferSize = this.ctx.sampleRate * 0.5;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 – 1;
const noise = this.ctx.createBufferSource(); noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter(); filter.type = ‘lowpass’; filter.frequency.value = 800;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.3, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.5);
noise.connect(filter); filter.connect(gain); gain.connect(this.masterGain);
noise.start();
},
startRedAlert() {
if(!this.ctx || this.redAlertLoop) return;
this.redAlertLoop = setInterval(() => {
if(this.ctx.state === ‘suspended’) return;
const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain();
osc.connect(gain); gain.connect(this.masterGain);
osc.frequency.setValueAtTime(400, this.ctx.currentTime); osc.frequency.linearRampToValueAtTime(800, this.ctx.currentTime + 0.4);
gain.gain.setValueAtTime(0.05, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0.01, this.ctx.currentTime + 0.5);
osc.start(this.ctx.currentTime); osc.stop(this.ctx.currentTime + 0.5);
}, 1000);
},
stopRedAlert() { if(this.redAlertLoop) clearInterval(this.redAlertLoop); this.redAlertLoop = null; },
stopAll() {
this.stopRedAlert();
if (this.ctx) {
this.ctx.suspend();
}
}
};
// — GAME ENGINE —
const canvas = document.getElementById(‘sensorCanvas’);
const ctx = canvas.getContext(‘2d’);
const canvasContainer = document.getElementById(‘canvas-container’);
const consoleLog = document.getElementById(‘console-log’);
const mainView = document.getElementById(‘main-view’);
const btnQuit = document.getElementById(‘btn-quit’);
const uiEnergy = document.getElementById(‘ui-energy’), uiShields = document.getElementById(‘ui-shields’);
const uiTorps = document.getElementById(‘ui-torps’), uiKlingons = document.getElementById(‘ui-klingons’);
const uiStardate = document.getElementById(‘ui-stardate’);
const indRed = document.getElementById(‘ind-1’), indYel = document.getElementById(‘ind-2’), indGrn = document.getElementById(‘ind-3’);
const startScreen = document.getElementById(‘start-screen’), gameOverScreen = document.getElementById(‘game-over’);
const inputModal = document.getElementById(‘input-modal’), modalButtonsContainer = document.getElementById(‘modal-buttons’);
const G_SIZE = 8, S_SIZE = 8, MAX_ENERGY = 3000, MAX_TORPS = 10;
const ENT = ‘E’, KLN = ‘K’, BAS = ‘B’, STR = ‘*’, EMP = ‘.’, ROM = ‘R’, ANO = ‘A’;
let state = ‘START’, mode = ‘SRS’, modalAction = null;
let isWarping = false, warpStars = [], activeBeams = [];
let ship = { qX: 0, qY: 0, sX: 0, sY: 0, energy: MAX_ENERGY, shields: 0, torps: MAX_TORPS };
let galaxy = [], currentQuadrant = [];
let stardate = 3000.0, endStardate = 3030.0;
let totalKlingons = 0, totalBases = 0, basesDestroyedByPlayer = 0;
let tribbles = 0;
let staticTimer = 0, romulansDecloaked = false;
const crewQuotes = [
“Spock: ‘Fascinating.'”, “Spock: ‘Logic dictates we proceed.'”, “Scotty: ‘I’m givin’ her all she’s got!'”,
“Scotty: ‘The dilithium crystals!'”, “Sulu: ‘Course locked in, sir.'”, “Uhura: ‘Subspace frequencies clear.'”,
“Chekov: ‘Weapons armed.'”, “Bones: ‘I’m a doctor, not an engineer!'”
];
setInterval(() => {
if (state === ‘PLAY’ && Math.random() < 0.30 && !isWarping) triggerRandomChatter();
}, 12000);
function triggerRandomChatter() {
const quote = crewQuotes[Math.floor(Math.random() * crewQuotes.length)];
log(quote, ‘chatter’);
}
function log(msg, type = ”) {
const div = document.createElement(‘div’);
div.className = `log-entry ${type ? ‘log-‘+type : ”}`;
div.innerHTML = type === ‘computer’ ? `<span class=”log-computer”>${msg}</span>` : `> ${msg}`;
consoleLog.prepend(div);
}
function initGame() {
sfx.init(); sfx.click();
log(‘COMPUTER ONLINE.’, ‘computer’);
galaxy = []; totalKlingons = 0; totalBases = 0; activeBeams = []; basesDestroyedByPlayer = 0; tribbles = 0;
for (let y = 0; y < G_SIZE; y++) {
let row = [];
for (let x = 0; x < G_SIZE; x++) {
let k = 0, b = 0, s = Math.floor(Math.random() * 8) + 1;
let rand = Math.random();
if (rand > 0.95) k = 3; else if (rand > 0.85) k = 2; else if (rand > 0.6) k = 1;
if (Math.random() > 0.90) b = 1;
totalKlingons += k; totalBases += b;
row.push({ k: k, b: b, s: s, explored: false, name: getQuadrantName(x, y) });
}
galaxy.push(row);
}
if (totalBases === 0) { galaxy[0][0].b = 1; totalBases = 1; }
if (totalKlingons < 5) { galaxy[1][1].k = 5; totalKlingons += 5; }
ship = {
qX: Math.floor(Math.random()*8), qY: Math.floor(Math.random()*8),
sX: Math.floor(Math.random()*8), sY: Math.floor(Math.random()*8),
energy: MAX_ENERGY, shields: 0, torps: MAX_TORPS
};
stardate = 3000.0; consoleLog.innerHTML = ”;
log(‘SENSORS ONLINE.’, ‘info’);
log(`ORDERS: DESTROY ${totalKlingons} KLINGONS BY SD ${endStardate}.`, ‘info’);
setupQuadrant();
state = ‘PLAY’; setMode(‘SRS’);
startScreen.classList.add(‘hidden’); gameOverScreen.classList.add(‘hidden’);
btnQuit.style.display = ‘flex’;
updateUI(); resizeCanvas(); requestAnimationFrame(renderLoop);
}
function quitGame() {
sfx.click();
state = ‘START’;
isWarping = false;
activeBeams = [];
staticTimer = 0;
sfx.stopAll();
mainView.classList.remove(‘red-alert-pulse’);
setLights(false, false, true);
btnQuit.style.display = ‘none’;
inputModal.classList.add(‘hidden’);
gameOverScreen.classList.add(‘hidden’);
startScreen.classList.remove(‘hidden’);
log(‘SIMULATION ABORTED.’, ‘computer’);
ctx.fillStyle = ‘#050a15’;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function getQuadrantName(x, y) {
const regions = [“Antares”, “Rigel”, “Procyon”, “Vega”, “Canopus”, “Altair”, “Sagittarius”, “Pollux”];
return regions[y] + ” ” + [“I”, “II”, “III”, “IV”][x % 4];
}
function setupQuadrant() {
currentQuadrant = Array(S_SIZE).fill(null).map(() => Array(S_SIZE).fill(EMP));
let q = galaxy[ship.qY][ship.qX];
q.explored = true;
currentQuadrant[ship.sY][ship.sX] = ENT;
function place(type, count) {
let placed = 0, attempts = 0;
while(placed < count && attempts < 50) {
let rx = Math.floor(Math.random()*S_SIZE), ry = Math.floor(Math.random()*S_SIZE);
if (currentQuadrant[ry][rx] === EMP) { currentQuadrant[ry][rx] = type; placed++; }
attempts++;
}
}
place(KLN, q.k); place(BAS, q.b); place(STR, q.s);
if(Math.random() < 0.2) place(ANO, 1);
if(Math.random() < 0.15) place(ROM, 1);
romulansDecloaked = false;
log(`Entering ${q.name}.`, ‘info’);
checkCondition(); checkDocking();
}
function setLights(r, y, g) {
indRed.className = `indicator ind-red ${r ? ‘on’ : ”}`;
indYel.className = `indicator ind-yellow ${y ? ‘on’ : ”}`;
indGrn.className = `indicator ind-green ${g ? ‘on’ : ”}`;
}
function checkCondition() {
if(state !== ‘PLAY’) return;
let q = galaxy[ship.qY][ship.qX];
let hasHostiles = q.k > 0 || currentQuadrant.flat().includes(ROM);
if (hasHostiles) {
if(!indRed.classList.contains(‘on’)) {
log(‘RED ALERT.’, ‘computer’); mainView.classList.add(‘red-alert-pulse’); sfx.startRedAlert();
}
setLights(true, false, false);
} else {
mainView.classList.remove(‘red-alert-pulse’); sfx.stopRedAlert();
if (ship.energy < 1000) setLights(false, true, false);
else setLights(false, false, true);
}
}
function checkDocking() {
let docked = false;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
let ny = ship.sY + dy, nx = ship.sX + dx;
if (ny>=0 && ny<S_SIZE && nx>=0 && nx<S_SIZE && currentQuadrant[ny][nx] === BAS) docked = true;
}
}
if (docked) {
ship.energy = MAX_ENERGY; ship.torps = MAX_TORPS;
log(‘Docked at Starbase. Resupplied.’, ‘success’); log(‘ENERGY REPLENISHED.’, ‘computer’); sfx.click();
if (tribbles === 0 && Math.random() < 0.3) {
tribbles = 2; log(“Captain, some furry creatures came aboard…”, “chatter”);
}
}
}
function triggerAnomaly() {
staticTimer = 60;
sfx.noiseBurst();
let drain = Math.floor(Math.random() * 200) + 100;
ship.energy -= drain;
log(`Ion storm discharge! Lost ${drain} energy!`, ‘alert’);
if (ship.energy <= 0) gameOver(“HULL BREACH”, “Ion storm destroyed the Enterprise.”);
updateUI();
}
function updateUI() {
uiEnergy.innerText = Math.floor(ship.energy);
uiShields.innerText = Math.floor(ship.shields);
uiTorps.innerText = ship.torps;
uiKlingons.innerText = totalKlingons;
uiStardate.innerText = stardate.toFixed(1);
uiEnergy.className = ship.energy < 500 ? ‘stat-val warning’ : ‘stat-val’;
uiShields.className = ship.shields < 100 && (galaxy[ship.qY][ship.qX].k > 0 || currentQuadrant.flat().includes(ROM)) ? ‘stat-val warning’ : ‘stat-val’;
}
function setMode(newMode) {
if(state !== ‘PLAY’ || isWarping) return;
sfx.click(); mode = newMode;
[‘srs’, ‘lrs’, ‘imp’, ‘wrp’, ‘pha’, ‘tor’].forEach(id => { document.getElementById(‘btn-‘+id).classList.remove(‘btn-active’); });
document.getElementById(‘btn-‘+mode.toLowerCase())?.classList.add(‘btn-active’);
draw();
}
function resizeCanvas() { canvas.width = canvasContainer.clientWidth; canvas.height = canvasContainer.clientHeight; draw(); }
window.addEventListener(‘resize’, resizeCanvas);
// — Drawing Functions —
function drawEnterprise(ctx, x, y, size) {
ctx.save(); ctx.translate(x, y); ctx.fillStyle = ‘#e0e5eb’;
ctx.beginPath(); ctx.arc(0, -size*0.15, size*0.3, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = ‘#fff’; ctx.beginPath(); ctx.arc(0, -size*0.15, size*0.08, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = ‘#d0d5db’; ctx.fillRect(-size*0.08, -size*0.1, size*0.16, size*0.4);
ctx.fillStyle = ‘#a0a5ab’; ctx.beginPath(); ctx.moveTo(-size*0.25, size*0.15); ctx.lineTo(size*0.25, size*0.15); ctx.lineTo(0, -size*0.05); ctx.fill();
ctx.fillStyle = ‘#d0d5db’; ctx.fillRect(-size*0.32, 0, size*0.12, size*0.45); ctx.fillRect(size*0.20, 0, size*0.12, size*0.45);
ctx.fillStyle = ‘#ff3333’; ctx.beginPath(); ctx.arc(-size*0.26, 0, size*0.06, Math.PI, 0); ctx.fill(); ctx.beginPath(); ctx.arc(size*0.26, 0, size*0.06, Math.PI, 0); ctx.fill();
ctx.restore();
}
function drawKlingon(ctx, x, y, size) {
ctx.save(); ctx.translate(x, y);
let angle = Math.atan2(ship.sY * (canvas.height/8) + (canvas.height/16) – y, ship.sX * (canvas.width/8) + (canvas.width/16) – x);
ctx.rotate(angle + Math.PI/2);
ctx.fillStyle = ‘#7cbd5a’;
ctx.fillRect(-size*0.12, 0, size*0.24, size*0.25); ctx.fillRect(-size*0.04, -size*0.3, size*0.08, size*0.3);
ctx.beginPath(); ctx.moveTo(0, -size*0.45); ctx.lineTo(size*0.12, -size*0.3); ctx.lineTo(0, -size*0.2); ctx.lineTo(-size*0.12, -size*0.3); ctx.fill();
ctx.beginPath(); ctx.moveTo(-size*0.12, size*0.1); ctx.lineTo(-size*0.45, size*0.25); ctx.lineTo(-size*0.45, -size*0.05); ctx.closePath(); ctx.fill();
ctx.beginPath(); ctx.moveTo(size*0.12, size*0.1); ctx.lineTo(size*0.45, size*0.25); ctx.lineTo(size*0.45, -size*0.05); ctx.closePath(); ctx.fill();
ctx.fillStyle = ‘#ff3333’; ctx.fillRect(-size*0.48, -size*0.05, size*0.06, size*0.15); ctx.fillRect(size*0.42, -size*0.05, size*0.06, size*0.15);
ctx.restore();
}
function drawRomulan(ctx, x, y, size) {
ctx.save(); ctx.translate(x, y);
if(!romulansDecloaked) ctx.globalAlpha = 0.15;
let angle = Math.atan2(ship.sY * (canvas.height/8) + (canvas.height/16) – y, ship.sX * (canvas.width/8) + (canvas.width/16) – x);
ctx.rotate(angle + Math.PI/2);
ctx.fillStyle = ‘#99aa99’;
ctx.beginPath(); ctx.ellipse(0, 0, size*0.4, size*0.25, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = ‘#cc5555’;
ctx.beginPath(); ctx.moveTo(0, -size*0.25); ctx.lineTo(size*0.2, size*0.1); ctx.lineTo(-size*0.2, size*0.1); ctx.fill();
ctx.fillStyle = ‘#778877’; ctx.fillRect(-size*0.05, size*0.2, size*0.1, size*0.2);
ctx.restore();
}
function drawAnomaly(ctx, x, y, size) {
ctx.save(); ctx.translate(x, y);
let pulse = Math.sin(Date.now() / 200) * 0.2 + 0.8;
ctx.shadowBlur = 20 * pulse; ctx.shadowColor = ‘#aa55ff’; ctx.fillStyle = `rgba(170, 85, 255, ${0.6 * pulse})`;
ctx.beginPath(); ctx.arc(0, 0, size*0.3 * pulse, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, 0, size*0.15, 0, Math.PI*2); ctx.fillStyle = ‘rgba(255,255,255,0.8)’; ctx.fill();
ctx.restore();
}
function drawStarbase(ctx, x, y, size) {
ctx.save(); ctx.translate(x, y); ctx.fillStyle = ‘#cccccc’;
ctx.beginPath(); ctx.arc(0, 0, size*0.15, 0, Math.PI*2); ctx.fill();
ctx.fillRect(-size*0.35, -size*0.03, size*0.7, size*0.06); ctx.fillRect(-size*0.03, -size*0.35, size*0.06, size*0.7);
ctx.fillStyle = ‘#4488ff’;
ctx.beginPath(); ctx.arc(-size*0.35, 0, size*0.08, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(size*0.35, 0, size*0.08, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, -size*0.35, size*0.08, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, size*0.35, size*0.08, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawStar(ctx, x, y, size) {
ctx.save(); ctx.translate(x, y); ctx.shadowBlur = 10; ctx.shadowColor = ‘#ffffaa’; ctx.fillStyle = ‘#ffffaa’;
ctx.beginPath(); ctx.arc(0, 0, size*0.12, 0, Math.PI*2); ctx.fill(); ctx.restore();
}
function drawWarpEffect() {
if(state !== ‘PLAY’) return;
ctx.fillStyle = ‘rgba(5, 10, 21, 0.4)’; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = ‘#ffffff’; let cx = canvas.width/2, cy = canvas.height/2;
warpStars.forEach(s => {
s.dist *= 1.15; let x = cx + Math.cos(s.angle) * s.dist, y = cy + Math.sin(s.angle) * s.dist, len = s.dist * 0.15;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(s.angle) * len, y + Math.sin(s.angle) * len);
ctx.strokeStyle = ‘#ffffff’; ctx.lineWidth = 2; ctx.stroke();
if (s.dist > Math.max(canvas.width, canvas.height)) { s.dist = Math.random() * 50; s.angle = Math.random() * Math.PI * 2; }
});
if (isWarping) requestAnimationFrame(drawWarpEffect);
}
function drawStatic() {
let cw = canvas.width, ch = canvas.height, imgData = ctx.createImageData(cw, ch);
for(let i=0; i<imgData.data.length; i+=4) {
let v = Math.random() * 255;
imgData.data[i] = v; imgData.data[i+1] = v; imgData.data[i+2] = v; imgData.data[i+3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}
function draw() {
if (state !== ‘PLAY’ || isWarping) return;
ctx.fillStyle = ‘#050a15’; ctx.fillRect(0, 0, canvas.width, canvas.height);
if (mode === ‘LRS’ || mode === ‘WRP’) drawLRS(); else drawSRS();
if (staticTimer > 0) drawStatic();
}
function drawSRS() {
let cw = canvas.width, ch = canvas.height, cellW = cw / S_SIZE, cellH = ch / S_SIZE, objSize = Math.min(cellW, cellH) * 0.8;
if (mode === ‘IMP’ || mode === ‘TOR’) {
ctx.fillStyle = ‘rgba(255, 255, 255, 0.1)’;
for (let y = 0; y < S_SIZE; y++) { for (let x = 0; x < S_SIZE; x++) { ctx.fillRect(x*cellW, y*cellH, cellW, cellH); } }
}
ctx.fillStyle = ‘rgba(0,0,0,0.2)’; for(let i=0; i<ch; i+=4) ctx.fillRect(0, i, cw, 1);
ctx.strokeStyle = ‘#2a3b4c’; ctx.lineWidth = 2;
for (let i = 0; i <= S_SIZE; i++) {
ctx.beginPath(); ctx.moveTo(i * cellW, 0); ctx.lineTo(i * cellW, ch); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i * cellH); ctx.lineTo(cw, i * cellH); ctx.stroke();
}
for (let y = 0; y < S_SIZE; y++) {
for (let x = 0; x < S_SIZE; x++) {
let item = currentQuadrant[y][x];
let px = x * cellW + cellW/2, py = y * cellH + cellH/2;
if (item === ENT) {
drawEnterprise(ctx, px, py, objSize);
if (ship.shields > 0) {
ctx.strokeStyle = `rgba(100, 200, 255, ${Math.min(1, ship.shields/1000)})`;
ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(px, py, objSize*0.5, 0, Math.PI*2); ctx.stroke();
}
}
else if (item === KLN) drawKlingon(ctx, px, py, objSize);
else if (item === ROM) drawRomulan(ctx, px, py, objSize);
else if (item === ANO) drawAnomaly(ctx, px, py, objSize);
else if (item === BAS) drawStarbase(ctx, px, py, objSize);
else if (item === STR) drawStar(ctx, px, py, objSize);
}
}
activeBeams.forEach(b => {
ctx.strokeStyle = b.color; ctx.lineWidth = b.isTorp ? 6 : 4;
if (b.isTorp) ctx.setLineDash([10, 15]); else ctx.setLineDash([]);
ctx.beginPath(); ctx.moveTo(b.sx * cellW + cellW/2, b.sy * cellH + cellH/2); ctx.lineTo(b.tx * cellW + cellW/2, b.ty * cellH + cellH/2); ctx.stroke();
ctx.setLineDash([]);
});
}
function drawLRS() {
let cw = canvas.width, ch = canvas.height, cellW = cw / G_SIZE, cellH = ch / G_SIZE;
ctx.strokeStyle = ‘#2a4c2a’; ctx.lineWidth = 2;
for (let i = 0; i <= G_SIZE; i++) {
ctx.beginPath(); ctx.moveTo(i * cellW, 0); ctx.lineTo(i * cellW, ch); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i * cellH); ctx.lineTo(cw, i * cellH); ctx.stroke();
}
ctx.textAlign = ‘center’; ctx.textBaseline = ‘middle’;
let fontSize = Math.min(cellW, cellH) * 0.4; ctx.font = `${fontSize}px var(–font-screen)`;
for (let y = 0; y < G_SIZE; y++) {
for (let x = 0; x < G_SIZE; x++) {
let px = x * cellW + cellW/2, py = y * cellH + cellH/2, q = galaxy[y][x];
if (x === ship.qX && y === ship.qY) {
ctx.fillStyle = ‘rgba(100, 200, 255, 0.2)’; ctx.fillRect(x*cellW, y*cellH, cellW, cellH);
ctx.strokeStyle = ‘#64c8ff’; ctx.strokeRect(x*cellW, y*cellH, cellW, cellH);
}
if (q.explored || (Math.abs(x – ship.qX) <= 1 && Math.abs(y – ship.qY) <= 1)) {
q.explored = true;
ctx.fillStyle = q.k > 0 ? ‘#ff5555’ : (q.b > 0 ? ‘#55ff55’ : ‘#aaaaaa’);
ctx.fillText(`${q.k}${q.b}${q.s}`, px, py);
} else { ctx.fillStyle = ‘#444’; ctx.fillText(‘???’, px, py); }
if (mode === ‘WRP’) { ctx.fillStyle = ‘rgba(200, 100, 255, 0.2)’; ctx.fillRect(x*cellW, y*cellH, cellW, cellH); }
}
}
}
function renderLoop() {
if (state === ‘PLAY’ && staticTimer > 0) staticTimer–;
draw();
requestAnimationFrame(renderLoop);
}
canvas.addEventListener(‘pointerdown’, (e) => {
if (state !== ‘PLAY’ || isWarping || activeBeams.length > 0 || staticTimer > 0) return;
const rect = canvas.getBoundingClientRect();
const cx = e.clientX – rect.left, cy = e.clientY – rect.top;
if (mode === ‘IMP’ || mode === ‘TOR’ || mode === ‘SRS’) {
let sX = Math.floor(cx / (canvas.width / S_SIZE)), sY = Math.floor(cy / (canvas.height / S_SIZE));
if (mode === ‘IMP’ || (mode === ‘SRS’ && currentQuadrant[sY][sX] === EMP || currentQuadrant[sY][sX] === ANO)) doImpulse(sX, sY);
else if (mode === ‘TOR’ || (mode === ‘SRS’ && (currentQuadrant[sY][sX] === KLN || currentQuadrant[sY][sX] === ROM))) doTorpedo(sX, sY);
} else if (mode === ‘WRP’ || mode === ‘LRS’) {
let qX = Math.floor(cx / (canvas.width / G_SIZE)), qY = Math.floor(cy / (canvas.height / G_SIZE));
if (mode === ‘WRP’ || mode === ‘LRS’) doWarp(qX, qY);
}
});
function doImpulse(tx, ty) {
if (tx === ship.sX && ty === ship.sY) return;
if (currentQuadrant[ty][tx] !== EMP && currentQuadrant[ty][tx] !== ANO) { sfx.error(); return log(“Error: Sector occupied.”, “alert”); }
let dist = Math.sqrt((tx-ship.sX)**2 + (ty-ship.sY)**2);
let cost = Math.floor(dist * 10);
if (ship.energy < cost) { sfx.error(); return log(“Not enough energy.”, “alert”); }
ship.energy -= cost;
let isAnomaly = currentQuadrant[ty][tx] === ANO;
currentQuadrant[ship.sY][ship.sX] = EMP;
ship.sX = tx; ship.sY = ty;
currentQuadrant[ship.sY][ship.sX] = ENT;
stardate += 0.1;
log(`Impulsed. Cost: ${cost}E`);
if (isAnomaly) triggerAnomaly();
if(Math.random() < 0.4) triggerRandomChatter();
checkDocking();
postAction();
setMode(‘SRS’);
}
function doWarp(tx, ty) {
if (tx === ship.qX && ty === ship.qY) { log(“Already there.”, “info”); return setMode(‘SRS’); }
let dist = Math.sqrt((tx-ship.qX)**2 + (ty-ship.qY)**2);
let cost = Math.floor(dist * 100);
if (ship.energy < cost) { sfx.error(); return log(`Need ${cost}E for warp.`, “alert”); }
ship.energy -= cost;
isWarping = true; sfx.warp();
log(`Warping to quadrant ${tx},${ty}…`, ‘info’);
warpStars = [];
for(let i=0; i<50; i++) warpStars.push({angle: Math.random()*Math.PI*2, dist: Math.random()*100 + 10});
requestAnimationFrame(drawWarpEffect);
setTimeout(() => {
if (state !== ‘PLAY’) return;
isWarping = false;
ship.qX = tx; ship.qY = ty;
ship.sX = Math.floor(Math.random()*S_SIZE); ship.sY = Math.floor(Math.random()*S_SIZE);
stardate += Math.max(0.2, dist * 0.1);
if(Math.random() < 0.5) triggerRandomChatter();
setupQuadrant();
setMode(‘SRS’);
postAction();
}, 1200);
}
function doTorpedo(tx, ty) {
if (ship.torps <= 0) { sfx.error(); return log(“No torpedoes!”, “alert”); }
ship.torps–;
log(`Torpedo away…`); sfx.torpedo();
let dx = tx – ship.sX, dy = ty – ship.sY, steps = Math.max(Math.abs(dx), Math.abs(dy));
let xInc = dx / steps, yInc = dy / steps, cx = ship.sX + xInc, cy = ship.sY + yInc;
let hit = false;
activeBeams.push({sx: ship.sX, sy: ship.sY, tx: tx, ty: ty, color: ‘#ffaa00’, isTorp: true});
setTimeout(() => {
if (state !== ‘PLAY’) return;
activeBeams = [];
for (let i = 0; i < steps; i++) {
let ix = Math.round(cx), iy = Math.round(cy);
if (ix < 0 || ix >= S_SIZE || iy < 0 || iy >= S_SIZE) break;
let target = currentQuadrant[iy][ix];
if (target === KLN || target === ROM) {
log(“Direct hit!”, “success”);
destroyKlingon(ix, iy, target); hit = true; break;
} else if (target === STR) {
log(“Hit a star. Absorbed.”, “alert”); sfx.explosion(); hit = true; break;
} else if (target === ANO) {
log(“Torpedo detonated in Anomaly.”, “info”); sfx.explosion(); hit = true; break;
} else if (target === BAS) {
log(“Starbase hit!”, “alert”); log(‘COMPUTER: FRIENDLY FIRE. BASE DESTROYED.’, ‘computer’);
sfx.explosion(); currentQuadrant[iy][ix] = EMP; galaxy[ship.qY][ship.qX].b–; hit = true;
basesDestroyedByPlayer++;
if (basesDestroyedByPlayer >= 2) {
setTimeout(() => { if(state===’PLAY’) gameOver(“COURT MARTIAL”, “You destroyed multiple Starbases.<br><br>You face trial for treason.”); }, 800);
} else {
log(“Starfleet issues formal warning.”, “alert”);
}
break;
}
cx += xInc; cy += yInc;
}
if (!hit) log(“Torpedo missed.”);
stardate += 0.1; postAction(); setMode(‘SRS’);
}, 600);
}
function openModal(action) {
if (state !== ‘PLAY’ || isWarping || activeBeams.length > 0 || staticTimer > 0) return;
sfx.click(); modalAction = action; modalButtonsContainer.innerHTML = ”;
if (action === ‘PHASERS’) {
document.getElementById(‘modal-title’).innerText = ‘PHASER POWER’;
modalButtonsContainer.innerHTML = `
<button class=”tos-btn btn-blue modal-btn” onclick=”modalVal(100)”>100</button>
<button class=”tos-btn btn-blue modal-btn” onclick=”modalVal(250)”>250</button>
<button class=”tos-btn btn-blue modal-btn” onclick=”modalVal(500)”>500</button>
<button class=”tos-btn btn-red modal-btn text-white” onclick=”modalVal(‘MAX’)”>MAX</button>
`;
} else if (action === ‘SHIELDS’) {
document.getElementById(‘modal-title’).innerText = ‘SHIELD POWER’;
modalButtonsContainer.innerHTML = `
<div class=”modal-label”>RAISE SHIELDS (USE ENERGY)</div>
<button class=”tos-btn btn-blue modal-btn” onclick=”modalVal(100)”>+100</button>
<button class=”tos-btn btn-blue modal-btn” onclick=”modalVal(250)”>+250</button>
<button class=”tos-btn btn-blue modal-btn” onclick=”modalVal(500)”>+500</button>
<button class=”tos-btn btn-red modal-btn text-white” onclick=”modalVal(‘MAX’)”>MAX</button>
<div class=”modal-label” style=”margin-top:0.8rem;”>LOWER SHIELDS (GAIN ENERGY)</div>
<button class=”tos-btn btn-purple modal-btn” onclick=”modalVal(-100)”>-100</button>
<button class=”tos-btn btn-purple modal-btn” onclick=”modalVal(-250)”>-250</button>
<button class=”tos-btn btn-purple modal-btn” onclick=”modalVal(-500)”>-500</button>
<button class=”tos-btn btn-orange modal-btn text-black” onclick=”modalVal(‘DRAIN’)”>DRAIN</button>
`;
}
inputModal.classList.remove(‘hidden’);
}
window.closeModal = function() { sfx.click(); inputModal.classList.add(‘hidden’); modalAction = null; }
window.modalVal = function(val) {
if (modalAction === ‘PHASERS’) {
if (val === ‘MAX’) val = ship.energy;
if (val > ship.energy) { sfx.error(); log(“Low energy.”, “alert”); return closeModal(); }
firePhasers(val);
} else if (modalAction === ‘SHIELDS’) {
if (val === ‘MAX’) val = Math.min(ship.energy, 2000 – ship.shields);
if (val === ‘DRAIN’) val = -ship.shields;
if (val > 0 && val > ship.energy) { sfx.error(); log(“Low energy.”, “alert”); return closeModal(); }
if (val < 0 && Math.abs(val) > ship.shields) val = -ship.shields;
setShields(val);
}
closeModal();
}
function setShields(amount) {
ship.energy -= amount; ship.shields += amount;
if (amount > 0) { log(`Shields raised by ${amount}.`); sfx.click(); }
else if (amount < 0) { log(`Shields lowered by ${Math.abs(amount)}.`); sfx.click(); }
updateUI(); draw();
}
function firePhasers(energySpent) {
let q = galaxy[ship.qY][ship.qX];
let roms = currentQuadrant.flat().filter(e => e === ROM).length;
if (q.k === 0 && roms === 0) { sfx.error(); return log(“No targets.”); }
sfx.phaser(); ship.energy -= energySpent;
let energyPerEnemy = energySpent / (q.k + roms);
log(`Phasers fired: ${energySpent}E`);
activeBeams = [];
for (let y = 0; y < S_SIZE; y++) {
for (let x = 0; x < S_SIZE; x++) {
let target = currentQuadrant[y][x];
if (target === KLN || target === ROM) {
activeBeams.push({sx: ship.sX, sy: ship.sY, tx: x, ty: y, color: ‘#64c8ff’});
let dist = Math.sqrt((x-ship.sX)**2 + (y-ship.sY)**2);
let dmgReq = target === ROM ? 200 : 150;
let distanceFalloff = Math.max(1, dist * 0.4);
let damage = (energyPerEnemy / distanceFalloff) * (Math.random() * 0.4 + 0.8);
if (damage >= dmgReq) {
log(`Target destroyed!`, “success”);
setTimeout(() => { if (state === ‘PLAY’) destroyKlingon(x, y, target); }, 300);
} else {
log(`Target hit (${Math.floor(damage)} dmg). Failed to penetrate.`, “alert”);
}
}
}
}
stardate += 0.1;
setTimeout(() => { if(state !== ‘PLAY’) return; activeBeams = []; postAction(); setMode(‘SRS’); }, 400);
}
function destroyKlingon(x, y, type) {
if(state !== ‘PLAY’) return;
sfx.explosion(); currentQuadrant[y][x] = EMP;
if (type === KLN) {
galaxy[ship.qY][ship.qX].k–; totalKlingons–;
if (totalKlingons <= 0) setTimeout(() => { if (state===’PLAY’) checkWin(); }, 500);
}
checkCondition();
}
function moveHostiles() {
let coords = [];
for (let y = 0; y < S_SIZE; y++) {
for (let x = 0; x < S_SIZE; x++) {
if (currentQuadrant[y][x] === KLN || currentQuadrant[y][x] === ROM) coords.push({x, y, t: currentQuadrant[y][x]});
}
}
coords.forEach(k => {
let dx = Math.sign(ship.sX – k.x), dy = Math.sign(ship.sY – k.y);
if (dx !== 0 && dy !== 0 && currentQuadrant[k.y + dy][k.x + dx] === EMP) {
currentQuadrant[k.y][k.x] = EMP; currentQuadrant[k.y + dy][k.x + dx] = k.t;
} else if (dx !== 0 && currentQuadrant[k.y][k.x + dx] === EMP) {
currentQuadrant[k.y][k.x] = EMP; currentQuadrant[k.y][k.x + dx] = k.t;
} else if (dy !== 0 && currentQuadrant[k.y + dy][k.x] === EMP) {
currentQuadrant[k.y][k.x] = EMP; currentQuadrant[k.y + dy][k.x] = k.t;
}
});
}
function postAction() {
if (state !== ‘PLAY’ || isWarping) return;
if (tribbles > 0) {
tribbles = Math.floor(tribbles * 1.5);
if (Math.random() < 0.3) { sfx.purr(); log(`Tribbles: ${tribbles}`, ‘chatter’); }
if (tribbles > 10000) {
let drain = Math.floor(tribbles / 10000);
ship.energy -= drain;
log(`Tribbles consuming ${drain} energy!`, ‘alert’);
}
}
let nearAnomaly = false;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
let ny = ship.sY + dy, nx = ship.sX + dx;
if (ny>=0 && ny<S_SIZE && nx>=0 && nx<S_SIZE && currentQuadrant[ny][nx] === ANO) nearAnomaly = true;
}
}
if(nearAnomaly && Math.random() < 0.5) triggerAnomaly();
let q = galaxy[ship.qY][ship.qX];
let hasHostiles = q.k > 0 || currentQuadrant.flat().includes(ROM);
if (hasHostiles) {
moveHostiles();
activeBeams = [];
let incomingFire = false;
let decloakSoundPlayed = false;
for (let y = 0; y < S_SIZE; y++) {
for (let x = 0; x < S_SIZE; x++) {
let target = currentQuadrant[y][x];
if (target === KLN || target === ROM) {
if(target === ROM && !romulansDecloaked) {
romulansDecloaked = true;
if(!decloakSoundPlayed) { sfx.decloak(); decloakSoundPlayed=true; }
log(“Romulan vessel decloaking!”, “alert”);
}
incomingFire = true;
let color = target === ROM ? ‘#aa00ff’ : ‘#ff3333’;
activeBeams.push({sx: x, sy: y, tx: ship.sX, ty: ship.sY, color: color});
let dist = Math.sqrt((x-ship.sX)**2 + (y-ship.sY)**2);
let baseDmg = target === ROM ? 400 : 300;
let damage = Math.floor((baseDmg / Math.max(1, dist * 0.4)) * (Math.random() * 0.5 + 0.5));
log(`Incoming fire: ${damage} dmg!`, ‘alert’);
if (ship.shields >= damage) {
ship.shields -= damage; log(“Shields held.”);
setTimeout(() => { if (state === ‘PLAY’) sfx.click(); }, 100);
} else {
let remainder = damage – ship.shields;
ship.shields = 0; ship.energy -= remainder;
log(`Shields dropped! Hull hit: ${remainder}E`, ‘alert’);
setTimeout(() => { if (state === ‘PLAY’) sfx.explosion(); }, 100);
}
}
}
}
if (incomingFire) {
setTimeout(() => { if (state === ‘PLAY’) activeBeams = []; }, 400);
}
}
updateUI();
if (ship.energy <= 0) gameOver(“HULL BREACH”, “Hull integrity failed. The Enterprise is destroyed.<br><br>The Klingon Empire advances unopposed.”);
else if (stardate > endStardate) gameOver(“TIME EXPIRED”, “Stardate limit exceeded.<br><br>The Federation has surrendered to the Klingon armada.”);
}
function checkWin() {
state = ‘GAMEOVER’; sfx.stopAll(); mainView.classList.remove(‘red-alert-pulse’);
btnQuit.style.display = ‘none’;
let timeRemaining = endStardate – stardate;
let score = Math.floor(ship.energy) + Math.floor(timeRemaining * 100) – (basesDestroyedByPlayer * 2000) – (tribbles > 10000 ? 500 : 0);
let rank = “Ensign”;
if (score >= 4000) rank = “Fleet Admiral”;
else if (score >= 3000) rank = “Captain”;
else if (score >= 2000) rank = “Commander”;
else if (score >= 1000) rank = “Lieutenant”;
if (basesDestroyedByPlayer > 0 && score >= 3000) rank = “Commander (Demoted)”;
document.getElementById(‘go-title’).innerText = “VICTORY”;
document.getElementById(‘go-title’).style.color = “var(–tos-green)”;
document.getElementById(‘go-reason’).innerHTML = `
The Klingon invasion fleet has been stopped.<br><br>
<span style=”color:var(–tos-gold)”>Final Score:</span> ${score}<br>
<span style=”color:var(–tos-light-blue)”>Starfleet Rank:</span> ${rank}
`;
gameOverScreen.classList.remove(‘hidden’);
}
function gameOver(title, reason) {
state = ‘GAMEOVER’; sfx.stopAll(); mainView.classList.remove(‘red-alert-pulse’);
btnQuit.style.display = ‘none’;
document.getElementById(‘go-title’).innerText = title;
document.getElementById(‘go-title’).style.color = “var(–tos-red)”;
document.getElementById(‘go-reason’).innerHTML = reason;
gameOverScreen.classList.remove(‘hidden’);
}
document.getElementById(‘btn-srs’).onclick = () => setMode(‘SRS’);
document.getElementById(‘btn-lrs’).onclick = () => setMode(‘LRS’);
document.getElementById(‘btn-imp’).onclick = () => setMode(‘IMP’);
document.getElementById(‘btn-wrp’).onclick = () => setMode(‘WRP’);
document.getElementById(‘btn-tor’).onclick = () => setMode(‘TOR’);
document.getElementById(‘btn-pha’).onclick = () => openModal(‘PHASERS’);
document.getElementById(‘btn-shd’).onclick = () => openModal(‘SHIELDS’);
document.getElementById(‘btn-start’).onclick = initGame;
document.getElementById(‘btn-restart’).onclick = initGame;
document.getElementById(‘btn-quit’).onclick = quitGame;
if (‘serviceWorker’ in navigator) {
window.addEventListener(‘load’, () => {
const swCode = `self.addEventListener(‘install’, e => self.skipWaiting()); self.addEventListener(‘activate’, e => e.waitUntil(clients.claim())); self.addEventListener(‘fetch’, e => { if (e.request.method === ‘GET’) e.respondWith(fetch(e.request).catch(() => caches.match(e.request))); });`;
navigator.serviceWorker.register(URL.createObjectURL(new Blob([swCode], { type: ‘application/javascript’ }))).catch(err => console.log(‘SW Registration Failed’));
});
}
</script>
</body>
</html>
