diff --git a/src/brogue/MainMenu.c b/src/brogue/MainMenu.c index 7c12bd8..7f166a9 100644 --- a/src/brogue/MainMenu.c +++ b/src/brogue/MainMenu.c @@ -268,7 +268,12 @@ static void initializeMainMenuButton(brogueButton *button, char *textWithHotkey, button->command = command; } +#ifdef __EMSCRIPTEN__ +// No Quit on the web: the game is the page. +#define MAIN_MENU_BUTTON_COUNT 3 +#else #define MAIN_MENU_BUTTON_COUNT 4 +#endif /// @brief Initializes the main menu buttons /// @param buttons An array of buttons to initialize @@ -277,10 +282,14 @@ static void initializeMainMenuButtons(brogueButton *buttons) { initializeMainMenuButton(&(buttons[0]), " %sN%sew Game ", 'n', 'N', NG_NEW_GAME); initializeMainMenuButton(&(buttons[1]), " * %sP%slay ", 'p', 'P', NG_FLYOUT_PLAY); initializeMainMenuButton(&(buttons[2]), " * %sV%siew ", 'v', 'V', NG_FLYOUT_VIEW); +#ifdef __EMSCRIPTEN__ + for (int i=0; i +#include +#endif #include #include #include @@ -50,7 +54,12 @@ static void recordChar(unsigned char c) { } static void considerFlushingBufferToFile() { +#ifdef __EMSCRIPTEN__ + // On the web every turn is written at once, so the game in progress survives closing the page. + if (locationInRecordingBuffer > 0) { +#else if (locationInRecordingBuffer >= INPUT_RECORD_BUFFER) { +#endif flushBufferToFile(); } } @@ -1144,6 +1153,12 @@ static void getDefaultFilePath(char *defaultPath, boolean gameOver) { } if (!gameOver) { +#ifdef __EMSCRIPTEN__ + if (webSeeded) { + sprintf(defaultPath, "Saved %s seeded #%s at depth %d", gameConst->versionString, seed, rogue.depthLevel); + return; + } +#endif sprintf(defaultPath, "Saved %s #%s at depth %d", gameConst->versionString, seed, rogue.depthLevel); } else if (rogue.quit) { sprintf(defaultPath, "%s #%s Quit at depth %d", gameConst->versionString, seed, rogue.depthLevel); @@ -1251,7 +1266,11 @@ void saveRecording(char *filePathWithoutSuffix) { } } else { // Declined to save recording; save it anyway as LastRecording, and delete LastRecording if it already exists. snprintf(filePath, BROGUE_FILENAME_MAX, "%s%s", LAST_RECORDING_NAME, RECORDING_SUFFIX); +#ifdef __EMSCRIPTEN__ + if (fileExists(filePath) && strcmp(filePath, currentFilePath)) { // on the web the run may be LastRecording already +#else if (fileExists(filePath)) { +#endif remove(filePath); } rename(currentFilePath, filePath); @@ -1292,11 +1311,25 @@ void switchToPlaying() { rogue.playbackOmniscience = false; rogue.recording = true; locationInRecordingBuffer = 0; +#ifdef __EMSCRIPTEN__ + // The game in progress keeps its one name, so the page can open it again. + strcpy(lastGamePath, webGamePath()); + if (!strcmp(currentFilePath, lastGamePath)) { + truncate(lastGamePath, recordingLocation); // play goes on from where the load stopped + } else { + webSetAside(""); + copyFile(currentFilePath, lastGamePath, recordingLocation); + if (DELETE_SAVE_FILE_AFTER_LOADING) { + remove(currentFilePath); + } + } +#else copyFile(currentFilePath, lastGamePath, recordingLocation); #ifndef ENABLE_PLAYBACK_SWITCH if (DELETE_SAVE_FILE_AFTER_LOADING) { remove(currentFilePath); } +#endif #endif strcpy(currentFilePath, lastGamePath); @@ -1517,3 +1550,66 @@ void RNGLog(char *message) { fputs(message, RNGLogFile); #endif } + +#ifdef __EMSCRIPTEN__ +// On the web the game in progress is always one of these two files, which +// the page opens again at the next visit. Games from a seed the player picked +// carry "seeded" in their file names, and post no score. +#define WEB_LAST_GAME LAST_GAME_NAME GAME_SUFFIX +#define WEB_LAST_SEEDED LAST_GAME_NAME " seeded" GAME_SUFFIX + +boolean webSeeded = false; + +const char *webGamePath(void) { + return webSeeded ? WEB_LAST_SEEDED : WEB_LAST_GAME; +} + +// Renames a game left in progress (other than `except`), so it stays loadable but isn't opened again. +void webSetAside(const char *except) { + const char *names[2] = {WEB_LAST_GAME, WEB_LAST_SEEDED}; + char path[BROGUE_FILENAME_MAX]; + for (int i = 0; i < 2; i++) { + if (strcmp(names[i], except) && fileExists(names[i])) { + getAvailableFilePath(path, i ? "Unfinished seeded game" : "Unfinished game", GAME_SUFFIX); + strcat(path, GAME_SUFFIX); + rename(names[i], path); + } + } +} + +// A game in progress that didn't load to the point of play: one that ended +// (the page closed during its last screens) becomes a recording, and one out +// of sync is set aside, so neither is opened at every visit. +void webLoadEnded(const char *path) { + char recording[BROGUE_FILENAME_MAX], defaultPath[BROGUE_FILENAME_MAX]; + if (strcmp(path, WEB_LAST_GAME) && strcmp(path, WEB_LAST_SEEDED)) return; + if (!fileExists(path)) return; + if (rogue.gameHasEnded && !rogue.playbackOOS) { + getDefaultFilePath(defaultPath, true); + getAvailableFilePath(recording, defaultPath, RECORDING_SUFFIX); + strcat(recording, RECORDING_SUFFIX); + rename(path, recording); + } else if (rogue.playbackOOS) { + webSetAside(""); + } +} + +// The run just ended: it becomes LastRecording at once, as if the player +// declined to name it, so a page closed during the screens that follow +// doesn't leave it as the game in progress. Naming it then renames it. +void webEndRun(void) { + char path[BROGUE_FILENAME_MAX]; + if (rogue.playbackMode || !currentFilePath[0] || !fileExists(currentFilePath)) return; + snprintf(path, BROGUE_FILENAME_MAX, "%s%s", LAST_RECORDING_NAME, RECORDING_SUFFIX); + if (fileExists(path)) remove(path); + rename(currentFilePath, path); + strcpy(currentFilePath, path); +} + +// Tells the play page the score of a run of classic Brogue in normal mode +// from a random seed, as soon as the run ends. +void webReportScore(long score) { + if (rogue.playbackMode || webSeeded || gameVariant != VARIANT_BROGUE || rogue.mode != GAME_MODE_NORMAL) return; + EM_ASM({ if (Module.wgGameOver) Module.wgGameOver($0); }, score); +} +#endif diff --git a/src/brogue/Rogue.h b/src/brogue/Rogue.h index 8a82dd2..383b912 100644 --- a/src/brogue/Rogue.h +++ b/src/brogue/Rogue.h @@ -2876,6 +2876,14 @@ extern "C" { void initializeGameVariant(void); int deepestLevelForGameVariant(void); void initializeRogue(uint64_t seed); +#ifdef __EMSCRIPTEN__ + extern boolean webSeeded; + const char *webGamePath(void); + void webSetAside(const char *except); + void webLoadEnded(const char *path); + void webEndRun(void); + void webReportScore(long score); +#endif void gameOver(char *killedBy, boolean useCustomPhrasing); void victory(boolean superVictory); void setPlayerDisplayChar(void); diff --git a/src/brogue/RogueMain.c b/src/brogue/RogueMain.c index 93ce8b6..d29fed0 100644 --- a/src/brogue/RogueMain.c +++ b/src/brogue/RogueMain.c @@ -1058,9 +1058,18 @@ void gameOver(char *killedBy, boolean useCustomPhrasing) { } player.bookkeepingFlags |= MB_IS_DYING; +#ifdef __EMSCRIPTEN__ + // The page hears the score at once: the player may close it during the screens that follow. + if (!rogue.highScoreSaved) { + webReportScore(rogue.gold + 500 * numberOfMatchingPackItems(GEM, 0, 0, false)); + } +#endif rogue.autoPlayingLevel = false; rogue.gameInProgress = false; flushBufferToFile(); +#ifdef __EMSCRIPTEN__ + webEndRun(); +#endif if (rogue.playbackFastForward) { rogue.playbackFastForward = false; @@ -1227,6 +1236,9 @@ void victory(boolean superVictory) { rogue.gameInProgress = false; flushBufferToFile(); +#ifdef __EMSCRIPTEN__ + webEndRun(); +#endif if (rogue.playbackFastForward) { rogue.playbackFastForward = false; @@ -1304,6 +1316,9 @@ void victory(boolean superVictory) { sprintf(buf, "%li", totalValue); printString(buf, mapToWindowX(60), min(ROWS-1, i + 1), &lightBlue, &black, &dbuf); +#ifdef __EMSCRIPTEN__ + webReportScore(totalValue); +#endif funkyFade(&dbuf, &white, 0, 120, COLS/2, ROWS/2, true); displayMoreSign(); diff --git a/src/platform/sdl2-platform.c b/src/platform/sdl2-platform.c index 7181e12..ce028c8 100644 --- a/src/platform/sdl2-platform.c +++ b/src/platform/sdl2-platform.c @@ -7,6 +7,28 @@ #include "platform.h" #include "tiles.h" +#ifdef __EMSCRIPTEN__ +#include + +// The play page sizes the game's canvas: it calls wg_resize with the size in +// device pixels, and the window takes it before the next event is read. +static int pendingWidth = 0, pendingHeight = 0; + +EMSCRIPTEN_KEEPALIVE void wg_resize(int width, int height) { + if (width > 0 && height > 0) { + pendingWidth = width; + pendingHeight = height; + } +} + +static void applyPendingResize(void) { + if (pendingWidth > 0) { + resizeWindow(pendingWidth, pendingHeight); + pendingWidth = pendingHeight = 0; + } +} +#endif + #define PAUSE_BETWEEN_EVENT_POLLING 36L//17 #define MAX_REMAPS 128 @@ -123,8 +145,17 @@ static boolean eventFromKey(rogueEvent *event, SDL_Keycode key) { } +#ifdef __EMSCRIPTEN__ +// SDL_GetModState stays 0 in the browser: the modifiers come from the key events instead. +static SDL_Keymod webMods = 0; +#endif + static boolean _modifierHeld(int mod) { +#ifdef __EMSCRIPTEN__ + SDL_Keymod km = webMods; +#else SDL_Keymod km = SDL_GetModState(); +#endif return mod == 0 && (km & (KMOD_LSHIFT | KMOD_RSHIFT)) || mod == 1 && (km & (KMOD_LCTRL | KMOD_RCTRL)); } @@ -154,8 +185,21 @@ static boolean pollBrogueEvent(rogueEvent *returnEvent, boolean textInput) { boolean ret = false; +#ifdef __EMSCRIPTEN__ + applyPendingResize(); +#endif + // ~ for (int i=0; i < 100 && SDL_PollEvent(&event); i++) { while (SDL_PollEvent(&event)) { +#ifdef __EMSCRIPTEN__ + if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) { + webMods = event.key.keysym.mod; + } + if (event.type == SDL_QUIT || event.type == SDL_WINDOWEVENT) { + // The page owns the canvas: it can't be closed, and its size comes from wg_resize. + continue; + } +#endif if (event.type == SDL_QUIT) { // the player clicked the X button! SDL_Quit(); @@ -166,6 +210,15 @@ static boolean pollBrogueEvent(rogueEvent *returnEvent, boolean textInput) { } else if (event.type == SDL_KEYDOWN) { SDL_Keycode key = event.key.keysym.sym; +#ifdef __EMSCRIPTEN__ + // On the web the page sizes the game and owns full screen: these keys do nothing. + if (key == SDLK_PAGEUP || key == SDLK_PAGEDOWN || key == SDLK_F11 || key == SDLK_F12 + || key == SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT)) { + continue; + } + returnEvent->shiftKey = _modifierHeld(0); + returnEvent->controlKey = _modifierHeld(1); +#else if (key == SDLK_PAGEUP) { resizeWindow(max(windowWidth * 11/10, windowWidth + 1), max(windowHeight * 11/10, windowHeight + 1)); continue; @@ -174,16 +227,23 @@ static boolean pollBrogueEvent(rogueEvent *returnEvent, boolean textInput) { resizeWindow(max(windowWidth * 10/11, 1), max(windowHeight * 10/11, 1)); continue; } else if (key == SDLK_F11 || key == SDLK_F12 - || key == SDLK_RETURN && (SDL_GetModState() & KMOD_ALT)) { + || key == SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT)) { fullScreen = !fullScreen; resizeWindow(-1, -1); // Reset to starting resolution continue; } +#endif if (eventFromKey(returnEvent, key)) { returnEvent->eventType = KEYSTROKE; return true; } +#ifdef __EMSCRIPTEN__ + } else if (event.type == SDL_TEXTINPUT && ((unsigned char)(event.text.text[0]) < ' ' + || (webMods & KMOD_ALT))) { + // Browsers also send Enter and Alt combinations as text; the keys themselves were handled above. + continue; +#endif } else if (event.type == SDL_TEXTINPUT && (unsigned char)(event.text.text[0]) < 0x80) { /* It's difficult/impossible to check what characters are on the @@ -196,12 +256,14 @@ static boolean pollBrogueEvent(rogueEvent *returnEvent, boolean textInput) { if (!textInput) { c = applyRemaps(c); +#ifndef __EMSCRIPTEN__ if (c == '=' || c == '+') { resizeWindow(max(windowWidth * 11/10, windowWidth + 1), max(windowHeight * 11/10, windowHeight + 1)); } else if (c == '-') { fullScreen = false; resizeWindow(max(windowWidth * 10/11, 1), max(windowHeight * 10/11, 1)); } +#endif } returnEvent->eventType = KEYSTROKE; @@ -411,6 +473,10 @@ static void _remap(const char *from, const char *to) { * Take screenshot in current working directory (ScreenshotN.png) */ static boolean _takeScreenshot() { +#ifdef __EMSCRIPTEN__ + // It would land among the player's saves. + return false; +#endif SDL_Surface *screenshot = captureScreen(); if (!screenshot) return false; diff --git a/src/platform/tiles.c b/src/platform/tiles.c index 9631207..8d85408 100644 --- a/src/platform/tiles.c +++ b/src/platform/tiles.c @@ -3,6 +3,9 @@ #include #include "platform.h" #include "tiles.h" +#ifdef __EMSCRIPTEN__ +#include +#endif #define PI 3.14159265358979323846 @@ -738,6 +741,25 @@ void updateScreen() { Creates or resizes the game window with the currently loaded font. */ void resizeWindow(int width, int height) { +#ifdef __EMSCRIPTEN__ + // The play page sizes the canvas in device pixels (wg_resize); full + // screen is the page's. + if (width <= 0 || height <= 0) { + width = EM_ASM_INT({ return Module.wgWidth || 1280; }); + height = EM_ASM_INT({ return Module.wgHeight || 800; }); + } + if (Win == NULL) { + Win = SDL_CreateWindow("Brogue", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, 0); + if (!Win) sdlfatal(__FILE__, __LINE__); + } else { + SDL_SetWindowSize(Win, width, height); + } + fullScreen = false; + SDL_GetWindowSize(Win, &windowWidth, &windowHeight); + refreshScreen(); + updateScreen(); + return; +#endif SDL_DisplayMode mode; if (SDL_GetCurrentDisplayMode(0, &mode) < 0) sdlfatal(__FILE__, __LINE__);