bondage-college-mirr/BondageClub/Scripts/Game.js
2026-06-09 22:11:38 -04:00

339 lines
9.4 KiB
JavaScript

"use strict";
/** BC's version */
var GameVersion = "R129";
const GameVersionFormat = /^R([0-9]+)(?:(Alpha|Beta)([0-9]+)?)?$/;
/** @type {number | null} */
var GameAnimationFrameId = null;
/** @type {Worker | null} */
var GameWorker = null;
var CommonVersionUpdated = false;
/** @type {TouchList | null} */
var CommonTouchList = null;
const DEFAULT_FRAMERATE = 60;
async function GameStart() {
ServerURL = CommonGetServer();
//CheatImport();
console.log("Version: " + GameVersion + ", Server: " + ServerURL);
if (!GameVersionFormat.test(GameVersion)) console.error("GameVersion is not valid!");
CurrentTime = CommonTime();
CommonIsMobile = CommonDetectMobile();
TranslationLoad();
DrawLoad();
AssetLoadAll();
AssetInventoryIDValidate();
CommandsLoad();
ControllerStart();
await TextPrefetchFile(InterfaceStringsPath).loadedPromise;
CommonSetScreen("Character", "Login");
ServerInit();
// Create a blank character for our player. Its actual ID will be set when LoginResponse happens
Player = /** @type {PlayerCharacter} */ (CharacterCreate("Female3DCG", CharacterType.SIMPLE, ""));
PreferenceInitPlayer(Player, {});
// Those event listeners are all going through a lambda so that mods can
// correctly hook into them. Using the functions directly causes a copy of
// them to be made (well, their code, actually), breaking that ability.
document.addEventListener("keydown", (e) => GameKeyDown(e));
document.addEventListener("keyup", (e) => GameKeyUp(e));
document.addEventListener("paste", (e) => GamePaste(e));
const canvas = document.getElementById("MainCanvas");
if (!(canvas instanceof HTMLCanvasElement)) {
// Canvas is already initialized by index.html; this code path should _never_ occur
throw new Error("Canvas is null");
}
canvas.addEventListener("wheel", (e) => GameMouseWheel(e));
canvas.addEventListener("pointerdown", (e) => GamePointerDown(e, canvas));
canvas.addEventListener("pointerup", (e) => GamePointerUp(e, canvas));
canvas.addEventListener("pointermove", (e) => GamePointerMove(e));
canvas.addEventListener("pointercancel", (e) => GamePointerCancel(e, canvas));
canvas.addEventListener("touchend", (e) => GameTouchEnd(e));
canvas.addEventListener("click", (e) => GameClick(e));
GameAnimationFrameId = requestAnimationFrame(GameRun);
// Can't use setInterval, chrome throttles it to 1 minute on inactive pages, but not in workers...
GameWorker = new Worker("Scripts/GameWorker.js");
GameWorker.onmessage = GameFallbackTimer;
}
/** Promises that resolve upon reaching specific stages of the BC loading and login process. */
const GameReadyState = {
/**
* Promise that resolves upon fully loading BC (but before logging in).
* This promise can safely be accessed starting from the `interactive` document ready state.
* @readonly
* @type {Promise<void>}
*/
load: new Promise(resolve => window.addEventListener("load", async () => {
// When the code is loaded, we start the game engine
await GameStart();
resolve();
})),
/**
* @private
* Promise that resolves upon succesfully logging in.
* Should not be called directly; use {@link ServerIsLoggedInAsync} instead.
* @type {undefined | Promise<void>}
*/
login: undefined,
};
function GameHandleError() {
if (GameAnimationFrameId != null) {
cancelAnimationFrame(GameAnimationFrameId);
GameAnimationFrameId = null;
}
if (GameWorker != null) {
GameWorker.onmessage = null;
GameWorker.terminate();
GameWorker = null;
}
}
/**
* Periodically called in the background with low frequency, so the game doesn't freeze, even if the user switches to a different tab.
* @returns {void}
*/
function GameFallbackTimer() {
let Timestamp = performance.now();
if (Timestamp - TimerLastTime > 500) {
GameRunBackground(Timestamp);
}
}
/**
* Main game running state, runs the drawing
* @param {number} Timestamp
*/
function GameRun(Timestamp) {
try {
GameAnimationFrameId = null;
if (TimerLastTime > 0 && Timestamp > 0) {
// Default to 30 fps outside the game
const maxUnfocusedFPS = document.hasFocus() ? 0 : Player?.GraphicsSettings?.MaxUnfocusedFPS ?? 0;
const maxFocusedFPS = Player?.GraphicsSettings?.MaxFPS ?? DEFAULT_FRAMERATE;
const maxFPS = maxUnfocusedFPS === 0 ? maxFocusedFPS :
maxFocusedFPS === 0 ? maxUnfocusedFPS :
Math.min(maxUnfocusedFPS, maxFocusedFPS);
if (TimerLastTime + ((1000 / maxFPS) | 0) > Timestamp) {
GameAnimationFrameId = requestAnimationFrame(GameRun);
return;
}
}
let frameTime = 10000;
if (Timestamp > 0) {
frameTime = Timestamp - TimerLastTime;
}
// Increments the time from the last frame
TimerRunInterval = Timestamp - TimerLastTime;
TimerLastTime = Timestamp;
CurrentTime += TimerRunInterval;
DrawProcess(Timestamp);
ControllerProcess();
TimerProcess();
ServerSendQueueProcess();
GameAnimationFrameId = requestAnimationFrame(GameRun);
if (Timestamp > 0 && Player?.GraphicsSettings?.ShowFPS) {
MainCanvas.textAlign = "center";
DrawTextFit((Math.round(10000 / frameTime / 10)).toString(), 15, 12, 30, "white", "black");
}
} catch (e) {
GameHandleError();
throw e;
}
}
/**
* Main game running state, when in the background. Skips drawing if possible.
* @param {number} Timestamp
*/
function GameRunBackground(Timestamp) {
try {
// Increments the time from the last frame
TimerRunInterval = Timestamp - TimerLastTime;
TimerLastTime = Timestamp;
CurrentTime = CurrentTime + TimerRunInterval;
if (CurrentCharacter == null && !ScreenIsLoading) {
CurrentScreenFunctions.Run(Timestamp);
}
// Ignore gamepad when in the background
TimerProcess();
ServerSendQueueProcess();
} catch (e) {
GameHandleError();
throw e;
}
}
/**
* When the user presses a key, we send the KeyDown event to the current screen if it can accept it
* @param {KeyboardEvent} event
*/
function GameKeyDown(event) {
if (ScreenIsLoading) return;
let handled = false;
handleKeyDown: {
if (ControllerIsActive() && ControllerSupportKeyDown(event)) {
handled = true;
break handleKeyDown;
}
if (CurrentScreen === "Preference" && PreferenceSubscreen?.name === "Keybindings" && KeyGUI.selectedAction) {
handled = true;
break handleKeyDown;
}
if (KeyManager.handleKeyPress(event)) {
handled = true;
break handleKeyDown;
}
if (CommonKey.IsPressed(event, "Escape")) {
if (document.activeElement instanceof HTMLElement && document.activeElement !== document.body) {
document.activeElement.blur();
handled = true;
break handleKeyDown;
} else if (CurrentCharacter) {
DialogMenuBack();
handled = true;
break handleKeyDown;
} else {
CurrentScreenFunctions.Exit();
handled = true;
break handleKeyDown;
}
}
if (DialogKeyDown(event)) {
handled = true;
break handleKeyDown;
}
if (CurrentScreenFunctions.KeyDown(event)) {
handled = true;
break handleKeyDown;
}
}
if (handled) {
event.preventDefault();
event.stopImmediatePropagation();
}
return handled;
}
/** @type {KeyboardEventListener} */
function GameKeyUp(event) {
if (ScreenIsLoading) return false;
if (StruggleMinigameIsRunning()) { return false; }
const handled = CurrentScreenFunctions.KeyUp(event);
if (handled) {
event.preventDefault();
event.stopImmediatePropagation();
}
return handled;
}
/** @type {ClipboardEventListener} */
function GamePaste(event) {
if (!ScreenIsLoading) {
CurrentScreenFunctions.Paste(event);
}
}
/**
* If the user presses the mouse button, we fire the mousedown event for other screens
* @param {PointerEvent} event
* @param {HTMLCanvasElement} canvas
*/
function GamePointerDown(event, canvas) {
CommonMouseDown(event);
// Guarantees that a `pointdown` event is followed up with `pointerup`
canvas.setPointerCapture(event.pointerId);
}
/**
* If the user releases the mouse button, we fire the mouseup and click events for other screens
* @param {PointerEvent} event
* @param {HTMLCanvasElement} canvas
*/
function GamePointerUp(event, canvas) {
GamePointerMove(event, false);
CommonMouseUp(event);
canvas.releasePointerCapture(event.pointerId);
}
/**
* If the user stops touching the screen (mobile only), we fire the mouseup event for other screens
* @param {TouchEvent} event
*/
function GameTouchEnd(event) {
CommonTouchList = event.touches;
}
/**
* If the user rolls the mouse wheel, we fire the mousewheel event for other screens
* @type {MouseWheelEventListener}
*/
function GameMouseWheel(event) {
CommonMouseWheel(event);
}
/**
* If the user moves the mouse mouse, we keep the mouse position for other scripts and fire the mousemove event for other screens
* @param {PointerEvent} event
*/
function GamePointerMove(event, forwardToScreens = true) {
MouseX = Math.round((event.clientX - MainCanvas.canvas.offsetLeft) * 2000 / MainCanvas.canvas.clientWidth);
MouseY = Math.round((event.clientY - MainCanvas.canvas.offsetTop) * 1000 / MainCanvas.canvas.clientHeight);
if (forwardToScreens) {
CommonMouseMove(event);
}
}
/**
* Only fire canvas clicks via the `click` event, lest there is the possibility of a canvas click and DOM click firing twice on certain
* platforms (_e.g._ Android) due to said mobile browsers firing both `touch<x>` and `mouse<x>` events
* @param {PointerEvent} event
*/
function GameClick(event) {
CommonClick(event);
}
/**
* @param {PointerEvent} event
* @param {HTMLCanvasElement} canvas
*/
function GamePointerCancel(event, canvas) {
MouseX = -1;
MouseY = -1;
CommonTouchList = null;
canvas.releasePointerCapture(event.pointerId);
}