#include "game.h" #include #include #include #include "util.h" #include "renderer.h" #include "consts.h" Game::Game(std::mt19937& rng, Muxer& muxer) : rng(rng), muxer(muxer) { losePopLampTimer.accumulate(losePopLampTimer.getDt()); loadMap(); for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { map.at(x,y).tile = Tile::Floor; } } tick(); } inline bool isTileSetOrNotLit(const Map& map, int x, int y) { return (map.inBounds(x, y) && (map.at(x,y).tile == Tile::Wall || !map.at(x,y).lit)); } inline bool isTileSet(const Map& map, int x, int y, Tile val = Tile::Wall) { return (map.inBounds(x, y) && map.at(x,y).tile == val); } inline void incrementIfSet(const Game& game, int& count, int x, int y, Tile val = Tile::Wall) { if (isTileSet(game.map, x, y, val)) { count++; } } inline int getZoomLevel(const Game& game) { return game.curZoom - INIT_ZOOM; } void Game::tick( int x1, int y1, int x2, int y2, bool invert, bool onlyDark) { std::vector temp(map.data()); for (int y = map.getTop(); y < map.getBottom(); y++) { for (int x = map.getLeft(); x < map.getRight(); x++) { if (invert == (x >= x1 && x < x2 && y >= y1 && y < y2)) { continue; } if (onlyDark && map.at(x,y).lit) { continue; } if (map.at(x,y).tile == Tile::Lamp) { continue; } int count = 0; incrementIfSet(*this, count, x-1, y-1); incrementIfSet(*this, count, x-1, y ); incrementIfSet(*this, count, x-1, y+1); incrementIfSet(*this, count, x , y-1); incrementIfSet(*this, count, x , y ); incrementIfSet(*this, count, x , y+1); incrementIfSet(*this, count, x+1, y-1); incrementIfSet(*this, count, x+1, y ); incrementIfSet(*this, count, x+1, y+1); int tempIndex = map.getTrueX(x) + map.getTrueY(y) * map.getWidth(); if (count >= 5) { temp[tempIndex].tile = Tile::Wall; } else { temp[tempIndex].tile = Tile::Floor; } if (temp[tempIndex].tile != map.at(x,y).tile) { temp[tempIndex].dirtyRender = true; dirtyRender = true; } } } map.data() = std::move(temp); } void Game::tick(bool onlyDark) { tick( map.getLeft(), map.getTop(), map.getRight(), map.getBottom(), false, onlyDark); } bool Game::movePlayer(int x, int y) { if (/*x >= curBoundX && y >= curBoundY && x < curBoundX + curZoom * ZOOM_X_FACTOR && y < curBoundY + curZoom * ZOOM_Y_FACTOR &&*/ map.at(x,y).tile == Tile::Floor) { if (map.at(player_x, player_y).tile == Tile::Floor) { map.at(player_x, player_y).tile = Tile::Dust; map.at(player_x, player_y).dustLife = 1; numDust++; } player_oldx = player_x; player_oldy = player_y; player_x = x; player_y = y; muxer.setPlayerLoc(x, y); moving = true; moveProgress.start(66); dirtyLighting = true; int chunkX, chunkY, old_chunkX, old_chunkY; toChunkPos(player_x, player_y, chunkX, chunkY); toChunkPos(player_oldx, player_oldy, old_chunkX, old_chunkY); if ((chunkX != old_chunkX) || (chunkY != old_chunkY)) { loadMap(); } return true; } else { if (!alreadyBumped) { muxer.playSoundAtPosition("bump", x, y); alreadyBumped = true; bumpCooldown.reset(); } return false; } } void Game::recalculateLighting() { litSpots = 0; dirtyRender = true; for (MapData& md : map.data()) { md.wasLit = md.lit; md.lit = false; md.litTiles.clear(); if (md.tile == Tile::Wall) { md.dirtyRender = true; } } fov_settings_type fov; fov_settings_init(&fov); 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* source) { Game& game = *static_cast(data); if (game.map.inBounds(x, y)) { MapData& sourceData = *static_cast(source); double lightRadius = static_cast(sourceData.lightRadius); if (!game.map.at(x,y).lit) { game.litSpots++; } game.map.at(x,y).lit = true; sourceData.litTiles.emplace(x,y); } }); for (int y = map.getTop(); y < map.getBottom(); y++) { for (int x = map.getLeft(); x < map.getRight(); x++) { Source ls = Source::None; int lightRadius; if (renderPlayer && player_x == x && player_y == y) { ls = Source::Player; lightRadius = RADIUS; } else if (map.at(x,y).tile == Tile::Dust) { ls = Source::Dust; lightRadius = 2; } else if (map.at(x,y).tile == Tile::Lamp) { ls = Source::Lamp; lightRadius = RADIUS; } map.at(x,y).lightType = ls; if (ls != Source::None) { map.at(x,y).lightRadius = lightRadius; map.at(x,y).litTiles.emplace(x,y); fov_circle( &fov, static_cast(this), static_cast(&map.at(x,y)), x, y, lightRadius); map.at(x,y).lit = true; } } } dirtyLighting = false; } void Game::recalculateRender() { for (int y = map.getTop(); y < map.getBottom(); y++) { for (int x = map.getLeft(); x < map.getRight(); x++) { if (map.at(x,y).dirtyRender) { map.at(x,y).dirtyRender = false; if (map.at(x,y).tile == Tile::Floor) { if (std::bernoulli_distribution(0.05)(rng)) { static const std::vector furnishings { TilesetIndex(20, 16), TilesetIndex(21, 2), TilesetIndex(22, 2), TilesetIndex(21, 3), TilesetIndex(22, 3)}; map.at(x,y).renderId = furnishings.at(std::uniform_int_distribution(0, furnishings.size()-1)(rng)); } else { map.at(x,y).renderId = -1; } } else if (map.at(x,y).tile == Tile::Wall) { static bool initWalls = false; static std::vector wallRenders(256, TilesetIndex(0, 0)); if (!initWalls) { initWalls = true; // Start in top left and go clockwise. wallRenders[0b00011100] = TilesetIndex(16, 14); wallRenders[0b00111100] = TilesetIndex(16, 14); wallRenders[0b00011110] = TilesetIndex(16, 14); wallRenders[0b00111110] = TilesetIndex(16, 14); wallRenders[0b00011111] = TilesetIndex(17, 14); wallRenders[0b10011111] = TilesetIndex(17, 14); wallRenders[0b00111111] = TilesetIndex(17, 14); wallRenders[0b10111111] = TilesetIndex(17, 14); wallRenders[0b00000111] = TilesetIndex(18, 14); wallRenders[0b00001111] = TilesetIndex(18, 14); wallRenders[0b10000111] = TilesetIndex(18, 14); wallRenders[0b10001111] = TilesetIndex(18, 14); wallRenders[0b01111100] = TilesetIndex(16, 15); wallRenders[0b11111100] = TilesetIndex(16, 15); wallRenders[0b01111110] = TilesetIndex(16, 15); wallRenders[0b11111110] = TilesetIndex(16, 15); wallRenders[0b11000111] = TilesetIndex(18, 15); wallRenders[0b11001111] = TilesetIndex(18, 15); wallRenders[0b11100111] = TilesetIndex(18, 15); wallRenders[0b11101111] = TilesetIndex(18, 15); wallRenders[0b01110000] = TilesetIndex(16, 16); wallRenders[0b01111000] = TilesetIndex(16, 16); wallRenders[0b11110000] = TilesetIndex(16, 16); wallRenders[0b11111000] = TilesetIndex(16, 16); wallRenders[0b11110001] = TilesetIndex(17, 16); wallRenders[0b11110011] = TilesetIndex(17, 16); wallRenders[0b11111001] = TilesetIndex(17, 16); wallRenders[0b11111011] = TilesetIndex(17, 16); wallRenders[0b11000001] = TilesetIndex(18, 16); wallRenders[0b11000011] = TilesetIndex(18, 16); wallRenders[0b11100001] = TilesetIndex(18, 16); wallRenders[0b11100011] = TilesetIndex(18, 16); wallRenders[0b11110111] = TilesetIndex(21, 14); wallRenders[0b11111101] = TilesetIndex(22, 14); wallRenders[0b11011111] = TilesetIndex(21, 15); wallRenders[0b01111111] = TilesetIndex(22, 15); } int renderDesc = 0; if (isTileSetOrNotLit(map, x-1, y-1)) renderDesc |= (1 << 7); if (isTileSetOrNotLit(map, x , y-1)) renderDesc |= (1 << 6); if (isTileSetOrNotLit(map, x+1, y-1)) renderDesc |= (1 << 5); if (isTileSetOrNotLit(map, x+1, y )) renderDesc |= (1 << 4); if (isTileSetOrNotLit(map, x+1, y+1)) renderDesc |= (1 << 3); if (isTileSetOrNotLit(map, x , y+1)) renderDesc |= (1 << 2); if (isTileSetOrNotLit(map, x-1, y+1)) renderDesc |= (1 << 1); if (isTileSetOrNotLit(map, x-1, y )) renderDesc |= (1 << 0); map.at(x,y).renderId = wallRenders.at(renderDesc); if (wallRenders.at(renderDesc) == 0 && renderDesc != 255) { std::cout << renderDesc << std::endl; } } } } } dirtyRender = false; } bool Game::processKeys(const Input& keystate) { int px = player_x; int py = player_y; Direction dir = Direction::up; if (keystate.up) { py--; } if (keystate.down) { py++; dir = Direction::down; } if (keystate.left) { px--; dir = Direction::left; } if (keystate.right) { px++; dir = Direction::right; } if (!(player_x == px && player_y == py)) { playerAnim.setAnimation("walk"); playerAnim.setDirection(dir); bool succeeds = movePlayer(px, py); if (!succeeds && px != player_x) { succeeds = movePlayer(px, player_y); } if (!succeeds && py != player_y) { succeeds = movePlayer(player_x, py); } return succeeds; } else { return false; } } void Game::kickUpDust(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); kickups.push_back(dk); } void Game::popLamp(int x, int y, size_t chain) { muxer.playSoundAtPosition("pop", x, y); if (map.at(x,y).tile == Tile::Lamp) { numLamps--; } map.at(x,y).tile = Tile::Dust; map.at(x,y).dustLife = 2; numDust++; dirtyLighting = true; kickUpDust(x, y, chain); } void Game::processKickup() { for (Kickup& kickup : kickups) { kickup.cur++; std::set newFront; for (const coord& xy : kickup.front) { auto processDir = [&] (int x, int y) { coord c {x,y}; if (map.inBounds(x,y) && (map.at(x,y).tile == Tile::Floor || map.at(x,y).tile == Tile::Lamp) && !kickup.done.count(c)) { newFront.insert(c); kickup.done.insert(c); if (map.at(x,y).tile == Tile::Floor) { map.at(x,y).tile = Tile::Dust; map.at(x,y).dustLife = 2; numDust++; dirtyLighting = true; } else if (map.at(x,y).tile == Tile::Lamp) { popLamp(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)(rng)) { processDir(std::get<0>(xy) - 1, std::get<1>(xy) - 1); } if (std::bernoulli_distribution(0.5)(rng)) { processDir(std::get<0>(xy) - 1, std::get<1>(xy) + 1); } if (std::bernoulli_distribution(0.5)(rng)) { processDir(std::get<0>(xy) + 1, std::get<1>(xy) - 1); } if (std::bernoulli_distribution(0.5)(rng)) { processDir(std::get<0>(xy) + 1, std::get<1>(xy) + 1); } } kickup.front.swap(newFront); } erase_if( kickups, [] (const Kickup& kickup) { return kickup.cur == kickup.radius; }); } void Game::loadMap() { int newChunksHoriz = std::ceil(static_cast(curZoom) * ZOOM_X_FACTOR / CHUNK_WIDTH) + 4; int newChunksVert = std::ceil(static_cast(curZoom) * ZOOM_Y_FACTOR / CHUNK_HEIGHT) + 4; int curPlayerChunkX, curPlayerChunkY; toChunkPos(player_x, player_y, curPlayerChunkX, curPlayerChunkY); map.load( curPlayerChunkX - newChunksHoriz / 2, curPlayerChunkY - newChunksVert / 2, newChunksHoriz, newChunksVert, rng); tick(); tick(); tick(); dirtyLighting = true; dirtyRender = true; } void Game::setZoom(size_t zoom) { if (zoom == curZoom) { return; } zoomProgress = 0; zoomLength = std::abs(static_cast(zoom - curZoom)) * TILE_WIDTH; curZoom = zoom; zooming = true; loadMap(); int zoomLevel = getZoomLevel(*this); if (zoomLevel == 0) { muxer.setMusicLevel(0); } else if (zoomLevel < 3) { muxer.setMusicLevel(1); } else if (zoomLevel < 5) { muxer.setMusicLevel(2); } else if (zoomLevel < 7) { muxer.setMusicLevel(3); } else { muxer.setMusicLevel(4); } } void Game::performDash() { if (map.at(player_x, player_y).tile == Tile::Floor) { std::vector freeSpaces; auto addIfFree = [&] (int x, int y) { if (map.inBounds(x,y) && map.at(x,y).tile == Tile::Floor) { freeSpaces.emplace_back(x, y); } }; addIfFree(player_x - 1, player_y - 1); addIfFree(player_x , player_y - 1); addIfFree(player_x + 1, player_y - 1); addIfFree(player_x - 1, player_y ); addIfFree(player_x + 1, player_y ); addIfFree(player_x - 1, player_y + 1); addIfFree(player_x , player_y + 1); addIfFree(player_x + 1, player_y + 1); if (!freeSpaces.empty()) { map.at(player_x, player_y).tile = Tile::Lamp; numLamps++; dirtyLighting = true; kickUpDust(player_x, player_y, 0); muxer.playSoundAtPosition("drop", player_x, player_y); if (firstInput) { for (int i = 0; i < 5; i++) { if (!processKeys(lastInput)) { std::uniform_int_distribution freeDist(0, freeSpaces.size() - 1); int freeIndex = freeDist(rng); coord& moveTo = freeSpaces[freeIndex]; movePlayer(std::get<0>(moveTo), std::get<1>(moveTo)); } tick( player_x - (RADIUS - 1), player_y - (RADIUS - 1), player_x + RADIUS, player_y + RADIUS); } } else { std::uniform_int_distribution freeDist(0, freeSpaces.size() - 1); int freeIndex = freeDist(rng); coord& moveTo = freeSpaces[freeIndex]; movePlayer(std::get<0>(moveTo), std::get<1>(moveTo)); } //muxer.playSoundAtPosition("dash", player_x, player_y); } } } void Game::update(size_t frameTime) { SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) { if (losing != LoseState::None) { quit = true; } else { losing = LoseState::PoppingLamps; muxer.stopMusic(); } } else if (e.type == SDL_KEYDOWN) { switch (e.key.keysym.sym) { case SDLK_ESCAPE: { if (losing != LoseState::None) { quit = true; } else { losing = LoseState::PoppingLamps; muxer.stopMusic(); } break; } case SDLK_SPACE: { if (losing == LoseState::None) { if (moving) { queueDash = true; } else { performDash(); } } 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]; bumpCooldown.accumulate(frameTime); if (alreadyBumped && keystate != lastInput && bumpCooldown.step()) { alreadyBumped = false; } if (queueDash && !moving) { queueDash = false; performDash(); } if (keystate.left || keystate.right || keystate.up || keystate.down) { firstInput = true; lastInput = keystate; } else if (losing == LoseState::None) { playerAnim.setAnimation("still"); } dustTimer.accumulate(frameTime); inputTimer.accumulate(frameTime); while (dustTimer.step()) { numDust = 0; for (MapData& md : map.data()) { if (md.tile == Tile::Dust) { md.dustLife--; if (md.dustLife <= 0) { md.tile = Tile::Floor; dirtyLighting = true; } else { numDust++; } } } processKickup(); } switch (losing) { case LoseState::None: { if (moving) { moveProgress.tick(frameTime); if (moveProgress.isComplete()) { moving = false; } } while (inputTimer.step()) { if (!moving) { processKeys(keystate); } } break; } case LoseState::PoppingLamps: { if (numLamps == 0) { if (numDust == 0) { losing = LoseState::PoppingPlayer; } } else { losePopLampTimer.accumulate(frameTime); while (losePopLampTimer.step()) { std::vector> lamps; for (int y = map.getTop(); y < map.getBottom(); y++) { for (int x = map.getLeft(); x < map.getRight(); x++) { if (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(std::get<0>(popPos), std::get<1>(popPos), 1); } } break; } case LoseState::PoppingPlayer: { losePopPlayerTimer.accumulate(frameTime); if (losePopPlayerTimer.step()) { popLamp(player_x, player_y, 10); renderPlayer = false; losing = LoseState::Outro; } break; } case LoseState::Outro: { if (numDust == 0) { quit = true; } break; } } if (dirtyLighting) { recalculateLighting(); for (int y = map.getTop(); y < map.getBottom(); y++) { for (int x = map.getLeft(); x < map.getRight(); x++) { if (!map.at(x,y).lit && map.at(x,y).wasLit) { if (std::bernoulli_distribution(0.5)(rng)) { map.at(x,y).tile = Tile::Wall; } else { map.at(x,y).tile = Tile::Floor; } map.at(x,y).dirtyRender = true; } } } tick(true); tick(true); tick(true); // TODO: better zoom algorithm setZoom(litSpots / 1500 + INIT_ZOOM); } if (dirtyRender) { recalculateRender(); } zoomTimer.accumulate(frameTime); while (zoomTimer.step()) { if (zooming) { zoomProgress++; if (zoomProgress == zoomLength) { zooming = false; } } } playerAnim.update(frameTime); }