/* * Buttons.c * Brogue * * Created by Brian Walker on 1/14/12. * Copyright 2012. All rights reserved. * * This file is part of Brogue. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "Rogue.h" #include "IncludeGlobals.h" #include #include #define MENU_FLAME_PRECISION_FACTOR 10 #define MENU_FLAME_RISE_SPEED 50 #define MENU_FLAME_SPREAD_SPEED 20 #define MENU_FLAME_COLOR_DRIFT_SPEED 500 #define MENU_FLAME_FADE_SPEED 20 #define MENU_FLAME_UPDATE_DELAY 50 #define MENU_FLAME_ROW_PADDING 2 #define MENU_TITLE_OFFSET_X (-4) #define MENU_TITLE_OFFSET_Y (-1) #define MENU_FLAME_COLOR_SOURCE_COUNT 1136 #define MENU_FLAME_DENOMINATOR (100 + MENU_FLAME_RISE_SPEED + MENU_FLAME_SPREAD_SPEED) void drawMenuFlames(signed short flames[COLS][(ROWS + MENU_FLAME_ROW_PADDING)][3], unsigned char mask[COLS][ROWS]) { short i, j, versionStringLength; color tempColor = {0}; const color *maskColor = &black; char dchar; versionStringLength = strLenWithoutEscapes(BROGUE_VERSION_STRING); for (j=0; j= COLS - versionStringLength) { dchar = BROGUE_VERSION_STRING[i - (COLS - versionStringLength)]; } else { dchar = ' '; } if (mask[i][j] == 100) { plotCharWithColor(dchar, i, j, &veryDarkGray, maskColor); } else { tempColor = black; tempColor.red = flames[i][j][0] / MENU_FLAME_PRECISION_FACTOR; tempColor.green = flames[i][j][1] / MENU_FLAME_PRECISION_FACTOR; tempColor.blue = flames[i][j][2] / MENU_FLAME_PRECISION_FACTOR; if (mask[i][j] > 0) { applyColorAverage(&tempColor, maskColor, mask[i][j]); } plotCharWithColor(dchar, i, j, &veryDarkGray, &tempColor); } } } } void updateMenuFlames(const color *colors[COLS][(ROWS + MENU_FLAME_ROW_PADDING)], signed short colorSources[MENU_FLAME_COLOR_SOURCE_COUNT][4], signed short flames[COLS][(ROWS + MENU_FLAME_ROW_PADDING)][3]) { short i, j, k, l, x, y; signed short tempFlames[COLS][3]; short colorSourceNumber, rand; colorSourceNumber = 0; for (j=0; j<(ROWS + MENU_FLAME_ROW_PADDING); j++) { // Make a temp copy of the current row. for (i=0; irand * colorSources[colorSourceNumber][0] / 1000; flames[i][j][0] += (colors[i][j]->red + (colors[i][j]->redRand * colorSources[colorSourceNumber][1] / 1000) + rand) * MENU_FLAME_PRECISION_FACTOR; flames[i][j][1] += (colors[i][j]->green + (colors[i][j]->greenRand * colorSources[colorSourceNumber][2] / 1000) + rand) * MENU_FLAME_PRECISION_FACTOR; flames[i][j][2] += (colors[i][j]->blue + (colors[i][j]->blueRand * colorSources[colorSourceNumber][3] / 1000) + rand) * MENU_FLAME_PRECISION_FACTOR; colorSourceNumber++; } } } } // Takes a grid of values, each of which is 0 or 100, and fills in some middle values in the interstices. void antiAlias(unsigned char mask[COLS][ROWS]) { short i, j, x, y, dir, nbCount; const short intensity[5] = {0, 0, 35, 50, 60}; for (i=0; i= 0; i--) { y -= 2; buttons[i].x = x; buttons[i].y = y; buttons[i].buttonColor = titleButtonColor; buttons[i].flags |= B_WIDE_CLICK_AREA; } blackOutScreen(); clearDisplayBuffer(shadowBuf); initializeButtonState(&state, buttons, b, x, y, 20, b*2-1); rectangularShading(x, y, 20, b*2-1, &black, INTERFACE_OPACITY, shadowBuf); drawButtonsInState(&state); initializeMenuFlames(true, colors, colorStorage, colorSources, flames, mask); rogue.creaturesWillFlashThisTurn = false; // total unconscionable hack do { if (isApplicationActive()) { // Revert the display. overlayDisplayBuffer(state.rbuf, NULL); if (!controlKeyWasDown && controlKeyIsDown()) { strcpy(state.buttons[0].text, customNewGameText); drawButtonsInState(&state); buttonCommands[0] = NG_NEW_GAME_WITH_SEED; controlKeyWasDown = true; } else if (controlKeyWasDown && !controlKeyIsDown()) { strcpy(state.buttons[0].text, newGameText); drawButtonsInState(&state); buttonCommands[0] = NG_NEW_GAME; controlKeyWasDown = false; } // Update the display. updateMenuFlames(colors, colorSources, flames); drawMenuFlames(flames, mask); overlayDisplayBuffer(shadowBuf, NULL); overlayDisplayBuffer(state.dbuf, NULL); // Pause briefly. if (pauseBrogue(MENU_FLAME_UPDATE_DELAY)) { // There was input during the pause! Get the input. nextBrogueEvent(&theEvent, true, false, true); // Process the input. button = processButtonInput(&state, NULL, &theEvent); } } else { pauseBrogue(64); } } while (button == -1 && rogue.nextGame == NG_NOTHING); drawMenuFlames(flames, mask); if (button != -1) { if (button == 0 && controlKeyIsDown()) { // Should fix an issue with Linux/Windows ports that require moving the mouse after // pressing control to get the button to change. rogue.nextGame = NG_NEW_GAME_WITH_SEED; } else { rogue.nextGame = buttonCommands[button]; } } } // Closes Brogue without any further prompts, animations, or user interaction. void quitImmediately() { // If we are recording a game, save it. if (rogue.recording) { flushBufferToFile(); if (rogue.gameInProgress && !rogue.quit && !rogue.gameHasEnded) { // Game isn't over yet, create a savegame. saveGameNoPrompt(); } else { // Save it as a recording. char path[BROGUE_FILENAME_MAX]; saveRecordingNoPrompt(path); } } exit(0); } void dialogAlert(char *message) { cellDisplayBuffer rbuf[COLS][ROWS]; brogueButton OKButton; initializeButton(&OKButton); strcpy(OKButton.text, " OK "); OKButton.hotkey[0] = RETURN_KEY; OKButton.hotkey[1] = ACKNOWLEDGE_KEY; printTextBox(message, COLS/3, ROWS/3, COLS/3, &white, &interfaceBoxColor, rbuf, &OKButton, 1); overlayDisplayBuffer(rbuf, NULL); } boolean stringsExactlyMatch(const char *string1, const char *string2) { short i; for (i=0; string1[i] && string2[i]; i++) { if (string1[i] != string2[i]) { return false; } } return string1[i] == string2[i]; } // Used to compare the dates of two fileEntry variables // Returns (int): // < 0 if 'b' date is lesser than 'a' date // = 0 if 'b' date is equal to 'a' date, // > 0 if 'b' date is greater than 'a' date int fileEntryCompareDates(const void *a, const void *b) { fileEntry *f1 = (fileEntry *)a; fileEntry *f2 = (fileEntry *)b; time_t t1, t2; double diff; t1 = mktime(&f1->date); t2 = mktime(&f2->date); diff = difftime(t2, t1); //char date_f1[11]; //char date_f2[11]; //strftime(date_f1, sizeof(date_f1), DATE_FORMAT, &f1->date); //strftime(date_f2, sizeof(date_f2), DATE_FORMAT, &f2->date); //printf("\nf1: %s\t%s",date_f1,f1->path); //printf("\nf2: %s\t%s",date_f2,f2->path); //printf("\ndiff: %f\n", diff); return (int)diff; } #define FILES_ON_PAGE_MAX (min(26, ROWS - 7)) // Two rows (top and bottom) for flames, two rows for border, one for prompt, one for heading. #define MAX_FILENAME_DISPLAY_LENGTH 53 boolean dialogChooseFile(char *path, const char *suffix, const char *prompt) { short i, j, count, x, y, width, height, suffixLength, pathLength, maxPathLength, currentPageStart; brogueButton buttons[FILES_ON_PAGE_MAX + 2]; fileEntry *files; boolean retval = false, again; cellDisplayBuffer dbuf[COLS][ROWS], rbuf[COLS][ROWS]; color *dialogColor = &interfaceBoxColor; char *membuf; char fileDate [11]; suffixLength = strlen(suffix); files = listFiles(&count, &membuf); copyDisplayBuffer(rbuf, displayBuffer); maxPathLength = strLenWithoutEscapes(prompt); // First, we want to filter the list by stripping out any filenames that do not end with suffix. // i is the entry we're testing, and j is the entry that we move it to if it qualifies. for (i=0, j=0; i j) { files[j] = files[i]; //strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[j].date); //printf("\nMatching file: %s\twith date: %s", files[j].path, fileDate); } j++; // Keep track of the longest length. if (min(pathLength, MAX_FILENAME_DISPLAY_LENGTH) + 10 > maxPathLength) { maxPathLength = min(pathLength, MAX_FILENAME_DISPLAY_LENGTH) + 10; } } } count = j; // Once we have all relevant files, we sort them by date descending qsort(files, count, sizeof(fileEntry), &fileEntryCompareDates); currentPageStart = 0; do { // Repeat to permit scrolling. again = false; for (i=0; i MAX_FILENAME_DISPLAY_LENGTH) { strcpy(&(buttons[i].text[MAX_FILENAME_DISPLAY_LENGTH - 3]), "..."); } //strftime(fileDate, sizeof(fileDate), DATE_FORMAT, &files[currentPageStart+i].date); //printf("\nFound file: %s, with date: %s", files[currentPageStart+i].path, fileDate); } x = (COLS - maxPathLength) / 2; width = maxPathLength; height = min(count - currentPageStart, FILES_ON_PAGE_MAX) + 2; y = max(4, (ROWS - height) / 2); for (i=0; i FILES_ON_PAGE_MAX) { // Create up and down arrows. initializeButton(&(buttons[i])); strcpy(buttons[i].text, " * "); buttons[i].symbol[0] = G_UP_ARROW; if (currentPageStart <= 0) { buttons[i].flags &= ~(B_ENABLED | B_DRAW); } else { buttons[i].hotkey[0] = UP_ARROW; buttons[i].hotkey[1] = NUMPAD_8; buttons[i].hotkey[2] = PAGE_UP_KEY; } buttons[i].x = x + (width - 11)/2; buttons[i].y = y; i++; initializeButton(&(buttons[i])); strcpy(buttons[i].text, " * "); buttons[i].symbol[0] = G_DOWN_ARROW; if (currentPageStart + FILES_ON_PAGE_MAX >= count) { buttons[i].flags &= ~(B_ENABLED | B_DRAW); } else { buttons[i].hotkey[0] = DOWN_ARROW; buttons[i].hotkey[1] = NUMPAD_2; buttons[i].hotkey[2] = PAGE_DOWN_KEY; } buttons[i].x = x + (width - 11)/2; buttons[i].y = y + i; } if (count) { clearDisplayBuffer(dbuf); printString(prompt, x, y - 1, &itemMessageColor, dialogColor, dbuf); rectangularShading(x - 1, y - 1, width + 1, height + 1, dialogColor, INTERFACE_OPACITY, dbuf); overlayDisplayBuffer(dbuf, NULL); // for (j=0; j FILES_ON_PAGE_MAX ? 2 : 0), x, y, width, height, NULL); // for (j=0; j= 0) { retval = true; strcpy(path, files[currentPageStart+i].path); } else { // i is -1 retval = false; } } else if (i == min(count - currentPageStart, FILES_ON_PAGE_MAX)) { // Up arrow again = true; currentPageStart -= FILES_ON_PAGE_MAX; } else if (i == min(count - currentPageStart, FILES_ON_PAGE_MAX) + 1) { // Down arrow again = true; currentPageStart += FILES_ON_PAGE_MAX; } } } while (again); free(files); free(membuf); if (count == 0) { dialogAlert("No applicable files found."); return false; } else { return retval; } } // This is the basic program loop. // When the program launches, or when a game ends, you end up here. // If the player has already said what he wants to do next // (by storing it in rogue.nextGame -- possibilities listed in enum NGCommands), // we'll do it. The path (rogue.nextGamePath) is essentially a parameter for this command, and // tells NG_VIEW_RECORDING and NG_OPEN_GAME which file to open. If there is a command but no // accompanying path, and it's a command that should take a path, then pop up a dialog to have // the player specify a path. If there is no command (i.e. if rogue.nextGame contains NG_NOTHING), // then we'll display the title screen so the player can choose. void mainBrogueJunction() { rogueEvent theEvent; char path[BROGUE_FILENAME_MAX], buf[100], seedDefault[100]; char *maxSeed = "4294967295"; // 2^32 - 1 short i, j, k; boolean seedTooBig; // clear screen and display buffer for (i=0; i buf[i]) { break; // we're good } else if (maxSeed[i] < buf[i]) { seedTooBig = true; break; } } } sscanf(seedTooBig ? maxSeed : buf, "%lu", &rogue.nextGameSeed); } else { rogue.nextGame = NG_NOTHING; break; // Don't start a new game after all. } } } else { rogue.nextGameSeed = 0; // Seed based on clock. } rogue.nextGame = NG_NOTHING; initializeRogue(rogue.nextGameSeed); startLevel(rogue.depthLevel, 1); // descending into level 1 mainInputLoop(); if(serverMode) { rogue.nextGame = NG_QUIT; } freeEverything(); break; case NG_OPEN_GAME: rogue.nextGame = NG_NOTHING; path[0] = '\0'; if (rogue.nextGamePath[0]) { strcpy(path, rogue.nextGamePath); rogue.nextGamePath[0] = '\0'; } else { dialogChooseFile(path, GAME_SUFFIX, "Open saved game:"); //chooseFile(path, "Open saved game: ", "Saved game", GAME_SUFFIX); } if (openFile(path)) { if (loadSavedGame()) { mainInputLoop(); } freeEverything(); } else { //dialogAlert("File not found."); } rogue.playbackMode = false; rogue.playbackOOS = false; if(serverMode) { rogue.nextGame = NG_QUIT; } break; case NG_VIEW_RECORDING: rogue.nextGame = NG_NOTHING; path[0] = '\0'; if (rogue.nextGamePath[0]) { strcpy(path, rogue.nextGamePath); rogue.nextGamePath[0] = '\0'; } else { dialogChooseFile(path, RECORDING_SUFFIX, "View recording:"); //chooseFile(path, "View recording: ", "Recording", RECORDING_SUFFIX); } if (openFile(path)) { randomNumbersGenerated = 0; rogue.playbackMode = true; initializeRogue(0); // Seed argument is ignored because we're in playback. if (!rogue.gameHasEnded) { startLevel(rogue.depthLevel, 1); rogue.playbackPaused = true; displayAnnotation(); // in case there's an annotation for turn 0 } while(!rogue.gameHasEnded && rogue.playbackMode) { if (rogue.playbackPaused) { rogue.playbackPaused = false; pausePlayback(); } #ifdef ENABLE_PLAYBACK_SWITCH // We are coming from the end of a recording the user has taken over. // No more event checks, that has already been handled if (rogue.gameHasEnded) { break; } #endif rogue.RNG = RNG_COSMETIC; // dancing terrain colors can't influence recordings rogue.playbackBetweenTurns = true; nextBrogueEvent(&theEvent, false, true, false); rogue.RNG = RNG_SUBSTANTIVE; executeEvent(&theEvent); } freeEverything(); } else { // announce file not found } rogue.playbackMode = false; rogue.playbackOOS = false; if(serverMode) { rogue.nextGame = NG_QUIT; } break; case NG_HIGH_SCORES: rogue.nextGame = NG_NOTHING; printHighScores(false); break; case NG_QUIT: // No need to do anything. break; default: break; } } while (rogue.nextGame != NG_QUIT); }