#include #include #include #include #include #include #include #include #include #include #include "util.h" #include "map.h" class sdl_error : public std::logic_error { public: sdl_error() : std::logic_error(SDL_GetError()) { } }; class window_deleter { public: void operator()(SDL_Window* ptr) { SDL_DestroyWindow(ptr); } }; using window_ptr = std::unique_ptr; class renderer_deleter { public: void operator()(SDL_Renderer* ptr) { SDL_DestroyRenderer(ptr); } }; using renderer_ptr = std::unique_ptr; class texture_deleter { public: void operator()(SDL_Texture* ptr) { SDL_DestroyTexture(ptr); } }; using texture_ptr = std::unique_ptr; enum class Tile { Floor, Wall, Dust, Lamp }; enum class LoseState { None, PoppingLamps, PoppingPlayer, Outro }; const int GAME_WIDTH = 640*2; const int GAME_HEIGHT = 480*2; const int TILE_WIDTH = 8*2; const int TILE_HEIGHT = TILE_WIDTH; const int INIT_ZOOM = 10; const int ZOOM_X_FACTOR = 8; const int ZOOM_Y_FACTOR = 6; const int RADIUS = 8; struct Input { bool left = false; bool right = false; bool up = false; bool down = false; }; using coord = std::tuple; struct Kickup { int x; int y; size_t cur; size_t radius; size_t chain; std::set done; std::set front; }; struct MapData { Tile tile = Tile::Floor; bool lit = false; bool wasLit = false; double visibility = 0.0; size_t dustLife = 0; }; class Game { public: Game(std::mt19937& rng) : rng(rng), map( -INIT_ZOOM * ZOOM_X_FACTOR / 2, -INIT_ZOOM * ZOOM_Y_FACTOR / 2, INIT_ZOOM * ZOOM_X_FACTOR, INIT_ZOOM * ZOOM_Y_FACTOR) { } std::mt19937& rng; Map map; std::list kickups; int litSpots = 0; bool dirtyLighting = true; size_t numLamps = 0; size_t numDust = 0; int player_x = 0; int player_y = 0; bool renderPlayer = true; int curZoom = INIT_ZOOM; int maxZoom = INIT_ZOOM; bool zooming = false; //size_t oldZoom; int zoomProgress = 0; }; void render( SDL_Renderer* ren, const Game& game, bool drawDark = true) { texture_ptr canvas( SDL_CreateTexture( ren, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, TILE_WIDTH * game.map.getWidth(), TILE_HEIGHT * game.map.getHeight())); if (!canvas) { throw sdl_error(); } SDL_SetRenderTarget(ren, canvas.get()); SDL_SetRenderDrawColor(ren, rand() % 255, rand() % 255, rand() % 255, 255); SDL_RenderClear(ren); for (int y = game.map.getTop(); y < game.map.getBottom(); y++) { for (int x = game.map.getLeft(); x < game.map.getRight(); x++) { bool draw = true; if ((game.player_x == x && game.player_y == y) && game.renderPlayer) { SDL_SetRenderDrawColor(ren, 255, 255, 0, 255); } else if (!game.map.at(x,y).lit) { if (drawDark) { SDL_SetRenderDrawColor(ren, 40, 40, 40, 255); } else { draw = false; } } else { switch (game.map.at(x,y).tile) { case Tile::Floor: { SDL_SetRenderDrawColor(ren, 210, 210, 210, 255); break; } case Tile::Wall: { SDL_SetRenderDrawColor(ren, 100, 100, 100, 255); break; } case Tile::Dust: { SDL_SetRenderDrawColor(ren, 128, 40, 255, 255); break; } case Tile::Lamp: { SDL_SetRenderDrawColor(ren, 0, 255, 255, 255); break; } } } if (draw) { SDL_Rect rect { game.map.getTrueX(x) * TILE_WIDTH, game.map.getTrueY(y) * TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT}; SDL_RenderFillRect(ren, &rect); int alpha = (1.0 - game.map.at(x,y).visibility) * 255; SDL_SetRenderDrawColor(ren, 40, 40, 40, alpha); SDL_RenderFillRect(ren, &rect); } } } SDL_SetRenderTarget(ren, nullptr); if (!game.zooming) { SDL_RenderCopy(ren, canvas.get(), nullptr, nullptr); } else { // TODO: zooming back in to the player SDL_Rect zoomRect { ((game.maxZoom - game.curZoom) * TILE_WIDTH + game.zoomProgress) * ZOOM_X_FACTOR / 2, ((game.maxZoom - game.curZoom) * TILE_HEIGHT + game.zoomProgress) * ZOOM_Y_FACTOR / 2, (game.curZoom * TILE_WIDTH - game.zoomProgress) * ZOOM_X_FACTOR, (game.curZoom * TILE_HEIGHT - game.zoomProgress) * ZOOM_Y_FACTOR }; SDL_RenderCopy(ren, canvas.get(), &zoomRect, nullptr); } SDL_RenderPresent(ren); } void incrementIfSet(Game& game, int& count, int x, int y, Tile val = Tile::Wall) { if (game.map.inBounds(x, y) && game.map.at(x,y).tile == val) { count++; } } void tick( Game& game, int x1, int y1, int x2, int y2, bool invert = false, bool onlyDark = false) { Map temp(game.map); for (int y = game.map.getTop(); y < game.map.getBottom(); y++) { for (int x = game.map.getLeft(); x < game.map.getRight(); x++) { if (invert == (x >= x1 && x < x2 && y >= y1 && y < y2)) { continue; } if (onlyDark && game.map.at(x,y).lit) { continue; } if (game.map.at(x,y).tile == Tile::Lamp) { continue; } int count = 0; incrementIfSet(game, count, x-1, y-1); incrementIfSet(game, count, x-1, y ); incrementIfSet(game, count, x-1, y+1); incrementIfSet(game, count, x , y-1); incrementIfSet(game, count, x , y ); incrementIfSet(game, count, x , y+1); incrementIfSet(game, count, x+1, y-1); incrementIfSet(game, count, x+1, y ); incrementIfSet(game, count, x+1, y+1); if (count >= 5) { temp.at(x,y).tile = Tile::Wall; } else { temp.at(x,y).tile = Tile::Floor; } } } game.map = std::move(temp); } void tick(Game& game, bool onlyDark = false) { tick( game, game.map.getLeft(), game.map.getTop(), game.map.getRight(), game.map.getBottom(), false, onlyDark); } void movePlayer(Game& game, int x, int y) { if (game.map.inBounds(x, y) && game.map.at(x,y).tile == Tile::Floor) { if (game.map.at(game.player_x, game.player_y).tile == Tile::Floor) { game.map.at(game.player_x, game.player_y).tile = Tile::Dust; game.map.at(game.player_x, game.player_y).dustLife = 1; game.numDust++; } game.player_x = x; game.player_y = y; game.dirtyLighting = true; } } void recalculateLighting(Game& game, fov_settings_type* fov) { game.litSpots = 0; for (MapData& md : game.map.data()) { md.wasLit = md.lit; md.lit = false; md.visibility = 0.0; } fov_settings_set_opacity_test_function( fov, [] (void* data, int x, int y) { Game& game = *static_cast(data); return game.map.inBounds(x,y) && game.map.at(x,y).tile == Tile::Wall; }); fov_settings_set_apply_lighting_function( fov, [] (void* data, int x, int y, int dx, int dy, void*) { Game& game = *static_cast(data); if (game.map.inBounds(x, y)) { if (!game.map.at(x,y).lit) { game.litSpots++; } game.map.at(x,y).lit = true; game.map.at(x,y).visibility = std::max( game.map.at(x,y).visibility, std::pow( std::max( 0.0, 1.0 - std::sqrt(dx * dx + dy * dy) / static_cast(RADIUS)), 1.0/3.0)); } }); for (int y = game.map.getTop(); y < game.map.getBottom(); y++) { for (int x = game.map.getLeft(); x < game.map.getRight(); x++) { if ((game.player_x == x && game.player_y == y && game.renderPlayer) || game.map.at(x,y).tile == Tile::Dust || game.map.at(x,y).tile == Tile::Lamp) { fov_circle(fov, static_cast(&game), nullptr, x, y, RADIUS); game.map.at(x,y).lit = true; game.map.at(x,y).visibility = 1.0; } } } game.dirtyLighting = false; } void processKeys(Game& game, const Input& keystate) { int px = game.player_x; int py = game.player_y; if (keystate.up) { py--; } if (keystate.down) { py++; } if (keystate.left) { px--; } if (keystate.right) { px++; } if (!(game.player_x == px && game.player_y == py)) { movePlayer(game, px, py); } } void kickUpDust(Game& game, int x, int y, size_t chain) { Kickup dk; dk.x = x; dk.y = y; dk.chain = chain; dk.cur = 0; dk.radius = RADIUS + (chain + 1) * (chain + 1); dk.front.emplace(x, y); dk.done.emplace(x, y); game.kickups.push_back(dk); } void popLamp(Game& game, int x, int y, size_t chain) { if (game.map.at(x,y).tile == Tile::Lamp) { game.numLamps--; } game.map.at(x,y).tile = Tile::Dust; game.map.at(x,y).dustLife = 2; game.numDust++; game.dirtyLighting = true; kickUpDust(game, x, y, chain); } void processKickup(Game& game) { for (Kickup& kickup : game.kickups) { kickup.cur++; std::set newFront; for (const coord& xy : kickup.front) { auto processDir = [&] (int x, int y) { coord c {x,y}; if (game.map.inBounds(x,y) && (game.map.at(x,y).tile == Tile::Floor || game.map.at(x,y).tile == Tile::Lamp) && !kickup.done.count(c)) { newFront.insert(c); kickup.done.insert(c); if (game.map.at(x,y).tile == Tile::Floor) { game.map.at(x,y).tile = Tile::Dust; game.map.at(x,y).dustLife = 2; game.numDust++; game.dirtyLighting = true; } else if (game.map.at(x,y).tile == Tile::Lamp) { popLamp(game, x, y, kickup.chain + 1); } } }; processDir(std::get<0>(xy) - 1, std::get<1>(xy) ); processDir(std::get<0>(xy) + 1, std::get<1>(xy) ); processDir(std::get<0>(xy) , std::get<1>(xy) - 1); processDir(std::get<0>(xy) , std::get<1>(xy) + 1); if (std::bernoulli_distribution(0.5)(game.rng)) { processDir(std::get<0>(xy) - 1, std::get<1>(xy) - 1); } if (std::bernoulli_distribution(0.5)(game.rng)) { processDir(std::get<0>(xy) - 1, std::get<1>(xy) + 1); } if (std::bernoulli_distribution(0.5)(game.rng)) { processDir(std::get<0>(xy) + 1, std::get<1>(xy) - 1); } if (std::bernoulli_distribution(0.5)(game.rng)) { processDir(std::get<0>(xy) + 1, std::get<1>(xy) + 1); } } kickup.front.swap(newFront); } erase_if( game.kickups, [] (const Kickup& kickup) { return kickup.cur == kickup.radius; }); } void growMap(Game& game, size_t zoom) { int ol = game.map.getLeft(); int ot = game.map.getTop(); int ow = game.map.getWidth(); int oh = game.map.getHeight(); game.map.resize( -zoom * ZOOM_X_FACTOR / 2, -zoom * ZOOM_Y_FACTOR / 2, zoom * ZOOM_X_FACTOR, zoom * ZOOM_Y_FACTOR); game.maxZoom = zoom; for (int y = game.map.getTop(); y < game.map.getBottom(); y++) { for (int x = game.map.getLeft(); x < game.map.getRight(); x++) { if (!(x >= ol && x < (ol + ow) && y >= ot && y < (ot + oh))) { if (std::bernoulli_distribution(0.5)(game.rng)) { game.map.at(x,y).tile = Tile::Wall; } } } } for (int i = 0; i < 3; i++) { tick(game, ol, ot, ol + ow, ot + oh, true); } } void setZoom(Game& game, size_t zoom) { if (zoom == game.curZoom) { return; } if (zoom > game.maxZoom) { growMap(game, zoom); } // TODO: don't think this works well with rapid zoom changes game.zoomProgress += (zoom - game.curZoom) * TILE_WIDTH; //game.oldZoom = game.curZoom; game.curZoom = zoom; game.zooming = true; } int main(int, char**) { std::random_device randomEngine; std::mt19937 rng(randomEngine()); if (SDL_Init(SDL_INIT_VIDEO) != 0) { throw sdl_error(); } try { window_ptr win( SDL_CreateWindow("Ether", 100, 100, GAME_WIDTH, GAME_HEIGHT, SDL_WINDOW_SHOWN)); if (!win) { throw sdl_error(); } renderer_ptr ren( SDL_CreateRenderer( win.get(), -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)); if (!ren) { throw sdl_error(); } SDL_SetRenderDrawBlendMode(ren.get(), SDL_BLENDMODE_BLEND); Game game(rng); std::unique_ptr fov(new fov_settings_type()); fov_settings_init(fov.get()); for (MapData& md : game.map.data()) { if (std::bernoulli_distribution(0.5)(rng)) { md.tile = Tile::Wall; } } tick(game); tick(game); for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { game.map.at(x,y).tile = Tile::Floor; } } tick(game); bool quit = false; LoseState losing = LoseState::None; Input keystate; SDL_Event e; size_t dustDt = 30; size_t dustAcc = 0; size_t inputDt = 50; size_t inputAcc = 0; size_t losePopLampDt = 800; size_t losePopLampAcc = losePopLampDt; size_t losePopPlayerDt = 3000; size_t losePopPlayerAcc = 0; size_t zoomDt = 62; size_t zoomAcc = 0; size_t lastTime = SDL_GetTicks(); while (!quit) { size_t currentTime = SDL_GetTicks(); size_t frameTime = currentTime - lastTime; lastTime = currentTime; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) { if (losing != LoseState::None) { quit = true; } else { losing = LoseState::PoppingLamps; } } else if (e.type == SDL_KEYDOWN) { switch (e.key.keysym.sym) { case SDLK_ESCAPE: { if (losing != LoseState::None) { quit = true; } else { losing = LoseState::PoppingLamps; } break; } case SDLK_SPACE: { if (losing == LoseState::None) { if (game.map.at(game.player_x, game.player_y).tile == Tile::Floor) { game.map.at(game.player_x, game.player_y).tile = Tile::Lamp; game.numLamps++; game.dirtyLighting = true; kickUpDust(game, game.player_x, game.player_y, 0); for (int i = 0; i < 5; i++) { processKeys(game, keystate); tick( game, game.player_x - (RADIUS - 1), game.player_y - (RADIUS - 1), game.player_x + RADIUS, game.player_y + RADIUS); } } } break; } } } } const Uint8* state = SDL_GetKeyboardState(NULL); keystate.left = state[SDL_SCANCODE_LEFT]; keystate.right = state[SDL_SCANCODE_RIGHT]; keystate.up = state[SDL_SCANCODE_UP]; keystate.down = state[SDL_SCANCODE_DOWN]; dustAcc += frameTime; inputAcc += frameTime; while (dustAcc >= dustDt) { game.numDust = 0; for (MapData& md : game.map.data()) { if (md.tile == Tile::Dust) { md.dustLife--; if (md.dustLife <= 0) { md.tile = Tile::Floor; game.dirtyLighting = true; } else { game.numDust++; } } } processKickup(game); dustAcc -= dustDt; } switch (losing) { case LoseState::None: { while (inputAcc >= inputDt) { processKeys(game, keystate); inputAcc -= inputDt; } break; } case LoseState::PoppingLamps: { if (game.numLamps == 0) { if (game.numDust == 0) { losing = LoseState::PoppingPlayer; } } else { losePopLampAcc += frameTime; while (losePopLampAcc >= losePopLampDt) { std::vector> lamps; for (int y = game.map.getTop(); y < game.map.getBottom(); y++) { for (int x = game.map.getLeft(); x < game.map.getRight(); x++) { if (game.map.at(x,y).tile == Tile::Lamp) { lamps.emplace_back(x, y); } } } std::uniform_int_distribution lampDist(0, lamps.size() - 1); std::tuple popPos = lamps[lampDist(rng)]; popLamp(game, std::get<0>(popPos), std::get<1>(popPos), 1); losePopLampAcc -= losePopLampDt; } } break; } case LoseState::PoppingPlayer: { losePopPlayerAcc += frameTime; if (losePopPlayerAcc >= losePopPlayerDt) { popLamp(game, game.player_x, game.player_y, 10); game.renderPlayer = false; losing = LoseState::Outro; } break; } case LoseState::Outro: { if (game.numDust == 0) { quit = true; } break; } } if (game.dirtyLighting) { recalculateLighting(game, fov.get()); for (int y = game.map.getTop(); y < game.map.getBottom(); y++) { for (int x = game.map.getLeft(); x < game.map.getRight(); x++) { if (!game.map.at(x,y).lit && game.map.at(x,y).wasLit) { if (std::bernoulli_distribution(0.5)(rng)) { game.map.at(x,y).tile = Tile::Wall; } else { game.map.at(x,y).tile = Tile::Floor; } } } } tick(game, true); tick(game, true); tick(game, true); // TODO: better zoom algorithm setZoom(game, game.litSpots / 1500 * 2 + INIT_ZOOM); } zoomAcc += frameTime; while (zoomAcc >= zoomDt) { if (game.zooming) { if (game.zoomProgress > 0) { game.zoomProgress--; } else if (game.zoomProgress < 0) { game.zoomProgress++; } if (game.zoomProgress == 0) { game.zooming = false; } } zoomAcc -= zoomDt; } render(ren.get(), game, true); } } catch (const sdl_error& ex) { std::cout << "SDL error (" << ex.what() << ")" << std::endl; } SDL_Quit(); return 0; }