LCOV - code coverage report
Current view: top level - src - game.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 196 355 55.2 %
Date: 2020-10-15 20:26:03 Functions: 17 20 85.0 %

          Line data    Source code
       1             : #include "game.h"
       2             : 
       3             : #include "event.h"
       4             : 
       5             : #include <iostream>
       6             : 
       7           5 : Game::Game(Window& window)
       8             :     : window(window), mobSystem_(*this), physicsSystem_(*this), renderSystem_(*this),
       9           5 :       groundTiles_(worldBounds.width, worldBounds.height, '.') {}
      10             : 
      11           5 : void Game::setup() {
      12           5 :     const auto& b = worldBounds;
      13             : 
      14             :     // Create player
      15           5 :     auto& playerMob = createMob(MobType::Player, {0, 0});
      16           5 :     player = playerMob.entity;
      17           5 :     cameraTarget = playerMob.position;
      18           5 :     cameraPosition = playerMob.position;
      19             : 
      20             :     // Setup terrain
      21           5 :     groundTiles_.fill('.');
      22         645 :     for (int x = b.left; x < b.left + b.width; x++) {
      23       31360 :         for (int y = b.top - b.height + 1; y <= b.top; y++) {
      24       30720 :             if (randInt(0, 6) == 0) {
      25        4420 :                 groundTile({x, y}) = choose({',', '_', ' '});
      26             :             }
      27             :         }
      28             :     }
      29             : 
      30             :     // Populate world with mobs
      31           5 :     int numMobs = (int)(0.5 * sqrt(b.width * b.height));
      32         200 :     for (int i = 0; i < numMobs; i++) {
      33         195 :         MobType type = choose({MobType::Rabbit, MobType::OrcStrong, MobType::Snake});
      34         195 :         vec2i pos{randInt(b.left, b.left + b.width - 1), randInt(b.top - b.height + 1, b.top)};
      35         195 :         createMob(type, pos);
      36             : 
      37         195 :         if (i % 32 == 0)
      38          10 :             sync();
      39             :     }
      40             : 
      41             :     // Mob-less sprites
      42         100 :     for (int i = 0; i < numMobs / 2; i++) {
      43          95 :         vec2i pos{randInt(b.left, b.left + b.width - 1), randInt(b.top - b.height + 1, b.top)};
      44          95 :         if (randInt(0, 2) != 0) {
      45          60 :             createSprite("vV", true, 6, TB_MAGENTA, TB_BLACK, pos, RenderLayer::GroundCover);
      46          35 :         } else if (randInt(0, 1) == 0) {
      47          16 :             createSprite("|/-\\", true, 2, TB_YELLOW, TB_BLACK, pos, RenderLayer::GroundCover);
      48             :         } else {
      49          19 :             createSprite("Xx", true, 1, TB_BLUE, TB_BLACK, pos, RenderLayer::GroundCover);
      50             :         }
      51             : 
      52          95 :         if (i % 32 == 0)
      53           5 :             sync();
      54             :     }
      55             : 
      56           5 :     sync();
      57           5 : }
      58             : 
      59           5 : bool Game::update() {
      60           5 :     handleInput();
      61           5 :     updateCamera(); // NB: Outside of world update
      62             : 
      63             :     // Use this to slow down the world update
      64           5 :     const int subTicksPerTick = 2;
      65           5 :     if (--subTick_ <= 0) {
      66           5 :         subTick_ = subTicksPerTick;
      67             : 
      68           5 :         if (freezeTimer > 0)
      69           0 :             freezeTimer--;
      70           5 :         bool updateWorld = (freezeTimer == 0);
      71             : 
      72           5 :         if (updateWorld) {
      73           5 :             updatePlayer();
      74             : 
      75          20 :             for (auto* sys : systems_) {
      76          15 :                 sys->update();
      77             :             }
      78             : 
      79         502 :             for (auto& e : entities.values()) {
      80         497 :                 e.age++;
      81         497 :                 if (e.life > 0 && e.age >= e.life) {
      82           0 :                     queueEvent(EvRemove{e.id});
      83             :                 }
      84             :             }
      85             : 
      86             :             // Dirt system
      87       30725 :             for (auto& c : groundTiles_.data()) {
      88             :                 // Roughen flat ground
      89       30720 :                 if (c == '_' && randInt(0, 60) == 0)
      90          24 :                     c = '.';
      91             :             }
      92             :         }
      93             : 
      94             :         // Events
      95           5 :         auto& events = events_[eventsIndex_];
      96           5 :         eventsIndex_ = 1 - eventsIndex_; // toggle buffer
      97          10 :         std::vector<ident> remove;
      98             : 
      99           5 :         for (const EvAny& any : events) {
     100           0 :             const bool logEvents = false;
     101             :             if (logEvents) {
     102             :                 log(to_string(any));
     103             :             }
     104             : 
     105           0 :             if (any.is<EvRemove>()) {
     106           0 :                 const auto& ev = any.get<EvRemove>();
     107           0 :                 remove.push_back({ev.entity});
     108           0 :             } else if (any.is<EvKillMob>()) {
     109           0 :                 const auto& ev = any.get<EvKillMob>();
     110           0 :                 Mob& mob = mobs[ev.who];
     111           0 :                 auto& e = entities[mob.entity];
     112           0 :                 auto& sprite = sprites[e.sprite];
     113           0 :                 queueEvent(EvRemove{mob.entity});
     114             : 
     115           0 :                 if (onScreen(mob.position)) {
     116           0 :                     cameraShake = true;
     117           0 :                     cameraShakeTimer = 0;
     118           0 :                     cameraShakeStrength = 2;
     119           0 :                     freezeTimer = 1;
     120             :                 }
     121             : 
     122           0 :                 createBloodSplatter(mob.position);
     123           0 :                 createBones(sprite.frames[sprite.frame], mob.position);
     124           0 :             } else if (any.is<EvSpawnMob>()) {
     125           0 :                 const auto& ev = any.get<EvSpawnMob>();
     126           0 :                 createMob(ev.type, ev.position);
     127           0 :             } else if (any.is<EvTryWalk>()) {
     128             :                 // const auto& ev = any.get<EvTryWalk>();
     129             :                 // ...
     130           0 :             } else if (any.is<EvWalked>()) {
     131           0 :                 const auto& ev = any.get<EvWalked>();
     132           0 :                 if (ev.mob == entities[player].mob) {
     133             :                     // Camera tracks player
     134             :                     const vec2i margin{8, 4};
     135           0 :                     vec2i newScreenPos = screenCoord(ev.to);
     136           0 :                     if ((window.width() - newScreenPos.x) < margin.x) {
     137           0 :                         cameraTarget.x += margin.x;
     138           0 :                     } else if (newScreenPos.x < margin.x) {
     139           0 :                         cameraTarget.x -= margin.x;
     140           0 :                     } else if ((window.height() - newScreenPos.y) < margin.y) {
     141           0 :                         cameraTarget.y -= margin.y;
     142           0 :                     } else if (newScreenPos.y < margin.y) {
     143           0 :                         cameraTarget.y += margin.y;
     144             :                     }
     145             :                 }
     146           0 :             } else if (any.is<EvAttack>()) {
     147           0 :                 const auto& ev = any.get<EvAttack>();
     148           0 :                 if (onScreen(mobs[ev.target].position)) {
     149           0 :                     cameraShake = true;
     150           0 :                     cameraShakeTimer = 0;
     151           0 :                     cameraShakeStrength = 1;
     152             :                 }
     153             :             }
     154             : 
     155           0 :             for (auto* sys : systems_) {
     156           0 :                 sys->handleEvent(any);
     157             :             }
     158             :         }
     159           5 :         events.clear();
     160             : 
     161           5 :         auto it = remove.begin();
     162           5 :         while (it != remove.end()) {
     163           0 :             const ident& id = *it;
     164           0 :             Entity& e = entities[id];
     165           0 :             if (!e) {
     166           0 :                 ++it;
     167           0 :                 continue; // Already removed
     168             :             }
     169             : 
     170             :             using cid = std::pair<ComponentType, ident>;
     171           0 :             auto comps = {cid{ComponentType::Mob, e.mob}, cid{ComponentType::Sprite, e.sprite},
     172           0 :                           cid{ComponentType::Physics, e.physics}};
     173             : 
     174           0 :             for (auto pair : comps) {
     175           0 :                 ident component = pair.second;
     176           0 :                 if (component) {
     177           0 :                     ComponentType type = pair.first;
     178             : 
     179           0 :                     switch (type) {
     180           0 :                     default:
     181           0 :                         break;
     182           0 :                     case ComponentType::Mob: {
     183           0 :                         mobs.remove(component);
     184           0 :                         break;
     185             :                     }
     186           0 :                     case ComponentType::Sprite: {
     187           0 :                         sprites.remove(component);
     188           0 :                         break;
     189             :                     }
     190           0 :                     case ComponentType::Physics: {
     191           0 :                         physics.remove(component);
     192           0 :                         break;
     193             :                     }
     194             :                     }
     195             :                 }
     196             :             }
     197             : 
     198           0 :             for (const auto& ch : e.children) {
     199           0 :                 queueEvent(EvRemove{ch});
     200             :             }
     201           0 :             e.children.clear();
     202             : 
     203           0 :             entities.remove(id);
     204           0 :             it++;
     205             :         }
     206             : 
     207           5 :         sync();
     208           5 :         tick_++;
     209             : 
     210           5 :         while (!log_.empty()) {
     211           0 :             const auto& pair = log_.front();
     212           0 :             if (tick_ > pair.second + 20) {
     213           0 :                 log_.pop_front();
     214             :             } else
     215           0 :                 break;
     216             :         }
     217             :     }
     218             : 
     219           5 :     return true;
     220             : }
     221             : 
     222           5 : void Game::render() {
     223           5 :     window.clear();
     224           5 :     renderSystem_.render();
     225             : 
     226           5 :     const bool showLog = true;
     227             :     if (showLog) {
     228           5 :         const int maxMessages = 10;
     229           5 :         int y = 0;
     230           5 :         for (const auto& message : log_) {
     231           0 :             auto tick = std::to_string(message.second);
     232           0 :             for (int x = 0; x < (int)tick.size(); x++) {
     233           0 :                 window.set(x, y, tick[x], TB_WHITE, TB_BLUE);
     234             :             }
     235           0 :             for (int x = (int)tick.size(); x < 6; x++) {
     236           0 :                 window.set(x, y, ' ', TB_WHITE, TB_BLUE);
     237             :             }
     238           0 :             for (int x = 0; x < (int)message.first.size(); x++) {
     239           0 :                 window.set(6 + x, y, message.first[x], TB_WHITE, TB_BLUE);
     240             :             }
     241           0 :             y++;
     242           0 :             if (y > maxMessages)
     243           0 :                 break;
     244             :         }
     245             :     }
     246             : 
     247          10 :     std::string header = "Some Roguelike Thing";
     248         105 :     for (int x = 0; x < (int)header.size(); x++) {
     249         100 :         window.set(x, 0, header[x], TB_WHITE, TB_BLUE);
     250             :     }
     251        1185 :     for (int x = (int)header.size(); x < window.width(); x++) {
     252        1180 :         window.set(x, 0, ' ', TB_WHITE, TB_BLUE);
     253             :     }
     254             : 
     255             : #ifdef __EMSCRIPTEN__
     256             :     std::string footer = "Arrows: Move. Code: https://github.com/eigenbom/game-example.";
     257             : #else
     258          10 :     std::string footer = "ESC: Exit. Arrows: Move.";
     259             : #endif
     260             : 
     261         125 :     for (int x = 0; x < (int)footer.size(); x++) {
     262         120 :         window.set(x, window.height() - 1, footer[x], TB_WHITE, TB_BLUE);
     263             :     }
     264        1165 :     for (int x = (int)footer.size(); x < window.width(); x++) {
     265        1160 :         window.set(x, 0, ' ', TB_WHITE, TB_BLUE);
     266             :     }
     267           5 : }
     268             : 
     269      327680 : vec2i Game::worldCoord(vec2i screenCoord) const {
     270      327680 :     const vec2i ws{window.width(), window.height()};
     271      327680 :     vec2i q = screenCoord - ws / 2;
     272      327680 :     vec2i camFinal = cameraPosition + (cameraShake ? cameraShakeOffset : vec2i{0, 0});
     273      327680 :     return {q.x + camFinal.x, -(q.y - camFinal.y)};
     274             : }
     275             : 
     276      346473 : vec2i Game::screenCoord(vec2i worldCoord) const {
     277      346473 :     const vec2i ws{window.width(), window.height()};
     278      346473 :     vec2i wc = worldCoord;
     279      346473 :     vec2i camFinal = cameraPosition + (cameraShake ? cameraShakeOffset : vec2i{0, 0});
     280      346473 :     return vec2i{wc.x - camFinal.x, camFinal.y - wc.y} + ws / 2;
     281             : }
     282             : 
     283      337078 : bool Game::onScreen(vec2i worldCoord) const {
     284      337078 :     vec2i sc = screenCoord(worldCoord);
     285      337078 :     recti windowBounds{0, window.height() - 1, window.width(), window.height()};
     286      337078 :     return windowBounds.contains(sc);
     287             : }
     288             : 
     289           1 : void Game::queueEvent(const EvAny& ev) { events_[eventsIndex_].push_back(ev); }
     290             : 
     291          25 : void Game::sync() {
     292          25 :     entities.sync();
     293          25 :     mobs.sync();
     294          25 :     sprites.sync();
     295          25 :     physics.sync();
     296          25 : }
     297             : 
     298           0 : void Game::log(const std::string message) { log_.push_back({message, tick_}); }
     299             : 
     300         297 : Sprite& Game::createSprite(std::string frames, bool animated, int frameRate, uint16_t fg,
     301             :                            uint16_t bg, vec2i position, RenderLayer renderLayer) {
     302         297 :     auto& e = entities.add();
     303             : 
     304         297 :     auto& spr = sprites.add(Sprite{frames, animated, frameRate, fg, bg, position, renderLayer});
     305         297 :     spr.entity = e.id;
     306         297 :     e.sprite = spr.id;
     307         297 :     return spr;
     308             : }
     309             : 
     310         200 : Mob& Game::createMob(MobType type, vec2i position) {
     311         200 :     auto& e = entities.add();
     312             : 
     313         200 :     auto& info = MobDatabase.at(type);
     314         200 :     Mob& mob = mobs.add(Mob{&info});
     315         200 :     e.mob = mob.id;
     316         200 :     mob.entity = e.id;
     317             : 
     318         200 :     mob.health = info.health;
     319         200 :     mob.position = position;
     320             : 
     321         200 :     const char* frames = "?!";
     322         200 :     int frameRate = 1;
     323         200 :     auto fg = TB_WHITE;
     324         200 :     auto bg = TB_BLACK;
     325         200 :     switch (mob.info->category) {
     326          62 :     case MobCategory::Rabbit:
     327          62 :         frames = "r";
     328          62 :         frameRate = 1;
     329          62 :         fg = TB_YELLOW;
     330          62 :         break;
     331          64 :     case MobCategory::Snake:
     332          64 :         frames = "i!~~";
     333          64 :         frameRate = 0;
     334          64 :         fg = TB_GREEN;
     335          64 :         break;
     336          69 :     case MobCategory::Orc:
     337          69 :         frames = "oO";
     338          69 :         frameRate = 3;
     339          69 :         fg = TB_GREEN;
     340          69 :         bg = TB_BLACK;
     341          69 :         break;
     342           5 :     case MobCategory::Player:
     343           5 :         frames = "@";
     344           5 :         break;
     345           0 :     default:
     346           0 :         frames = "?!";
     347           0 :         break;
     348             :     }
     349             :     auto& spr =
     350         200 :         sprites.add(Sprite(frames, frameRate > 0, frameRate, fg, bg, position, RenderLayer::Mob));
     351         200 :     e.sprite = spr.id;
     352         200 :     spr.entity = e.id;
     353             : 
     354         200 :     switch (mob.info->category) {
     355          64 :     case MobCategory::Snake: {
     356             :         auto spr = createSprite("oo", false, 0, TB_GREEN, TB_BLACK, mob.position + mob.dir,
     357         128 :                                 RenderLayer::Mob);
     358          64 :         auto& child = entities[spr.entity];
     359          64 :         e.addChild(child);
     360             : 
     361          64 :         mob.extraSprite = spr.id;
     362          64 :         break;
     363             :     }
     364          69 :     case MobCategory::Orc: {
     365             :         auto& spr1 = createSprite("\\|", true, 6, TB_GREEN, TB_BLACK, mob.position + vec2i{-1, 1},
     366          69 :                                   RenderLayer::MobBelow);
     367          69 :         auto& child1 = entities[spr1.entity];
     368          69 :         e.addChild(child1);
     369          69 :         mob.extraSprite = spr1.id;
     370             : 
     371             :         auto& spr2 = createSprite("/|", true, 6, TB_GREEN, TB_BLACK, mob.position + vec2i{1, 1},
     372          69 :                                   RenderLayer::MobBelow);
     373          69 :         auto& child2 = entities[spr2.entity];
     374          69 :         e.addChild(child2);
     375          69 :         mob.extraSprite2 = spr2.id;
     376          69 :         break;
     377             :     }
     378          67 :     default:
     379          67 :         break;
     380             :     }
     381             : 
     382         200 :     return mob;
     383             : }
     384             : 
     385           0 : void Game::createBloodSplatter(vec2i position) {
     386           0 :     if (sprites.size() >= sprites.max_size() / 2)
     387           0 :         return;
     388             : 
     389           0 :     const int radius = 3;
     390           0 :     const int sqradius = radius * radius;
     391           0 :     for (int dx = -radius; dx <= radius; dx++) {
     392           0 :         for (int dy = -radius; dy <= radius; dy++) {
     393           0 :             if ((dx * dx + dy * dy) <= sqradius) {
     394           0 :                 if (randInt(0, 4) != 0) {
     395             :                     auto& spr = createSprite(".", false, 0, TB_RED, TB_BLACK,
     396           0 :                                              position + vec2i{dx, dy}, RenderLayer::Ground);
     397           0 :                     auto& e = entities[spr.entity];
     398           0 :                     e.life = randInt(200, 300);
     399             :                 }
     400             :             }
     401             :         }
     402             :     }
     403             : 
     404           0 :     int numBloodParticles = randInt(10, 40);
     405           0 :     for (int i = 0; i < numBloodParticles; i++) {
     406           0 :         auto& spr = createSprite("o", false, 0, TB_RED, TB_BLACK, position, RenderLayer::Particles);
     407           0 :         auto& e = entities[spr.entity];
     408           0 :         e.life = randInt(6, 12);
     409             : 
     410           0 :         double vel = random(0.4, 0.6);
     411             : 
     412           0 :         Physics& ph = physics.add();
     413           0 :         ph.type = PhysicsType::Projectile;
     414           0 :         ph.position = (vec2d)position;
     415           0 :         double th = random(-M_PI, M_PI);
     416           0 :         ph.velocity.x = vel * cos(th);
     417           0 :         ph.velocity.y = vel * sin(th);
     418             : 
     419           0 :         e.physics = ph.id;
     420           0 :         ph.entity = e.id;
     421             :     }
     422             : }
     423             : 
     424           0 : void Game::createBones(char c, vec2i position) {
     425             :     auto& spr =
     426           0 :         createSprite(std::string(1, c), false, 0, TB_RED, TB_BLACK, position, RenderLayer::Ground);
     427           0 :     auto& e = entities[spr.entity];
     428           0 :     e.life = randInt(100, 110);
     429           0 : }
     430             : 
     431           5 : void Game::handleInput() {
     432           8 :     for (auto ev : window.events()) {
     433           3 :         bool isPlayerMove = [ev]() {
     434           3 :             switch (ev) {
     435           3 :             case WindowEvent::ArrowUp:
     436             :             case WindowEvent::ArrowDown:
     437             :             case WindowEvent::ArrowLeft:
     438             :             case WindowEvent::ArrowRight:
     439           3 :                 return true;
     440           0 :             default:
     441           0 :                 return false;
     442             :             }
     443           3 :         }();
     444             : 
     445           3 :         if (!isPlayerMove || windowEvents_.size() < 1) {
     446             :             // log(to_string(ev));
     447           3 :             windowEvents_.push_back(ev);
     448             :         }
     449             :     }
     450           5 : }
     451             : 
     452           5 : void Game::updatePlayer() {
     453           5 :     auto& entity = entities[player];
     454           5 :     auto& mob = mobs[entity.mob];
     455             : 
     456           5 :     mob.tick += mob.info->speed;
     457           5 :     mob.tick = std::min(mob.tick, 2 * Mob::TicksPerAction - 1);
     458             : 
     459             :     // Map input to player commands
     460           5 :     vec2i movePlayer{0, 0};
     461             : 
     462           5 :     while (!windowEvents_.empty()) {
     463           3 :         const auto& ev = windowEvents_.front();
     464           3 :         switch (ev) {
     465           0 :         default:
     466           0 :             break;
     467           0 :         case WindowEvent::ArrowUp:
     468           0 :             movePlayer = vec2i{0, 1};
     469           0 :             break;
     470           1 :         case WindowEvent::ArrowDown:
     471           1 :             movePlayer = vec2i{0, -1};
     472           1 :             break;
     473           1 :         case WindowEvent::ArrowLeft:
     474           1 :             movePlayer = vec2i{-1, 0};
     475           1 :             break;
     476           1 :         case WindowEvent::ArrowRight:
     477           1 :             movePlayer = vec2i{1, 0};
     478           1 :             break;
     479             :         }
     480             : 
     481             :         // A move requires a full action
     482           3 :         if (movePlayer != vec2i{0, 0}) {
     483           3 :             if (mob.tick >= Mob::TicksPerAction) {
     484           0 :                 windowEvents_.pop_front();
     485           0 :                 mob.tick -= Mob::TicksPerAction;
     486           0 :                 break;
     487             :             } else {
     488             :                 // Can't move yet
     489           6 :                 return;
     490             :             }
     491             :         }
     492             :     }
     493             : 
     494           2 :     if (movePlayer != vec2i{0, 0}) {
     495           0 :         vec2i oldPos = mob.position;
     496           0 :         vec2i newPos = oldPos + movePlayer;
     497             : 
     498           0 :         ident target = invalid_id;
     499           0 :         for (auto& other : mobs.values()) {
     500           0 :             if (other.id != mob.id && other.position == newPos) {
     501           0 :                 target = other.id;
     502           0 :                 break;
     503             :             }
     504             :         }
     505             : 
     506           0 :         if (target) {
     507           0 :             queueEvent(EvAttack{mob.id, target});
     508             :         } else {
     509           0 :             queueEvent(EvTryWalk{mob.id, oldPos, newPos});
     510             :         }
     511             :     }
     512             : }
     513             : 
     514           5 : void Game::updateCamera() {
     515           5 :     if (cameraShake) {
     516           0 :         cameraShakeTimer++;
     517           0 :         if (cameraShakeStrength == 1)
     518           0 :             cameraShakeTimer++;
     519             : 
     520           0 :         if (cameraShakeTimer > 7) {
     521           0 :             cameraShake = false;
     522           0 :             cameraShakeOffset = vec2i{0, 0};
     523           0 :             cameraShakeTimer = 0;
     524           0 :         } else if (cameraShakeTimer % 2 == 0) {
     525           0 :             if (cameraShakeStrength == 1) {
     526           0 :                 if (randInt(0, 1) == 0) {
     527           0 :                     cameraShakeOffset = vec2i{randInt(-1, 1), 0};
     528             :                 } else {
     529           0 :                     cameraShakeOffset = vec2i{0, randInt(-1, 1)};
     530             :                 }
     531             :             } else {
     532           0 :                 cameraShakeOffset = vec2i{randInt(-1, 1), randInt(-1, 1)};
     533             :             }
     534             :         }
     535             :     }
     536             : 
     537           5 :     if (cameraPosition != cameraTarget) {
     538             :         static int cameraTick_ = 0;
     539           0 :         if (cameraTick_++ >= 1) {
     540           0 :             cameraTick_ = 0;
     541           0 :             vec2i dc = cameraTarget - cameraPosition;
     542           0 :             int dx = sign(dc.x);
     543           0 :             int dy = sign(dc.y);
     544           0 :             cameraPosition += vec2i{dx, dy};
     545             :         }
     546             :     }
     547           8 : }

Generated by: LCOV version 1.13