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 : }
|