diff --git a/constants.h b/constants.h index 94f627d..5473996 100644 --- a/constants.h +++ b/constants.h @@ -195,6 +195,11 @@ */ #define SFG_WIN_ANIMATION_DURATION 2500 +/** + Time in ms of the level start stage. +*/ +#define SFG_LEVEL_START_DURATION 2000 + /** Vertical sprite size, in RCL_Units. */ diff --git a/game.h b/game.h index c178a52..3e317d4 100755 --- a/game.h +++ b/game.h @@ -117,6 +117,24 @@ void SFG_playSound(uint8_t soundIndex, uint8_t volume); */ void SFG_enableMusic(uint8_t enable); +#define SFG_SAVE_SIZE 12 + +/** + Optional function for permanently saving game state. Platform that don't have + permanent storage may let this function do nothing. If implemented, the + function should save the passed data into its permanent storage, e.g. a file, + a cookie etc. +*/ +void SFG_save(uint8_t data[SFG_SAVE_SIZE]); + +/** + Optional function for retrieving game data that were saved to permanent + storage. Platforms without permanent storage may let this function do nothing. + If implemented, the function should fill the passed array with data from + permanent storage, e.g. a file, a cookie etc. +*/ +void SFG_load(uint8_t data[SFG_SAVE_SIZE]); + /* ========================================================================= */ /** @@ -251,6 +269,7 @@ typedef struct #define SFG_GAME_STATE_INTRO 4 #define SFG_GAME_STATE_OUTRO 5 #define SFG_GAME_STATE_MAP 6 +#define SFG_GAME_STATE_LEVEL_START 7 #define SFG_MENU_ITEM_CONTINUE 0 #define SFG_MENU_ITEM_MAP 1 @@ -304,6 +323,7 @@ struct uint8_t selectedMenuItem; uint8_t selectedLevel; ///< Level to play selected in the main menu. uint8_t antiSpam; ///< Prevents log message spamming. + uint8_t saved; ///< Helper variable to know if game was saved. uint8_t settings; /**< Dynamic game settings (can be changed at runtime), bit meaning: @@ -355,6 +375,7 @@ struct struct { const SFG_Level *levelPointer; + uint8_t levelNumber; const uint8_t* textures[7]; uint32_t timeStart; @@ -507,6 +528,68 @@ uint8_t SFG_getDamageValue(uint8_t attackType) return value; } +/** + Saves game data to persistent storage. +*/ +void SFG_gameSave() +{ + /* + The save format is binary and platform independent. The save contains game + settings, game progress and a saved position. The format is as follows: + + 4b Last level that has been reached and is accessible to play. + 4b Level number of the saved position. + 8b game settings (SFG_game.settings) + 8b Health at saved position. + 8b Bullets ammo at saved position. + 8b Rockets ammo at saved position. + 8b Plasma ammo at saved position. + 32b Little endian total play time from start, in 10ths of second. + 16b Little endian total enemies killed from start. + */ + + SFG_LOG("Saving."); + + uint8_t data[SFG_SAVE_SIZE]; + + data[0] = SFG_currentLevel.levelNumber | 0; // TODO + data[1] = SFG_game.settings; + data[2] = SFG_player.health; + + for (uint8_t i = 0; i < 3; ++i) + data[3 + i] = SFG_player.ammo[i]; + + uint32_t time = 0; // TODO + + for (uint8_t i = 0; i < 4; ++i) + { + data[6 + i] = time % 256; + time /= 256; + } + + uint16_t kills = 0; // TODO + + data[10] = kills % 256; + data[11] = kills / 256; + + SFG_game.saved = 1; + +// SFG_save(data); +} + +/** + Loads game data from persistent storage. +*/ +void SFG_gameLoad() +{ + SFG_LOG("Loading."); + + uint8_t data[SFG_SAVE_SIZE]; + + +// SFG_load(data); +} + /** Returns ammo type for given weapon. */ @@ -1195,7 +1278,8 @@ void SFG_initPlayer() SFG_player.camera.height = SFG_floorHeightAt( SFG_currentLevel.levelPointer->playerStart[0], - SFG_currentLevel.levelPointer->playerStart[1]); + SFG_currentLevel.levelPointer->playerStart[1]) + + RCL_CAMERA_COLL_HEIGHT_BELOW; SFG_player.camera.direction = SFG_currentLevel.levelPointer->playerStart[2] * @@ -1312,11 +1396,21 @@ uint8_t SFG_itemCollides(uint8_t elementType) elementType == SFG_LEVEL_ELEMENT_LAMP; } -void SFG_setAndInitLevel(const SFG_Level *level) +void SFG_setGameState(uint8_t state) +{ + SFG_game.state = state; + SFG_game.stateChangeTime = SFG_game.frameTime; +} + +void SFG_setAndInitLevel(uint8_t levelNumber) { SFG_LOG("setting and initializing level"); + const SFG_Level *level = &SFG_levels[levelNumber]; + SFG_game.currentRandom = 0; + SFG_game.saved = 0; + SFG_currentLevel.levelNumber = levelNumber; SFG_currentLevel.frameEnd = 0; SFG_currentLevel.monstersDead = 0; SFG_currentLevel.backgroundImage = level->backgroundImage; @@ -1462,12 +1556,8 @@ void SFG_setAndInitLevel(const SFG_Level *level) SFG_game.spriteAnimationFrame = 0; SFG_initPlayer(); -} -void SFG_setGameState(uint8_t state) -{ - SFG_game.state = state; - SFG_game.stateChangeTime = SFG_game.frameTime; + SFG_setGameState(SFG_GAME_STATE_LEVEL_START); } void SFG_init() @@ -1542,8 +1632,7 @@ void SFG_init() #if SFG_START_LEVEL == 0 SFG_setGameState(SFG_GAME_STATE_MENU); #else - SFG_setAndInitLevel(&SFG_levels[SFG_START_LEVEL - 1]); - SFG_setGameState(SFG_GAME_STATE_PLAYING); + SFG_setAndInitLevel(SFG_START_LEVEL - 1); #endif } @@ -2103,13 +2192,7 @@ void SFG_monsterPerformAI(SFG_MonsterRecord *monster) { // explode - uint8_t properties; - - SFG_TileDefinition tile = - SFG_getMapTile(SFG_currentLevel.levelPointer,mX,mY,&properties); - SFG_createExplosion(pX,pY,pZ); - monster->health = 0; } } @@ -2697,6 +2780,103 @@ RCL_Unit SFG_autoaimVertically() return 0; } +/** + Draws text on screen using the bitmap font stored in assets. +*/ +void SFG_drawText( + const char *text, + uint16_t x, + uint16_t y, + uint8_t size, + uint8_t color, + uint16_t maxLength, + uint16_t limitX) +{ + if (size == 0) + size = 1; + + if (limitX == 0) + limitX = 65535; + + if (maxLength == 0) + maxLength = 65535; + + uint16_t pos = 0; + + uint16_t currentX = x; + uint16_t currentY = y; + + while (text[pos] != 0 && pos < maxLength) // for each character + { + uint16_t character = SFG_font[SFG_charToFontIndex(text[pos])]; + + for (uint8_t i = 0; i < SFG_FONT_CHARACTER_SIZE; ++i) // for each line + { + currentY = y; + + for (uint8_t j = 0; j < SFG_FONT_CHARACTER_SIZE; ++j) // for each row + { + if (character & 0x8000) + for (uint8_t k = 0; k < size; ++k) + for (uint8_t l = 0; l < size; ++l) + { + uint16_t drawX = currentX + k; + uint16_t drawY = currentY + l; + + if (drawX < SFG_GAME_RESOLUTION_X && + drawY < SFG_GAME_RESOLUTION_Y) + SFG_setGamePixel(drawX,drawY,color); + } + + currentY += size; + character = character << 1; + } + + currentX += size; + } + + currentX += size; // space + + if (currentX > limitX) + { + currentX = x; + y += (SFG_FONT_CHARACTER_SIZE + 1) * size; + } + + pos++; + } +} + +void SFG_drawLevelStartOverlay() +{ + uint8_t stage = ((SFG_game.frameTime - SFG_game.stateChangeTime) * 4) / + SFG_LEVEL_START_DURATION; + + // fade in: + + for (uint16_t y = 0; y < SFG_GAME_RESOLUTION_Y; ++y) + for (uint16_t x = 0; x < SFG_GAME_RESOLUTION_X; ++x) + { + uint8_t draw = 0; + + switch (stage) + { + case 0: draw = 1; break; + case 1: draw = (x % 2) || (y % 2); break; + case 2: draw = (x % 2) == (y % 2); break; + case 3: draw = (x % 2) && (y % 2); break; + default: break; + } + + if (draw) + SFG_setGamePixel(x,y,0); + } + + if (SFG_game.saved) + SFG_drawText(SFG_TEXT_SAVED,SFG_HUD_MARGIN,SFG_HUD_MARGIN, + SFG_FONT_SIZE_MEDIUM,7,255,0); +} + /** Part of SFG_gameStep() for SFG_GAME_STATE_PLAYING. */ @@ -2939,6 +3119,7 @@ void SFG_gameStepPlaying() } if (!SFG_keyIsDown(SFG_KEY_A) || !shearingOn) // U/D: movement + { if (SFG_keyIsDown(SFG_KEY_UP)) { moveOffset.x += SFG_player.direction.x; @@ -2955,6 +3136,7 @@ void SFG_gameStepPlaying() bobbing = 1; #endif } + } int16_t mouseX, mouseY; @@ -3345,8 +3527,7 @@ void SFG_gameStepMenu() } else { - SFG_setAndInitLevel(&SFG_levels[SFG_game.selectedLevel]); - SFG_setGameState(SFG_GAME_STATE_PLAYING); + SFG_setAndInitLevel(SFG_game.selectedLevel); } break; @@ -3443,8 +3624,7 @@ void SFG_gameStep() int32_t t = SFG_game.frameTime - SFG_game.stateChangeTime; - RCL_Unit h = SFG_floorHeightAt( - SFG_player.squarePosition[0], + RCL_Unit h = SFG_floorHeightAt(SFG_player.squarePosition[0], SFG_player.squarePosition[1]); SFG_player.camera.height = @@ -3468,13 +3648,14 @@ void SFG_gameStep() if (t > SFG_WIN_ANIMATION_DURATION) { - if (SFG_keyIsDown(SFG_KEY_RIGHT)) + if (SFG_keyIsDown(SFG_KEY_RIGHT) || + SFG_keyIsDown(SFG_KEY_LEFT)) { - // TODO: save here - SFG_setGameState(SFG_GAME_STATE_MENU); + SFG_setAndInitLevel(SFG_currentLevel.levelNumber); + + if (SFG_keyIsDown(SFG_KEY_RIGHT)) + SFG_gameSave(); } - else if (SFG_keyIsDown(SFG_KEY_LEFT)) - SFG_setGameState(SFG_GAME_STATE_MENU); } break; @@ -3487,14 +3668,26 @@ void SFG_gameStep() break; case SFG_GAME_STATE_INTRO: - if (SFG_keyJustPressed(SFG_KEY_A)) - { - SFG_setAndInitLevel(&SFG_levels[0]); - SFG_setGameState(SFG_GAME_STATE_PLAYING); - } + if (SFG_keyJustPressed(SFG_KEY_A) || SFG_keyJustPressed(SFG_KEY_B)) + SFG_setAndInitLevel(0); break; + case SFG_GAME_STATE_LEVEL_START: + { + SFG_updateLevel(); + + int16_t x,y; + + SFG_getMouseOffset(&x,&y); // this keeps centering the mouse + + if ((SFG_game.frameTime - SFG_game.stateChangeTime) >= + SFG_LEVEL_START_DURATION) + SFG_setGameState(SFG_GAME_STATE_PLAYING); + + break; + } + default: break; } @@ -3589,73 +3782,6 @@ void SFG_drawMap() } } -/** - Draws text on screen using the bitmap font stored in assets. -*/ -void SFG_drawText( - const char *text, - uint16_t x, - uint16_t y, - uint8_t size, - uint8_t color, - uint16_t maxLength, - uint16_t limitX) -{ - if (size == 0) - size = 1; - - if (limitX == 0) - limitX = 65535; - - if (maxLength == 0) - maxLength = 65535; - - uint16_t pos = 0; - - uint16_t currentX = x; - uint16_t currentY = y; - - while (text[pos] != 0 && pos < maxLength) // for each character - { - uint16_t character = SFG_font[SFG_charToFontIndex(text[pos])]; - - for (uint8_t i = 0; i < SFG_FONT_CHARACTER_SIZE; ++i) // for each line - { - currentY = y; - - for (uint8_t j = 0; j < SFG_FONT_CHARACTER_SIZE; ++j) // for each row - { - if (character & 0x8000) - for (uint8_t k = 0; k < size; ++k) - for (uint8_t l = 0; l < size; ++l) - { - uint16_t drawX = currentX + k; - uint16_t drawY = currentY + l; - - if (drawX < SFG_GAME_RESOLUTION_X && - drawY < SFG_GAME_RESOLUTION_Y) - SFG_setGamePixel(drawX,drawY,color); - } - - currentY += size; - character = character << 1; - } - - currentX += size; - } - - currentX += size; // space - - if (currentX > limitX) - { - currentX = x; - y += (SFG_FONT_CHARACTER_SIZE + 1) * size; - } - - pos++; - } -} - /** Draws fullscreen story text (intro/outro). */ @@ -4287,6 +4413,8 @@ void SFG_draw() if (SFG_game.state == SFG_GAME_STATE_WIN) SFG_drawWinOverlay(); + else if (SFG_game.state == SFG_GAME_STATE_LEVEL_START) + SFG_drawLevelStartOverlay(); } } diff --git a/sounds.h b/sounds.h index 3ae2ae4..346f281 100644 --- a/sounds.h +++ b/sounds.h @@ -102,7 +102,7 @@ uint8_t SFG_getNextMusicSample() case 3: { result = - ((((S >> ((S >> 2) % 32)) + (S >> ((S >> 7) % 32)))) & 0x3f | (S >> 5) + (((((S >> ((S >> 2) % 32)) + (S >> ((S >> 7) % 32)))) & 0x3f) | (S >> 5) | (S >> 11)) & (S & (32768 | 8192) ? 0xf0 : 0x30); break; diff --git a/texts.h b/texts.h index 974f77c..e09c88c 100644 --- a/texts.h +++ b/texts.h @@ -27,6 +27,7 @@ SFG_PROGRAM_MEMORY char *SFG_menuItemTexts[] = #define SFG_TEXT_KILLS "kills" #define SFG_TEXT_SAVE_PROMPT "save? L no yes R" +#define SFG_TEXT_SAVED "saved" SFG_PROGRAM_MEMORY char *SFG_introText = "Near future, capitalist hell, Macrochip corp has enslaved man via "