Files
basic-computer-games/84_Super_Star_Trek/javascript/superstartrek.mjs
Chris Reuter d26dbf036a Removed spaces from top-level directory names.
Spaces tend to cause annoyances in a Unix-style shell environment.
This change fixes that.
2021-11-21 18:30:21 -05:00

1680 lines
48 KiB
JavaScript

/**
* SUPER STARTREK - MAY 16,1978 - REQUIRES 24K MEMORY
*
* **** STAR TREK **** ****
* SIMULATION OF A MISSION OF THE STARSHIP ENTERPRISE,
* AS SEEN ON THE STAR TREK TV SHOW.
* ORIGIONAL PROGRAM BY MIKE MAYFIELD, MODIFIED VERSION
* PUBLISHED IN DEC'S "101 BASIC GAMES", BY DAVE AHL.
* MODIFICATIONS TO THE LATTER (PLUS DEBUGGING) BY BOB
* LEEDOM - APRIL & DECEMBER 1974,
* WITH A LITTLE HELP FROM HIS FRIENDS . . .
* COMMENTS, EPITHETS, AND SUGGESTIONS SOLICITED --
* SEND TO: R. C. LEEDOM
* WESTINGHOUSE DEFENSE & ELECTRONICS SYSTEMS CNTR.
* BOX 746, M.S. 338
* BALTIMORE, MD 21203
*
* CONVERTED TO MICROSOFT 8 K BASIC 3/16/78 BY JOHN GORDERS
* LINE NUMBERS FROM VERSION STREK7 OF 1/12/75 PRESERVED AS
* MUCH AS POSSIBLE WHILE USING MULTIPLE STATEMENTS PER LINE
* SOME LINES ARE LONGER THAN 72 CHARACTERS; THIS WAS DONE
* BY USING "?" INSTEAD OF "PRINT" WHEN ENTERING LINES
*
* Translated and reworked into JavaScript in February 2021
* by Les Orchard <me@lmorchard.com>
*/
export const setGameOptions = (options = {}) =>
Object.assign(gameOptions, options);
export const getGameState = () => ({ ...gameState });
export const onPrint = (fn) => (print = fn);
export const onInput = (fn) => (input = fn);
export const onExit = (fn) => (exit = fn);
export async function gameMain() {
await gameReset();
await gameLoop();
await exit();
}
let gameState = {};
let print = () => {};
let input = () => {};
let exit = () => {};
export const gameOptions = {
stardateStart: Math.floor(Math.random() * 20 + 20) * 100,
timeLimit: 25 + Math.floor(Math.random() * 10),
energyMax: 3000,
photonTorpedoesMax: 10,
starbaseSpawnChance: 0.96,
enemyMaxShield: 200,
enemySpawnChance: [0.8, 0.85, 0.98],
maxStarsPerSector: 8,
sectorWidth: 8,
sectorHeight: 8,
galaxyWidth: 8,
galaxyHeight: 8,
systemDamageChanceOnHit: 0.6,
systemDamageHitThroughShields: 0.02,
systemChanceAffectedInWarp: 0.2,
systemChanceDamageInWarp: 0.6,
nameEnemy: "KLINGON",
nameEnemies: "KLINGONS",
nameScienceOfficer: "SPOCK",
nameNavigationOfficer: "LT. SULU",
nameWeaponsOfficer: "ENSIGN CHEKOV",
nameCommunicationsOfficer: "LT. UHURA",
nameChiefEngineer: "SCOTT",
sectorMapSymbols: {
empty: " ",
star: " * ",
base: ">!<",
hero: "<*>",
enemy: "+K+",
},
shipSystems: [
"WARP ENGINES",
"SHORT RANGE SENSORS",
"LONG RANGE SENSORS",
"PHASER CONTROL",
"PHOTON TUBES",
"DAMAGE CONTROL",
"SHIELD CONTROL",
"LIBRARY-COMPUTER",
],
quadrantNames: [
[
"ANTARES",
"RIGEL",
"PROCYON",
"VEGA",
"CANOPUS",
"ALTAIR",
"SAGITTARIUS",
"POLLUX",
],
[
"SIRIUS",
"DENEB",
"CAPELLA",
"BETELGEUSE",
"ALDEBARAN",
"REGULUS",
"ARCTURUS",
"SPICA",
],
],
quadrantNumbers: ["I", "II", "III", "IV"],
};
let SYSTEM_WARP_ENGINES,
SYSTEM_SHORT_RANGE_SENSORS,
SYSTEM_LONG_RANGE_SENSORS,
SYSTEM_PHASER_CONTROL,
SYSTEM_PHOTON_TUBES,
SYSTEM_DAMAGE_CONTROL,
SYSTEM_SHIELD_CONTROL,
SYSTEM_LIBRARY_COMPUTER;
async function gameIntro() {
print("\n".repeat(10));
print(" ,------*------,");
print(" ,------------- '--- ------'");
print(" '-------- --' / /");
print(" ,---' '-------/ /--,");
print(" '----------------'");
print("");
print(" THE USS ENTERPRISE --- NCC-1701");
print("\n".repeat(4));
print("YOUR ORDERS ARE AS FOLLOWS:");
print();
print(
` DESTROY THE ${gameState.enemiesRemaining} ${gameOptions.nameEnemy} WARSHIPS WHICH HAVE INVADED`
);
print(" THE GALAXY BEFORE THEY CAN ATTACK FEDERATION HEADQUARTERS");
print(
` ON STARDATE ${formatStardate(
gameOptions.stardateStart + gameOptions.timeLimit
)} THIS GIVES YOU ${gameOptions.timeLimit} DAYS. THERE${
gameState.starbasesRemaining > 1 ? " ARE " : " IS "
}`
);
print(
` ${gameState.starbasesRemaining} STARBASE${
gameState.starbasesRemaining > 1 ? "S" : " ARE"
} IN THE GALAXY FOR RESUPPLYING YOUR SHIP`
);
}
async function gameReset() {
[
SYSTEM_WARP_ENGINES,
SYSTEM_SHORT_RANGE_SENSORS,
SYSTEM_LONG_RANGE_SENSORS,
SYSTEM_PHASER_CONTROL,
SYSTEM_PHOTON_TUBES,
SYSTEM_DAMAGE_CONTROL,
SYSTEM_SHIELD_CONTROL,
SYSTEM_LIBRARY_COMPUTER,
] = gameOptions.shipSystems;
gameState = {
gameOver: false,
gameWon: false,
gameQuit: false,
destroyed: false,
shouldRestart: false,
sectorMap: "",
alertCondition: "",
stardateCurrent: gameOptions.stardateStart,
isDocked: false,
energyRemaining: gameOptions.energyMax,
photonTorpedoesRemaining: gameOptions.photonTorpedoesMax,
shieldsCurrent: 0,
starbasesRemaining: 0,
enemiesRemaining: 0,
quadrantPositionY: randomInt(gameOptions.galaxyHeight, 1),
quadrantPositionX: randomInt(gameOptions.galaxyWidth, 1),
sectorPositionY: randomInt(gameOptions.sectorHeight, 1),
sectorPositionX: randomInt(gameOptions.sectorWidth, 1),
sectorEnemiesCount: 0,
sectorStarbasesCount: 0,
sectorStarsCount: 0,
galacticMap: [],
galacticMapDiscovered: [],
};
gameState.systemsDamage = {};
for (const systemName of gameOptions.shipSystems) {
gameState.systemsDamage[systemName] = 0;
}
for (let mapY = 1; mapY <= gameOptions.galaxyHeight; mapY++) {
gameState.galacticMap[mapY] = [];
gameState.galacticMapDiscovered[mapY] = [];
for (let mapX = 1; mapX <= gameOptions.galaxyWidth; mapX++) {
gameState.galacticMapDiscovered[mapY][mapX] = 0;
gameState.sectorEnemiesCount = 0;
const enemySpawnRoll = Math.random();
if (enemySpawnRoll > gameOptions.enemySpawnChance[2]) {
gameState.sectorEnemiesCount = 3;
gameState.enemiesRemaining = gameState.enemiesRemaining + 3;
} else if (enemySpawnRoll > gameOptions.enemySpawnChance[1]) {
gameState.sectorEnemiesCount = 2;
gameState.enemiesRemaining = gameState.enemiesRemaining + 2;
} else if (enemySpawnRoll > gameOptions.enemySpawnChance[0]) {
gameState.sectorEnemiesCount = 1;
gameState.enemiesRemaining = gameState.enemiesRemaining + 1;
}
gameState.sectorStarbasesCount = 0;
if (Math.random() > gameOptions.starbaseSpawnChance) {
gameState.sectorStarbasesCount = 1;
gameState.starbasesRemaining = gameState.starbasesRemaining + 1;
}
// 1040
gameState.galacticMap[mapY][mapX] =
gameState.sectorEnemiesCount * 100 +
gameState.sectorStarbasesCount * 10 +
randomInt(gameOptions.maxStarsPerSector, 1);
}
}
if (gameState.enemiesRemaining > gameOptions.timeLimit) {
// Ensure the player has at least one more stardate than the number of enemies
gameOptions.timeLimit = gameState.enemiesRemaining + 1;
}
if (gameState.starbasesRemaining === 0) {
if (
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] < 200
) {
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] =
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] + 120;
}
gameState.enemiesRemaining = gameState.enemiesRemaining + 1;
gameState.starbasesRemaining = 1;
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] =
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] + 10;
gameState.quadrantPositionY = randomInt(gameOptions.galaxyHeight, 1);
gameState.quadrantPositionX = randomInt(gameOptions.galaxyWidth, 1);
}
gameState.enemiesInitialCount = gameState.enemiesRemaining;
await gameIntro();
await newQuadrantEntered();
}
async function newQuadrantEntered() {
gameState.sectorEnemiesCount = 0;
gameState.sectorStarbasesCount = 0;
gameState.sectorStarsCount = 0;
gameState.starbaseRepairDelay = 0.5 * Math.random();
// Add this sector to the known map
gameState.galacticMapDiscovered[gameState.quadrantPositionY][
gameState.quadrantPositionX
] =
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
];
// Initialize a sector enemy for each that had a chance to spawn
gameState.sectorEnemies = gameOptions.enemySpawnChance.map((c) => ({
health: 0,
posY: 0,
posX: 0,
}));
if (
gameState.quadrantPositionY >= 1 &&
gameState.quadrantPositionY <= gameOptions.galaxyHeight &&
gameState.quadrantPositionX >= 1 &&
gameState.quadrantPositionX <= gameOptions.galaxyWidth
) {
let currentQuadrantName = buildQuadrantName(
gameState.quadrantPositionY,
gameState.quadrantPositionX
);
print();
if (gameOptions.stardateStart == gameState.stardateCurrent) {
print("YOUR MISSION BEGINS WITH YOUR STARSHIP LOCATED");
print(`IN THE GALACTIC QUADRANT, '${currentQuadrantName}'.`);
} else {
print(`NOW ENTERING ${currentQuadrantName} QUADRANT . . .`);
}
print();
gameState.sectorEnemiesCount = Math.floor(
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] * 0.01
);
gameState.sectorStarbasesCount =
Math.floor(
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] * 0.1
) -
10 * gameState.sectorEnemiesCount;
gameState.sectorStarsCount =
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] -
100 * gameState.sectorEnemiesCount -
10 * gameState.sectorStarbasesCount;
if (gameState.sectorEnemiesCount != 0) {
print("COMBAT AREA CONDITION RED");
if (gameState.shieldsCurrent <= 200) {
print(" SHIELDS DANGEROUSLY LOW");
}
}
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
gameState.sectorEnemies[enemyIdx].posY = 0;
gameState.sectorEnemies[enemyIdx].posX = 0;
}
}
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
gameState.sectorEnemies[enemyIdx].health = 0;
}
gameState.sectorMap = " ".repeat(
gameOptions.sectorMapSymbols.empty.length *
gameOptions.sectorHeight *
gameOptions.sectorWidth
);
insertInSectorMap(
gameOptions.sectorMapSymbols.hero,
gameState.sectorPositionY,
gameState.sectorPositionX
);
if (gameState.sectorEnemiesCount >= 1) {
// 1720
for (
let enemyIdx = 0;
enemyIdx < gameState.sectorEnemiesCount;
enemyIdx++
) {
const [posY, posX] = findSpaceInSectorMap();
insertInSectorMap(gameOptions.sectorMapSymbols.enemy, posY, posX);
gameState.sectorEnemies[enemyIdx] = {
posY,
posX,
health: gameOptions.enemyMaxShield * (0.5 + Math.random()),
};
}
}
if (gameState.sectorStarbasesCount >= 1) {
const [R1, R2] = findSpaceInSectorMap();
gameState.sectorStarbaseY = R1;
gameState.sectorStarbaseX = R2;
insertInSectorMap(gameOptions.sectorMapSymbols.base, R1, R2);
}
for (let i = 1; i <= gameState.sectorStarsCount; i++) {
insertInSectorMap(
gameOptions.sectorMapSymbols.star,
...findSpaceInSectorMap()
);
}
return shortRangeSensorScanAndStartup();
}
async function gameLoop() {
while (!gameState.gameOver) {
await acceptCommand();
if (gameState.gameOver) {
await endOfGame();
}
if (gameState.shouldRestart) {
await gameReset();
}
}
}
async function acceptCommand() {
if (
gameState.shieldsCurrent + gameState.energyRemaining <= 10 ||
(gameState.energyRemaining < 10 &&
gameState.systemsDamage[SYSTEM_SHIELD_CONTROL] != 0)
) {
print();
print("** FATAL ERROR ** YOU'VE JUST STRANDED YOUR SHIP IN SPACE");
print("YOU HAVE INSUFFICIENT MANEUVERING ENERGY, AND SHIELD CONTROL");
print("IS PRESENTLY INCAPABLE OF CROSS-CIRCUITING TO ENGINE ROOM!!");
print();
gameState.gameOver = true;
return;
}
const commandInput = (await input("COMMAND")).trim().toUpperCase();
const command = COMMANDS[commandInput] || commandHelp;
await command();
}
/************************************************************************/
const COMMANDS = {
NAV: commandCourseControl,
SRS: shortRangeSensorScanAndStartup,
LRS: commandLongRangeScan,
PHA: commandPhaserControl,
TOR: commandPhotonTorpedo,
SHE: commandShieldControl,
DAM: commandDamageControl,
COM: commandLibraryComputer,
XXX: () => {
// todo more confirmation here?
gameState.gameOver = true;
gameState.gameQuit = true;
},
DUMP: () => {
console.log(JSON.stringify(gameState));
},
};
async function commandHelp() {
print("ENTER ONE OF THE FOLLOWING:");
print(" NAV (TO SET COURSE)");
print(" SRS (FOR SHORT RANGE SENSOR SCAN)");
print(" LRS (FOR LONG RANGE SENSOR SCAN)");
print(" PHA (TO FIRE PHASERS)");
print(" TOR (TO FIRE PHOTON TORPEDOES)");
print(" SHE (TO RAISE OR LOWER SHIELDS)");
print(" DAM (FOR DAMAGE CONTROL REPORTS)");
print(" COM (TO CALL ON LIBRARY-COMPUTER)");
print(" XXX (TO RESIGN YOUR COMMAND)");
print();
}
async function shortRangeSensorScanAndStartup() {
checkIfDocked();
if (gameState.isDocked) {
gameState.alertCondition = "DOCKED";
gameState.energyRemaining = gameOptions.energyMax;
gameState.photonTorpedoesRemaining = gameOptions.photonTorpedoesMax;
print("SHIELDS DROPPED FOR DOCKING PURPOSES");
gameState.shieldsCurrent = 0;
} else {
gameState.alertCondition = "GREEN";
if (gameState.energyRemaining < gameOptions.energyMax * 0.1)
gameState.alertCondition = "YELLOW";
if (gameState.sectorEnemiesCount > 0) gameState.alertCondition = "RED";
}
if (gameState.systemsDamage[SYSTEM_SHORT_RANGE_SENSORS] < 0) {
print();
print("*** SHORT RANGE SENSORS ARE OUT ***");
print();
return;
}
const statusLines = [
`STARDATE ${formatStardate(gameState.stardateCurrent)}`,
`CONDITION ${gameState.alertCondition}`,
`QUADRANT ${gameState.quadrantPositionY} , ${gameState.quadrantPositionX}`,
`SECTOR ${gameState.sectorPositionY} , ${gameState.sectorPositionX}`,
`PHOTON TORPEDOES ${gameState.photonTorpedoesRemaining}`,
`TOTAL ENERGY ${
gameState.energyRemaining + gameState.shieldsCurrent
}`,
`SHIELDS ${gameState.shieldsCurrent}`,
`${gameOptions.nameEnemies} REMAINING ${gameState.enemiesRemaining}`,
];
const lineSplit = new RegExp(
`.{${gameOptions.sectorMapSymbols.empty.length * gameOptions.sectorWidth}}`,
"g"
);
const cellSplit = new RegExp(
`.{${gameOptions.sectorMapSymbols.empty.length}}`,
"g"
);
const borderLine = "-".repeat(
(gameOptions.sectorMapSymbols.empty.length + 1) * gameOptions.sectorWidth
);
print(borderLine);
print(
gameState.sectorMap
// Split the map into lines of 24 chars
.match(lineSplit)
// Split each line into cells of 3 chars
.map((line) => line.match(cellSplit))
// Format each line with Y coord, spaced out cells, and a line of status
.map((line, idx) => line.join(" ") + " ".repeat(4) + statusLines[idx])
// Finally, join all the lines with returns
.join("\n")
);
print(borderLine);
print();
}
function checkIfDocked() {
const { sectorPositionY: sY, sectorPositionX: sX } = gameState;
for (let posY = sY - 1; posY <= sY + 1; posY++) {
for (let posX = sX - 1; posX <= sX + 1; posX++) {
if (
posY >= 1 ||
posY <= gameOptions.sectorHeight ||
posX >= 1 ||
posX <= gameOptions.sectorWidth
) {
if (findInsectorMap(gameOptions.sectorMapSymbols.base, posY, posX)) {
gameState.isDocked = true;
return;
}
}
}
}
gameState.isDocked = false;
}
async function commandCourseControl() {
let courseInput = parseFloat(await input("COURSE (0-9)"));
if (courseInput == 9) courseInput = 1;
if (isNaN(courseInput) || courseInput < 1 || courseInput > 9) {
print(
` ${gameOptions.nameNavigationOfficer} REPORTS, 'INCORRECT COURSE DATA, SIR!'`
);
return;
}
const warpFactorInput = parseFloat(
await input(
`WARP FACTOR (0-${
gameState.systemsDamage[SYSTEM_WARP_ENGINES] < 0 ? "0.2" : "8"
})`
)
);
if (warpFactorInput == 0 || isNaN(warpFactorInput)) return;
if (
gameState.systemsDamage[SYSTEM_WARP_ENGINES] < 0 &&
warpFactorInput > 0.2
) {
return print("WARP ENGINES ARE DAMAGED. MAXIMUM SPEED = WARP 0.2");
}
if (warpFactorInput < 0 && warpFactorInput > 8) {
return print(
` CHIEF ENGINEER ${gameOptions.nameChiefEngineer} REPORTS 'THE ENGINES WON'T TAKE WARP ${warpFactorInput}!'`
);
}
// FIXME: This seems to depend on square sectors - which we have, but could be changed in config
const sectorsToWarp = Math.floor(warpFactorInput * gameOptions.sectorWidth + 0.5);
if (gameState.energyRemaining - sectorsToWarp < 0) {
print("ENGINEERING REPORTS 'INSUFFICIENT ENERGY AVAILABLE");
print(
" FOR MANEUVERING AT WARP ",
warpFactorInput,
" !'"
);
if (
gameState.shieldsCurrent > sectorsToWarp - gameState.energyRemaining &&
gameState.systemsDamage[SYSTEM_SHIELD_CONTROL] > 0
) {
print(
"DEFLECTOR CONTROL ROOM ACKNOWLEDGES ",
gameState.shieldsCurrent,
" UNITS OF ENERGY"
);
print(" PRESENTLY DEPLOYED TO SHIELDS.");
}
}
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
if (gameState.sectorEnemies[enemyIdx].health > 0) {
insertInSectorMap(
gameOptions.sectorMapSymbols.empty,
gameState.sectorEnemies[enemyIdx].posY,
gameState.sectorEnemies[enemyIdx].posX
);
const [rY, rX] = findSpaceInSectorMap();
gameState.sectorEnemies[enemyIdx].posY = rY;
gameState.sectorEnemies[enemyIdx].posX = rX;
insertInSectorMap(
gameOptions.sectorMapSymbols.enemy,
gameState.sectorEnemies[enemyIdx].posY,
gameState.sectorEnemies[enemyIdx].posX
);
}
}
enemiesShoot();
let damageControlHeaderPrinted = false;
const printDamageReport = (msg) => {
if (!damageControlHeaderPrinted) {
damageControlHeaderPrinted = true;
print("DAMAGE CONTROL REPORT:");
}
print(msg);
};
let repairFactorDuringWarp = Math.min(1, warpFactorInput);
// Continually repair damaged systems during warp
for (const systemName of gameOptions.shipSystems) {
if (gameState.systemsDamage[systemName] >= 0) continue;
gameState.systemsDamage[systemName] =
gameState.systemsDamage[systemName] + repairFactorDuringWarp;
if (
gameState.systemsDamage[systemName] > -0.1 &&
gameState.systemsDamage[systemName] < 0
) {
gameState.systemsDamage[systemName] = -0.1;
continue;
}
if (gameState.systemsDamage[systemName] < 0) continue;
printDamageReport(` ${systemName} REPAIR COMPLETED.`);
}
// 20% chance of a random system being damaged, repaired, or improved in warp
if (Math.random() < gameOptions.systemChanceAffectedInWarp) {
const systemIdx = randomInt(gameOptions.shipSystems.length);
const systemName = gameOptions.shipSystems[systemIdx];
if (Math.random() < gameOptions.systemChanceDamageInWarp) {
// 60% chance of random system damage
gameState.systemsDamage[systemName] =
gameState.systemsDamage[systemName] - (Math.random() * 5 + 1);
printDamageReport(` ${systemName} DAMAGED`);
} else {
// 40% chance of random system repair or improvement
gameState.systemsDamage[systemName] =
gameState.systemsDamage[systemName] + Math.random() * 3 + 1;
printDamageReport(` ${systemName} STATE OF REPAIR IMPROVED`);
}
print();
}
// 3060 REM BEGIN MOVING STARSHIP
insertInSectorMap(
gameOptions.sectorMapSymbols.empty,
Math.floor(gameState.sectorPositionY),
Math.floor(gameState.sectorPositionX)
);
const [courseDeltaY, courseDeltaX] = courseToDeltaXY(courseInput);
let currentSectorPositionY = gameState.sectorPositionY;
let currentSectorPositionX = gameState.sectorPositionX;
let currentQuadrantPosY = gameState.quadrantPositionY;
let currentQuadrantPosX = gameState.quadrantPositionX;
for (let sectorsWarped = 1; sectorsWarped < sectorsToWarp; sectorsWarped++) {
gameState.sectorPositionY = gameState.sectorPositionY + courseDeltaY;
gameState.sectorPositionX = gameState.sectorPositionX + courseDeltaX;
if (
gameState.sectorPositionY < 1 ||
gameState.sectorPositionY >= 9 ||
gameState.sectorPositionX < 1 ||
gameState.sectorPositionX >= 9
) {
// 3490 REM EXCEEDED QUADRANT LIMITS
currentSectorPositionY =
gameOptions.sectorHeight * gameState.quadrantPositionY +
currentSectorPositionY +
sectorsToWarp * courseDeltaY;
currentSectorPositionX =
gameOptions.sectorWidth * gameState.quadrantPositionX +
currentSectorPositionX +
sectorsToWarp * courseDeltaX;
gameState.quadrantPositionY = Math.floor(currentSectorPositionY / 8);
gameState.quadrantPositionX = Math.floor(currentSectorPositionX / 8);
gameState.sectorPositionY = Math.floor(
currentSectorPositionY - gameState.quadrantPositionY * 8
);
gameState.sectorPositionX = Math.floor(
currentSectorPositionX - gameState.quadrantPositionX * 8
);
if (gameState.sectorPositionY == 0) {
gameState.quadrantPositionY = gameState.quadrantPositionY - 1;
gameState.sectorPositionY = 8;
}
if (gameState.sectorPositionX == 0) {
gameState.quadrantPositionX = gameState.quadrantPositionX - 1;
gameState.sectorPositionX = 8;
}
let galacticPerimeterHit = false;
if (gameState.quadrantPositionY < 1) {
galacticPerimeterHit = true;
gameState.quadrantPositionY = 1;
gameState.sectorPositionY = 1;
}
if (gameState.quadrantPositionY > 8) {
galacticPerimeterHit = true;
gameState.quadrantPositionY = 8;
gameState.sectorPositionY = 8;
}
if (gameState.quadrantPositionX < 1) {
galacticPerimeterHit = true;
gameState.quadrantPositionX = 1;
gameState.sectorPositionX = 1;
}
if (gameState.quadrantPositionX > 8) {
galacticPerimeterHit = true;
gameState.quadrantPositionX = 8;
gameState.sectorPositionX = 8;
}
if (galacticPerimeterHit) {
print(
`${gameOptions.nameCommunicationsOfficer} REPORTS MESSAGE FROM STARFLEET COMMAND:`
);
print(" 'PERMISSION TO ATTEMPT CROSSING OF GALACTIC PERIMETER");
print(" IS HEREBY *DENIED*. SHUT DOWN YOUR ENGINES.'");
print(
`CHIEF ENGINEER ${gameOptions.nameChiefEngineer} REPORTS 'WARP ENGINES SHUT DOWN`
);
print(
` AT SECTOR ${gameState.sectorPositionY} , ${gameState.sectorPositionX} OF QUADRANT ${gameState.quadrantPositionY} , ${gameState.quadrantPositionX}.'`
);
if (checkIfTimeExpired()) {
return;
}
}
if (
gameOptions.sectorHeight * gameState.quadrantPositionY + gameState.quadrantPositionX ==
gameOptions.sectorHeight * currentQuadrantPosY + currentQuadrantPosX
) {
break;
}
gameState.stardateCurrent = gameState.stardateCurrent + 1;
consumeEnergyForWarp(sectorsToWarp);
return newQuadrantEntered();
}
if (
!findInsectorMap(
gameOptions.sectorMapSymbols.empty,
gameState.sectorPositionY,
gameState.sectorPositionX
)
) {
// Undo this step of warp travel if the space isn't empty
gameState.sectorPositionY = Math.floor(
gameState.sectorPositionY - courseDeltaY
);
gameState.sectorPositionX = Math.floor(
gameState.sectorPositionX - courseDeltaX
);
print(
`WARP ENGINES SHUT DOWN AT SECTOR ${gameState.sectorPositionY} , ${gameState.sectorPositionX} DUE TO BAD NAVAGATION`
);
break;
}
}
gameState.sectorPositionY = Math.floor(gameState.sectorPositionY);
gameState.sectorPositionX = Math.floor(gameState.sectorPositionX);
insertInSectorMap(
gameOptions.sectorMapSymbols.hero,
Math.floor(gameState.sectorPositionY),
Math.floor(gameState.sectorPositionX)
);
consumeEnergyForWarp(sectorsToWarp);
let timeElapsedDuringWarp = 1;
if (warpFactorInput < 1) {
timeElapsedDuringWarp = 0.1 * Math.floor(10 * warpFactorInput);
}
gameState.stardateCurrent = gameState.stardateCurrent + timeElapsedDuringWarp;
if (checkIfTimeExpired()) {
return;
}
await shortRangeSensorScanAndStartup();
}
function checkIfTimeExpired() {
if (
gameState.stardateCurrent >
gameOptions.stardateStart + gameOptions.timeLimit
) {
gameState.gameOver = true;
}
return gameState.gameOver;
}
function consumeEnergyForWarp(sectorsToWarp) {
// 3900 REM MANEUVER ENERGY S/R **
gameState.energyRemaining = gameState.energyRemaining - sectorsToWarp - 10;
if (gameState.energyRemaining >= 0) {
return;
}
print("SHIELD CONTROL SUPPLIES ENERGY TO COMPLETE THE MANEUVER.");
gameState.shieldsCurrent =
gameState.shieldsCurrent + gameState.energyRemaining;
gameState.energyRemaining = 0;
if (gameState.shieldsCurrent <= 0) {
gameState.shieldsCurrent = 0;
}
}
async function commandLongRangeScan() {
// 3990 REM LONG RANGE SENSOR SCAN CODE
if (gameState.systemsDamage[SYSTEM_LONG_RANGE_SENSORS] < 0) {
print("LONG RANGE SENSORS ARE INOPERABLE");
return;
}
print(
"LONG RANGE SCAN FOR QUADRANT ",
gameState.quadrantPositionY,
" , ",
gameState.quadrantPositionX
);
const separatorLine = "-------------------";
print(separatorLine);
for (
let posY = gameState.quadrantPositionY - 1;
posY <= gameState.quadrantPositionY + 1;
posY++
) {
// Scan a line of sectors
const lineSectors = [null, null, null];
for (
let posX = gameState.quadrantPositionX - 1;
posX <= gameState.quadrantPositionX + 1;
posX++
) {
if (posY > 0 && posY < 9 && posX > 0 && posX < 9) {
// Add the scanned cell to the current scan output
lineSectors[posX - gameState.quadrantPositionX + 1] =
gameState.galacticMap[posY][posX];
// Add the scanned cell to the discovered map
gameState.galacticMapDiscovered[posY][posX] =
gameState.galacticMap[posY][posX];
}
}
// Print a formatted line of the scan - e.g. ": 004 : 205 : 004 :"
print(
": " +
lineSectors
.map((sector) =>
sector === null ? "***" : sector.toString().padStart(3, "0")
)
.join(" : ") +
" :"
);
print(separatorLine);
}
}
async function commandPhaserControl() {
// 4250 REM PHASER CONTROL CODE BEGINS HERE
if (gameState.systemsDamage[SYSTEM_PHASER_CONTROL] < 0) {
print("PHASERS INOPERATIVE");
return;
}
if (gameState.sectorEnemiesCount <= 0) {
print(
`SCIENCE OFFICER ${gameOptions.nameScienceOfficer} REPORTS 'SENSORS SHOW NO ENEMY SHIPS`
);
print(" IN THIS QUADRANT'");
return;
}
if (gameState.systemsDamage[SYSTEM_LIBRARY_COMPUTER] < 0) {
print("COMPUTER FAILURE HAMPERS ACCURACY");
}
print(
"PHASERS LOCKED ON TARGET; ENERGY AVAILABLE = ",
gameState.energyRemaining,
" UNITS"
);
let phaserUnitsToFire;
const continueCommandLoop = true;
while (continueCommandLoop) {
phaserUnitsToFire = parseFloat(await input("NUMBER OF UNITS TO FIRE"));
if (phaserUnitsToFire <= 0) return;
if (gameState.energyRemaining - phaserUnitsToFire >= 0) {
break;
}
print(`ENERGY AVAILABLE = ${gameState.energyRemaining} UNITS`);
}
gameState.energyRemaining = gameState.energyRemaining - phaserUnitsToFire;
// FIXED: in the original, this was shield system. Changed to phaser system.
if (gameState.systemsDamage[SYSTEM_PHASER_CONTROL] < 0) {
phaserUnitsToFire = phaserUnitsToFire * Math.random();
}
// Spread phaser fire between all enemies
let phaserUnitsPerEnemy = Math.floor(
phaserUnitsToFire / gameState.sectorEnemiesCount
);
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
if (gameState.sectorEnemies[enemyIdx].health <= 0) {
// Skip dead enemies
continue;
}
print();
// Phaser damage falls off based on distance and a bit of chance
let phaserDamage = Math.floor(
(phaserUnitsPerEnemy / distanceFromEnemy(enemyIdx)) * (Math.random() + 2)
);
if (phaserDamage <= 0.15 * gameState.sectorEnemies[enemyIdx].health) {
print(
"SENSORS SHOW NO DAMAGE TO ENEMY AT ",
gameState.sectorEnemies[enemyIdx].posY,
" , ",
gameState.sectorEnemies[enemyIdx].posX
);
continue;
}
gameState.sectorEnemies[enemyIdx].health -= phaserDamage;
print(
`${phaserDamage} UNIT HIT ON ${gameOptions.nameEnemy} AT SECTOR ${gameState.sectorEnemies[enemyIdx].posY} , ${gameState.sectorEnemies[enemyIdx].posX}`
);
if (gameState.sectorEnemies[enemyIdx].health > 0) {
print(
` (SENSORS SHOW ${gameState.sectorEnemies[enemyIdx].health} UNITS REMAINING)`
);
print();
} else {
print(`*** ${gameOptions.nameEnemy} DESTROYED ***`);
print();
gameState.sectorEnemiesCount = gameState.sectorEnemiesCount - 1;
gameState.enemiesRemaining = gameState.enemiesRemaining - 1;
// Remove enemy from display
insertInSectorMap(
gameOptions.sectorMapSymbols.empty,
gameState.sectorEnemies[enemyIdx].posY,
gameState.sectorEnemies[enemyIdx].posX
);
// Set enemy health at exactly zero
gameState.sectorEnemies[enemyIdx].health = 0;
// Update the galactic map with one fewer enemy
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] -= 100;
// Copy updated galactic map sector to discovered map.
gameState.galacticMapDiscovered[gameState.quadrantPositionY][
gameState.quadrantPositionX
] =
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
];
if (gameState.enemiesRemaining <= 0) {
// If that was the last enemy, we've won!
gameState.gameOver = true;
gameState.gameWon = true;
return;
}
}
}
enemiesShoot();
}
async function commandPhotonTorpedo() {
// 4690 REM PHOTON TORPEDO CODE BEGINS HERE
// 4700
if (gameState.photonTorpedoesRemaining <= 0) {
return print("ALL PHOTON TORPEDOES EXPENDED");
}
if (gameState.systemsDamage[SYSTEM_PHOTON_TUBES] < 0) {
return print("PHOTON TUBES ARE NOT OPERATIONAL");
}
let torpedoCourse = parseFloat(await input("PHOTON TORPEDO COURSE (1-9)"));
if (torpedoCourse == 9) torpedoCourse = 1;
if (torpedoCourse < 1 || torpedoCourse > 9) {
print(
`${gameOptions.nameWeaponsOfficer} REPORTS, 'INCORRECT COURSE DATA, SIR!'`
);
}
const [courseDeltaY, courseDeltaX] = courseToDeltaXY(torpedoCourse);
gameState.energyRemaining = gameState.energyRemaining - 2;
gameState.photonTorpedoesRemaining = gameState.photonTorpedoesRemaining - 1;
let currPosY = gameState.sectorPositionY;
let currPosX = gameState.sectorPositionX;
print("TORPEDO TRACK:");
// Fly the torpedo along its course...
let quantizedPosY, quantizedPosX;
const forever = true;
while (forever) {
currPosY = currPosY + courseDeltaY;
currPosX = currPosX + courseDeltaX;
// The course will move in decimals, quantize to whole numbers
quantizedPosY = Math.floor(currPosY + 0.5);
quantizedPosX = Math.floor(currPosX + 0.5);
// Exiting the sector means the torpedo missed
if (
quantizedPosY < 1 ||
quantizedPosY > gameOptions.sectorHeight ||
quantizedPosX < 1 ||
quantizedPosX > gameOptions.sectorWidth
) {
print("TORPEDO MISSED");
return enemiesShoot();
}
print(` ${quantizedPosY} , ${quantizedPosX}`);
if (
!findInsectorMap(
gameOptions.sectorMapSymbols.empty,
quantizedPosY,
quantizedPosX
)
) {
// Torpedo hit something solid, so stop flying.
break;
}
}
// Did the torpedo hit an enemy?
if (
findInsectorMap(
gameOptions.sectorMapSymbols.enemy,
quantizedPosY,
quantizedPosX
)
) {
print(`*** ${gameOptions.nameEnemy} DESTROYED ***`);
gameState.sectorEnemiesCount = gameState.sectorEnemiesCount - 1;
gameState.enemiesRemaining = gameState.enemiesRemaining - 1;
if (gameState.enemiesRemaining <= 0) {
// If that was the last enemy, then we've won!
gameState.gameOver = true;
gameState.gameWon = true;
return;
}
// Find which enemy was hit and set health to zero
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
if (
quantizedPosY == gameState.sectorEnemies[enemyIdx].posY &&
quantizedPosX == gameState.sectorEnemies[enemyIdx].posX
) {
gameState.sectorEnemies[enemyIdx].health = 0;
break;
}
}
}
// Did the torpedo hit a star?
if (
findInsectorMap(
gameOptions.sectorMapSymbols.star,
quantizedPosY,
quantizedPosX
)
) {
print(
`STAR AT ${quantizedPosY} , ${quantizedPosX} ABSORBED TORPEDO ENERGY.`
);
return enemiesShoot();
}
// Did the torpedo hit a starbase?
if (
findInsectorMap(
gameOptions.sectorMapSymbols.base,
quantizedPosY,
quantizedPosX
)
) {
print("*** STARBASE DESTROYED ***");
gameState.sectorStarbasesCount = gameState.sectorStarbasesCount - 1;
gameState.starbasesRemaining = gameState.starbasesRemaining - 1;
if (
gameState.starbasesRemaining <= 0 ||
gameState.enemiesRemaining <=
gameState.stardateCurrent -
gameOptions.stardateStart -
gameOptions.timeLimit
) {
print("THAT DOES IT, CAPTAIN!! YOU ARE HEREBY RELIEVED OF COMMAND");
print("AND SENTENCED TO 99 STARDATES AT HARD LABOR ON CYGNUS 12!!");
gameState.gameOver = true;
return;
} else {
print("STARFLEET COMMAND REVIEWING YOUR RECORD TO CONSIDER");
print("COURT MARTIAL!");
gameState.isDocked = false;
}
}
// If we hit an enemy or a starbase, update the sector and galaxy map to
// remove the thing destroyed
insertInSectorMap(
gameOptions.sectorMapSymbols.empty,
quantizedPosY,
quantizedPosX
);
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
] =
gameState.sectorEnemiesCount * 100 +
gameState.sectorStarbasesCount * 10 +
gameState.sectorStarsCount;
gameState.galacticMapDiscovered[gameState.quadrantPositionY][
gameState.quadrantPositionX
] =
gameState.galacticMap[gameState.quadrantPositionY][
gameState.quadrantPositionX
];
return enemiesShoot();
}
async function enemiesShoot() {
if (gameState.sectorEnemiesCount <= 0) {
return;
}
if (gameState.isDocked) {
print("STARBASE SHIELDS PROTECT THE ENTERPRISE");
return;
}
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
if (gameState.sectorEnemies[enemyIdx].health <= 0) {
continue;
}
// Enemy damage based on health with drop-off for distance and chance
const enemyWeaponDamage = Math.floor(
(gameState.sectorEnemies[enemyIdx].health / distanceFromEnemy(enemyIdx)) *
(2 + Math.random())
);
gameState.shieldsCurrent = gameState.shieldsCurrent - enemyWeaponDamage;
// Consume enemy health for firing weapon
gameState.sectorEnemies[enemyIdx].health = Math.floor(
gameState.sectorEnemies[enemyIdx].health / (3 + Math.random())
);
print(
`${enemyWeaponDamage} UNIT HIT ON ENTERPRISE FROM SECTOR ${gameState.sectorEnemies[enemyIdx].posY} , ${gameState.sectorEnemies[enemyIdx].posX}`
);
if (gameState.shieldsCurrent <= 0) {
// If we're out of shields, we're out of luck
gameState.gameOver = true;
gameState.destroyed = true;
return;
}
print(` <SHIELDS DOWN TO ${gameState.shieldsCurrent} UNITS>`);
if (enemyWeaponDamage < 20) {
continue;
}
// Systems damage with 60% chance or a hit of more than 2% of shields
if (
Math.random() > gameOptions.systemDamageChanceOnHit ||
enemyWeaponDamage / gameState.shieldsCurrent <=
gameOptions.systemDamageHitThroughShields
) {
continue;
}
// Random system damaged proportional to enemy damage and current shields
const systemIdx = randomInt(gameOptions.shipSystems.length);
const systemName = gameOptions.shipSystems[systemIdx];
gameState.systemsDamage[systemName] =
gameState.systemsDamage[systemName] -
enemyWeaponDamage / gameState.shieldsCurrent -
0.5 * Math.random();
print(`DAMAGE CONTROL REPORTS ${systemName} DAMAGED BY THE HIT`);
}
}
async function commandShieldControl() {
// 5520 REM SHIELD CONTROL
if (gameState.systemsDamage[SYSTEM_SHIELD_CONTROL] < 0) {
print("SHIELD CONTROL INOPERABLE");
return;
}
print(
"ENERGY AVAILABLE = ",
gameState.energyRemaining + gameState.shieldsCurrent
);
const shieldUnits = parseFloat(await input("NUMBER OF UNITS TO SHIELDS"));
if (shieldUnits < 0 || gameState.shieldsCurrent == shieldUnits) {
print("<SHIELDS UNCHANGED>");
return;
}
if (shieldUnits > gameState.energyRemaining + gameState.shieldsCurrent) {
print("SHIELD CONTROL REPORTS 'THIS IS NOT THE FEDERATION TREASURY.'");
print("<SHIELDS UNCHANGED>");
return;
}
gameState.energyRemaining =
gameState.energyRemaining + gameState.shieldsCurrent - shieldUnits;
gameState.shieldsCurrent = shieldUnits;
print("DEFLECTOR CONTROL ROOM REPORT:");
print(
` 'SHIELDS NOW AT ${Math.floor(
gameState.shieldsCurrent
)} UNITS PER YOUR COMMAND.`
);
}
async function commandDamageControl() {
// 5680 REM DAMAGE CONTROL
// 5690
// FIXME: Seems like damage control should work while docked?
if (gameState.systemsDamage[SYSTEM_DAMAGE_CONTROL] < 0) {
print("DAMAGE CONTROL REPORT NOT AVAILABLE");
return;
}
// 5910
print();
print("DEVICE STATE OF REPAIR");
for (const systemName of gameOptions.shipSystems) {
print(
systemName.padEnd(25, " "),
Math.floor(gameState.systemsDamage[systemName] * 100) * 0.01
);
}
print();
if (gameState.isDocked) {
let repairTimeEstimate = 0;
for (const systemName of gameOptions.shipSystems) {
if (gameState.systemsDamage[systemName] < 0) {
repairTimeEstimate = repairTimeEstimate + 0.1;
}
}
if (repairTimeEstimate == 0) {
return;
}
print();
repairTimeEstimate = repairTimeEstimate + gameState.starbaseRepairDelay;
if (repairTimeEstimate >= 1) {
repairTimeEstimate = 0.9;
}
print("TECHNICIANS STANDING BY TO EFFECT REPAIRS TO YOUR SHIP;");
print(
`ESTIMATED TIME TO REPAIR: ${
0.01 * Math.floor(100 * repairTimeEstimate)
} STARDATES`
);
const authorizeRepairInput = await input(
"WILL YOU AUTHORIZE THE REPAIR ORDER (Y/N)"
);
if (authorizeRepairInput.toUpperCase() != "Y") {
return;
}
for (const systemName of gameOptions.shipSystems) {
gameState.systemsDamage[systemName] = 0;
}
gameState.stardateCurrent =
gameState.stardateCurrent + repairTimeEstimate + 0.1;
}
}
async function commandLibraryComputer() {
// 7280 REM LIBRARY COMPUTER CODE
// 7290
if (gameState.systemsDamage[SYSTEM_LIBRARY_COMPUTER] < 0) {
print("COMPUTER DISABLED");
return;
}
const commandInput = parseInt(
await input("COMPUTER ACTIVE AND AWAITING COMMAND")
);
if (commandInput < 0) return;
const command = COMMANDS_COMPUTER[commandInput] || computerHelp;
print();
await command();
}
const COMMANDS_COMPUTER = [
computerCumulativeRecord,
computerStatusReport,
computerPhotonData,
computerStarbaseData,
computerDirectionData,
computerGalaxyMap,
];
async function computerHelp() {
print("FUNCTIONS AVAILABLE FROM LIBRARY-COMPUTER:");
print(" 0 = CUMULATIVE GALACTIC RECORD");
print(" 1 = STATUS REPORT");
print(" 2 = PHOTON TORPEDO DATA");
print(" 3 = STARBASE NAV DATA");
print(" 4 = DIRECTION/DISTANCE CALCULATOR");
print(" 5 = GALAXY 'REGION NAME' MAP");
print();
}
async function computerPhotonData() {
if (gameState.sectorEnemiesCount <= 0) {
print(
`SCIENCE OFFICER ${gameOptions.nameScienceOfficer} REPORTS 'SENSORS SHOW NO ENEMY SHIPS`
);
print(" IN THIS QUADRANT'");
return;
}
print(
`FROM ENTERPRISE TO ${gameOptions.nameEnemy} BATTLE CRUISER${
gameState.sectorEnemiesCount > 1 ? "S" : ""
}`
);
for (
let enemyIdx = 0;
enemyIdx < gameOptions.enemySpawnChance.length;
enemyIdx++
) {
if (gameState.sectorEnemies[enemyIdx].health <= 0) continue;
computerDirectionCommon({
fromY: gameState.sectorPositionY,
fromX: gameState.sectorPositionX,
toY: gameState.sectorEnemies[enemyIdx].posY,
toX: gameState.sectorEnemies[enemyIdx].posX,
});
}
}
async function computerStarbaseData() {
if (gameState.sectorStarbasesCount == 0) {
print(
`MR. ${gameOptions.nameScienceOfficer} REPORTS, 'SENSORS SHOW NO STARBASES IN THIS QUADRANT.'`
);
return;
}
print("FROM ENTERPRISE TO STARBASE:");
computerDirectionCommon({
fromY: gameState.sectorPositionY,
fromX: gameState.sectorPositionX,
toY: gameState.sectorStarbaseY,
toX: gameState.sectorStarbaseX,
});
}
const inputCoords = async (prompt) =>
(await input(prompt)).split(",").map((s) => parseInt(s.trim()));
async function computerDirectionData() {
print("DIRECTION/DISTANCE CALCULATOR:");
print(
`YOU ARE AT QUADRANT ${gameState.quadrantPositionY} , ${gameState.quadrantPositionX} SECTOR ${gameState.sectorPositionY} , ${gameState.sectorPositionX}`
);
print("PLEASE ENTER");
const [fromY, fromX] = await inputCoords(" INITIAL COORDINATES (Y,X)");
const [toY, toX] = await inputCoords(" FINAL COORDINATES (Y,X)");
computerDirectionCommon({ fromX, fromY, toX, toY });
}
async function computerDirectionCommon({ fromX, fromY, toX, toY }) {
const distance = Math.sqrt(
Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)
);
const direction =
1 +
(8 / (Math.PI * 2)) *
((Math.atan2(0 - fromY - (0 - toY), fromX - toX) + Math.PI) %
(Math.PI * 2));
print(`DIRECTION = ${direction}`);
print(`DISTANCE = ${distance}`);
}
async function computerStatusReport() {
print("STATUS REPORT:");
print();
print(
`${
gameState.enemiesRemaining > 1
? gameOptions.nameEnemies
: gameOptions.nameEnemy
} LEFT: ${gameState.enemiesRemaining}`
);
print(
`MISSION MUST BE COMPLETED IN ${
0.1 *
Math.floor(
(gameOptions.stardateStart +
gameOptions.timeLimit -
gameState.stardateCurrent) *
10
)
} STARDATES`
);
if (gameState.starbasesRemaining < 1) {
print("YOUR STUPIDITY HAS LEFT YOU ON YOUR ON IN");
print(" THE GALAXY -- YOU HAVE NO STARBASES LEFT!");
} else {
print(
`THE FEDERATION IS MAINTAINING ${gameState.starbasesRemaining} STARBASE${
gameState.starbasesRemaining < 2 ? "" : "S"
} IN THE GALAXY`
);
}
commandDamageControl();
}
async function computerGalaxyMap() {
print(" THE GALAXY");
computerCommonMap(false);
}
async function computerCumulativeRecord() {
print();
print(
` COMPUTER RECORD OF GALAXY FOR QUADRANT ${gameState.quadrantPositionY} , ${gameState.quadrantPositionX}`
);
print();
computerCommonMap();
}
async function computerCommonMap(showMapCells = true) {
// Print the X column number header based on width of first galaxy row
print(
" " +
gameState.galacticMap[1]
.map((_, idx) => idx.toString().padStart(3, " "))
.join(" ")
);
// Assemble X column separator based on width of first galaxy row
const separator =
" " + gameState.galacticMap[1].map((_, idx) => "----- ").join("");
print(separator);
for (let mapY = 1; mapY <= gameOptions.galaxyHeight; mapY++) {
let out = mapY.toString().padStart(3, " ");
if (showMapCells) {
// 7630
for (let mapX = 1; mapX <= gameOptions.galaxyWidth; mapX++) {
out += ` ${
gameState.galacticMapDiscovered[mapY][mapX] == 0
? "***"
: ("" + gameState.galacticMapDiscovered[mapY][mapX]).padStart(
3,
"0"
)
}`;
}
} else {
let quadrantName = buildQuadrantName(mapY, 1, true);
let centerSpacing = Math.floor(12 - 0.5 * quadrantName.length);
out += ` ${" ".repeat(centerSpacing)}${quadrantName}${" ".repeat(
centerSpacing
)}`;
quadrantName = buildQuadrantName(mapY, 5, true);
centerSpacing = Math.floor(12 - 0.5 * quadrantName.length);
out += `${" ".repeat(centerSpacing)}${quadrantName}`;
}
print(out);
print(separator);
}
}
async function endOfGame() {
if (gameState.destroyed) {
print();
print(
"THE ENTERPRISE HAS BEEN DESTROYED. THEN FEDERATION WILL BE CONQUERED"
);
}
print(`IT IS STARDATE ${formatStardate(gameState.stardateCurrent)}`);
if (!gameState.gameWon) {
print(
`THERE WERE ${gameState.enemiesRemaining} ${gameOptions.nameEnemy} BATTLE CRUISERS LEFT AT`
);
print("THE END OF YOUR MISSION.");
} else {
print(
`CONGRULATION, CAPTAIN! THEN LAST ${gameOptions.nameEnemy} BATTLE CRUISER`
);
print("MENACING THE FEDERATION HAS BEEN DESTROYED.");
print();
print(
"YOUR EFFICIENCY RATING IS ",
(1000 *
(gameState.enemiesInitialCount /
(gameState.stardateCurrent - gameOptions.stardateStart))) ^
2
);
}
print();
print();
if (gameState.starbasesRemaining > 0) {
print("THE FEDERATION IS IN NEED OF A NEW STARSHIP COMMANDER");
print("FOR A SIMILAR MISSION -- IF THERE IS A VOLUNTEER,");
const playAgainInput = await input("LET HIM STEP FORWARD AND ENTER 'AYE'");
if (playAgainInput.toUpperCase() == "AYE") {
gameState.shouldRestart = true;
return;
}
}
}
const COURSE_TO_XY = [
[0, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[0, -1],
[1, -1],
[1, 0],
[1, 1],
[0, 1],
];
function courseToDeltaXY(course) {
const courseIdx = Math.floor(course) - 1;
//3110 X1=C(C1,1)+(C(C1+1,1)-C(C1,1))*(C1-INT(C1)):X=S1:Y=S2
//3140 X2=C(C1,2)+(C(C1+1,2)-C(C1,2))*(C1-INT(C1)):Q4=Q1:Q5=Q2
const courseDeltaY =
COURSE_TO_XY[courseIdx][0] +
(COURSE_TO_XY[courseIdx + 1][0] - COURSE_TO_XY[courseIdx][0]) *
(course - Math.floor(course));
const courseDeltaX =
COURSE_TO_XY[courseIdx][1] +
(COURSE_TO_XY[courseIdx + 1][1] - COURSE_TO_XY[courseIdx][1]) *
(course - Math.floor(course));
return [courseDeltaY, courseDeltaX];
}
function findSpaceInSectorMap() {
let posY,
posX,
foundEmptyPlace = false;
while (!foundEmptyPlace) {
posY = randomInt(8, 1);
posX = randomInt(8, 1);
foundEmptyPlace = findInsectorMap(
gameOptions.sectorMapSymbols.empty,
posY,
posX
);
}
return [posY, posX];
}
function findInsectorMap(str, y, x) {
const idx = (x - 1) * 3 + (y - 1) * 24;
return gameState.sectorMap.substring(idx, idx + 3) == str;
}
// 8660 REM INSERT IN STRING ARRAY FOR QUADRANT
function insertInSectorMap(str, y, x) {
// 8670
const strPos = (x - 1) * 3 + (y - 1) * 24;
if (str.length != 3) {
throw "ERROR";
}
gameState.sectorMap =
gameState.sectorMap.slice(0, strPos) +
str +
gameState.sectorMap.slice(strPos + 3);
}
function buildQuadrantName(y, x, regionNameOnly = false) {
const xIdx = x - 1;
const yIdx = y - 1;
const name = gameOptions.quadrantNames[xIdx < 4 ? 0 : 1][yIdx];
return `${name}${
regionNameOnly ? "" : ` ${gameOptions.quadrantNumbers[xIdx % 4]}`
}`;
}
const randomInt = (max, min = 0) =>
Math.floor(min + Math.random() * (max - min));
const formatStardate = (stardate) => Math.floor(stardate * 10) / 10;
const distanceFromEnemy = (sectorEnemyIndex) =>
Math.sqrt(
Math.pow(
gameState.sectorEnemies[sectorEnemyIndex].posY -
gameState.sectorPositionY,
2
) +
Math.pow(
gameState.sectorEnemies[sectorEnemyIndex].posX -
gameState.sectorPositionX,
2
)
);