summaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/cpp/animation.cpp37
-rw-r--r--core/src/cpp/animation.h45
-rw-r--r--core/src/cpp/assets.h585
-rw-r--r--core/src/cpp/fps_counter.cpp41
-rw-r--r--core/src/cpp/fps_counter.h31
-rw-r--r--core/src/cpp/main.cpp64
-rw-r--r--core/src/cpp/map.cpp43
-rw-r--r--core/src/cpp/map.h63
-rw-r--r--core/src/cpp/player.cpp154
-rw-r--r--core/src/cpp/player.h63
-rw-r--r--core/src/cpp/size2d.h15
-rw-r--r--core/src/cpp/sprite.cpp49
-rw-r--r--core/src/cpp/sprite.h48
-rw-r--r--core/src/cpp/vec2d.h7
14 files changed, 1245 insertions, 0 deletions
diff --git a/core/src/cpp/animation.cpp b/core/src/cpp/animation.cpp
new file mode 100644
index 0000000..fbda115
--- /dev/null
+++ b/core/src/cpp/animation.cpp
@@ -0,0 +1,37 @@
+#include "animation.h"
+
+Animation::Animation(
+ SDL_Renderer* renderer,
+ const std::string& filename,
+ const int width,
+ const double durationS,
+ const bool repeat
+)
+ : Sprite{renderer, filename, width}
+ , durationS{durationS}
+ , repeat{repeat}
+{
+}
+
+bool Animation::animation_ended(const TimeStamp timestamp) const
+{
+ double elapsed = std::chrono::duration<double>(Clock::now() - timestamp)
+ .count(); // seconds from timestamp to now
+ return !repeat && elapsed >= durationS;
+}
+
+int Animation::frame(const TimeStamp timestamp) const
+{
+ // seconds from timestamp to now
+ double elapsed =
+ std::chrono::duration<double>(Clock::now() - timestamp).count();
+ // FIXME: division by zero
+ int idx = static_cast<int>(nframes * elapsed / durationS);
+
+ return repeat ? idx % nframes : std::min(idx, nframes - 1);
+}
+
+SDL_Rect Animation::rect(const TimeStamp timestamp) const
+{
+ return {frame(timestamp) * width, 0, width, height};
+}
diff --git a/core/src/cpp/animation.h b/core/src/cpp/animation.h
new file mode 100644
index 0000000..d74ca75
--- /dev/null
+++ b/core/src/cpp/animation.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "sprite.h"
+
+struct Animation : public Sprite
+{
+ explicit Animation(
+ SDL_Renderer* renderer,
+ const std::string& filename,
+ const int width,
+ const double durationS,
+ const bool repeat
+ );
+ ~Animation() override = default;
+
+public:
+ /*!
+ * \brief animation_ended - is the animation sequence ended as specified
+ * timestamp?
+ * \param timestamp
+ * \return true if ended, false otherwise
+ */
+ bool animation_ended(const TimeStamp timestamp) const;
+
+ /*!
+ * \brief frame - compute the frame number at current time for the the
+ * animation started at timestamp
+ * \param timestamp
+ * \return frame number
+ */
+ int frame(const TimeStamp timestamp) const;
+
+ /*!
+ * \brief rect - choose the right frame from the texture
+ * \param timestamp
+ * \return frame rect
+ */
+ SDL_Rect rect(const TimeStamp timestamp) const;
+
+ /*!
+ * \brief durationS - durationS of the animation sequence in seconds
+ */
+ const double durationS{1.};
+ const bool repeat{false}; // should we repeat the animation?
+};
diff --git a/core/src/cpp/assets.h b/core/src/cpp/assets.h
new file mode 100644
index 0000000..8c0790a
--- /dev/null
+++ b/core/src/cpp/assets.h
@@ -0,0 +1,585 @@
+#pragma once
+
+#include <array>
+#include <string>
+
+/*
+ * Pixel Dungeon
+ * Copyright (C) 2012-2015 Oleg Dolya
+ *
+ * Shattered Pixel Dungeon
+ * Copyright (C) 2014-2025 Evan Debenham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General License for more details.
+ *
+ * You should have received a copy of the GNU General License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ */
+
+namespace Assets {
+namespace Effects {
+static inline std::string EFFECTS = "effects/effects.png";
+static inline std::string FIREBALL = "effects/fireball.png";
+static inline std::string SPECKS = "effects/specks.png";
+static inline std::string SPELL_ICONS = "effects/spell_icons.png";
+static inline std::string TEXT_ICONS = "effects/text_icons.png";
+} // namespace Effects
+
+namespace Environment {
+static inline std::string TERRAIN_FEATURES = "environment/terrain_features.png";
+static inline std::string VISUAL_GRID = "environment/visual_grid.png";
+static inline std::string WALL_BLOCKING = "environment/wall_blocking.png";
+static inline std::string TILES_SEWERS = "environment/tiles_sewers.png";
+static inline std::string TILES_PRISON = "environment/tiles_prison.png";
+static inline std::string TILES_CAVES = "environment/tiles_caves.png";
+static inline std::string TILES_CITY = "environment/tiles_city.png";
+static inline std::string TILES_HALLS = "environment/tiles_halls.png";
+static inline std::string TILES_CAVES_CRYSTAL = "environment/tiles_caves_crystal.png";
+static inline std::string TILES_CAVES_GNOLL = "environment/tiles_caves_gnoll.png";
+static inline std::string WATER_SEWERS = "environment/water0.png";
+static inline std::string WATER_PRISON = "environment/water1.png";
+static inline std::string WATER_CAVES = "environment/water2.png";
+static inline std::string WATER_CITY = "environment/water3.png";
+static inline std::string WATER_HALLS = "environment/water4.png";
+static inline std::string WEAK_FLOOR = "environment/custom_tiles/weak_floor.png";
+static inline std::string SEWER_BOSS = "environment/custom_tiles/sewer_boss.png";
+static inline std::string PRISON_QUEST = "environment/custom_tiles/prison_quest.png";
+static inline std::string PRISON_EXIT = "environment/custom_tiles/prison_exit.png";
+static inline std::string CAVES_QUEST = "environment/custom_tiles/caves_quest.png";
+static inline std::string CAVES_BOSS = "environment/custom_tiles/caves_boss.png";
+static inline std::string CITY_QUEST = "environment/custom_tiles/city_quest.png";
+static inline std::string CITY_BOSS = "environment/custom_tiles/city_boss.png";
+static inline std::string HALLS_SP = "environment/custom_tiles/halls_special.png";
+} // namespace Environment
+
+namespace Fonts {
+static inline std::string PIXELFONT = "fonts/pixel_font.png";
+}
+
+namespace Interfaces {
+static inline std::string ARCS_BG = "interfaces/arcs1.png";
+
+static inline std::string ARCS_FG = "interfaces/arcs2.png";
+
+static inline std::string BANNERS = "interfaces/banners.png";
+
+static inline std::string BADGES = "interfaces/badges.png";
+
+static inline std::string LOCKED = "interfaces/locked_badge.png";
+
+static inline std::string CHROME = "interfaces/chrome.png";
+
+static inline std::string ICONS = "interfaces/icons.png";
+
+static inline std::string STATUS = "interfaces/status_pane.png";
+
+static inline std::string MENU = "interfaces/menu_pane.png";
+
+static inline std::string MENU_BTN = "interfaces/menu_button.png";
+
+static inline std::string TOOLBAR = "interfaces/toolbar.png";
+
+static inline std::string SHADOW = "interfaces/shadow.png";
+
+static inline std::string BOSSHP = "interfaces/boss_hp.png";
+
+static inline std::string SURFACE = "interfaces/surface.png";
+
+static inline std::string BUFFS_SMALL = "interfaces/buffs.png";
+
+static inline std::string BUFFS_LARGE = "interfaces/large_buffs.png";
+
+static inline std::string TALENT_ICONS = "interfaces/talent_icons.png";
+
+static inline std::string TALENT_BUTTON = "interfaces/talent_button.png";
+
+static inline std::string HERO_ICONS = "interfaces/hero_icons.png";
+
+static inline std::string RADIAL_MENU = "interfaces/radial_menu.png";
+} // namespace Interfaces
+
+namespace Messages {
+static inline std::string ACTORS = "messages/actors/actors";
+
+static inline std::string ITEMS = "messages/items/items";
+
+static inline std::string JOURNAL = "messages/journal/journal";
+
+static inline std::string LEVELS = "messages/levels/levels";
+
+static inline std::string MISC = "messages/misc/misc";
+
+static inline std::string PLANTS = "messages/plants/plants";
+
+static inline std::string SCENES = "messages/scenes/scenes";
+
+static inline std::string UI = "messages/ui/ui";
+
+static inline std::string WINDOWS = "messages/windows/windows";
+} // namespace Messages
+
+namespace Music {
+static inline std::string THEME_1 = "music/theme_1.ogg";
+
+static inline std::string THEME_2 = "music/theme_2.ogg";
+
+static inline std::string THEME_FINALE = "music/theme_finale.ogg";
+
+static inline std::string SEWERS_1 = "music/sewers_1.ogg";
+
+static inline std::string SEWERS_2 = "music/sewers_2.ogg";
+
+static inline std::string SEWERS_3 = "music/sewers_3.ogg";
+
+static inline std::string SEWERS_TENSE = "music/sewers_tense.ogg";
+
+static inline std::string SEWERS_BOSS = "music/sewers_boss.ogg";
+
+static inline std::string PRISON_1 = "music/prison_1.ogg";
+
+static inline std::string PRISON_2 = "music/prison_2.ogg";
+
+static inline std::string PRISON_3 = "music/prison_3.ogg";
+
+static inline std::string PRISON_TENSE = "music/prison_tense.ogg";
+
+static inline std::string PRISON_BOSS = "music/prison_boss.ogg";
+
+static inline std::string CAVES_1 = "music/caves_1.ogg";
+
+static inline std::string CAVES_2 = "music/caves_2.ogg";
+
+static inline std::string CAVES_3 = "music/caves_3.ogg";
+
+static inline std::string CAVES_TENSE = "music/caves_tense.ogg";
+
+static inline std::string CAVES_BOSS = "music/caves_boss.ogg";
+
+static inline std::string CAVES_BOSS_FINALE = "music/caves_boss_finale.ogg";
+
+static inline std::string CITY_1 = "music/city_1.ogg";
+
+static inline std::string CITY_2 = "music/city_2.ogg";
+
+static inline std::string CITY_3 = "music/city_3.ogg";
+
+static inline std::string CITY_TENSE = "music/city_tense.ogg";
+
+static inline std::string CITY_BOSS = "music/city_boss.ogg";
+
+static inline std::string CITY_BOSS_FINALE = "music/city_boss_finale.ogg";
+
+static inline std::string HALLS_1 = "music/halls_1.ogg";
+
+static inline std::string HALLS_2 = "music/halls_2.ogg";
+
+static inline std::string HALLS_3 = "music/halls_3.ogg";
+
+static inline std::string HALLS_TENSE = "music/halls_tense.ogg";
+
+static inline std::string HALLS_BOSS = "music/halls_boss.ogg";
+
+static inline std::string HALLS_BOSS_FINALE = "music/halls_boss_finale.ogg";
+} // namespace Music
+
+namespace Sounds {
+static inline std::string CLICK = "sounds/click.mp3";
+
+static inline std::string BADGE = "sounds/badge.mp3";
+
+static inline std::string GOLD = "sounds/gold.mp3";
+
+static inline std::string OPEN = "sounds/door_open.mp3";
+
+static inline std::string UNLOCK = "sounds/unlock.mp3";
+
+static inline std::string ITEM = "sounds/item.mp3";
+
+static inline std::string DEWDROP = "sounds/dewdrop.mp3";
+
+static inline std::string STEP = "sounds/step.mp3";
+
+static inline std::string WATER = "sounds/water.mp3";
+
+static inline std::string GRASS = "sounds/grass.mp3";
+
+static inline std::string TRAMPLE = "sounds/trample.mp3";
+
+static inline std::string STURDY = "sounds/sturdy.mp3";
+
+static inline std::string HIT = "sounds/hit.mp3";
+
+static inline std::string MISS = "sounds/miss.mp3";
+
+static inline std::string HIT_SLASH = "sounds/hit_slash.mp3";
+
+static inline std::string HIT_STAB = "sounds/hit_stab.mp3";
+
+static inline std::string HIT_CRUSH = "sounds/hit_crush.mp3";
+
+static inline std::string HIT_MAGIC = "sounds/hit_magic.mp3";
+
+static inline std::string HIT_STRONG = "sounds/hit_strong.mp3";
+
+static inline std::string HIT_PARRY = "sounds/hit_parry.mp3";
+
+static inline std::string HIT_ARROW = "sounds/hit_arrow.mp3";
+
+static inline std::string ATK_SPIRITBOW = "sounds/atk_spiritbow.mp3";
+
+static inline std::string ATK_CROSSBOW = "sounds/atk_crossbow.mp3";
+
+static inline std::string HEALTH_WARN = "sounds/health_warn.mp3";
+
+static inline std::string HEALTH_CRITICAL = "sounds/health_critical.mp3";
+
+static inline std::string DESCEND = "sounds/descend.mp3";
+
+static inline std::string EAT = "sounds/eat.mp3";
+
+static inline std::string READ = "sounds/read.mp3";
+
+static inline std::string LULLABY = "sounds/lullaby.mp3";
+
+static inline std::string DRINK = "sounds/drink.mp3";
+
+static inline std::string SHATTER = "sounds/shatter.mp3";
+
+static inline std::string ZAP = "sounds/zap.mp3";
+
+static inline std::string LIGHTNING = "sounds/lightning.mp3";
+
+static inline std::string LEVELUP = "sounds/levelup.mp3";
+
+static inline std::string DEATH = "sounds/death.mp3";
+
+static inline std::string CHALLENGE = "sounds/challenge.mp3";
+
+static inline std::string CURSED = "sounds/cursed.mp3";
+
+static inline std::string TRAP = "sounds/trap.mp3";
+
+static inline std::string EVOKE = "sounds/evoke.mp3";
+
+static inline std::string TOMB = "sounds/tomb.mp3";
+
+static inline std::string ALERT = "sounds/alert.mp3";
+
+static inline std::string MELD = "sounds/meld.mp3";
+
+static inline std::string BOSS = "sounds/boss.mp3";
+
+static inline std::string BLAST = "sounds/blast.mp3";
+
+static inline std::string PLANT = "sounds/plant.mp3";
+
+static inline std::string RAY = "sounds/ray.mp3";
+
+static inline std::string BEACON = "sounds/beacon.mp3";
+
+static inline std::string TELEPORT = "sounds/teleport.mp3";
+
+static inline std::string CHARMS = "sounds/charms.mp3";
+
+static inline std::string MASTERY = "sounds/mastery.mp3";
+
+static inline std::string PUFF = "sounds/puff.mp3";
+
+static inline std::string ROCKS = "sounds/rocks.mp3";
+
+static inline std::string BURNING = "sounds/burning.mp3";
+
+static inline std::string FALLING = "sounds/falling.mp3";
+
+static inline std::string GHOST = "sounds/ghost.mp3";
+
+static inline std::string SECRET = "sounds/secret.mp3";
+
+static inline std::string BONES = "sounds/bones.mp3";
+
+static inline std::string BEE = "sounds/bee.mp3";
+
+static inline std::string DEGRADE = "sounds/degrade.mp3";
+
+static inline std::string MIMIC = "sounds/mimic.mp3";
+
+static inline std::string DEBUFF = "sounds/debuff.mp3";
+
+static inline std::string CHARGEUP = "sounds/chargeup.mp3";
+
+static inline std::string GAS = "sounds/gas.mp3";
+
+static inline std::string CHAINS = "sounds/chains.mp3";
+
+static inline std::string SCAN = "sounds/scan.mp3";
+
+static inline std::string SHEEP = "sounds/sheep.mp3";
+
+static inline std::string MINE = "sounds/mine.mp3";
+
+static inline std::array all{CLICK,
+ BADGE,
+ GOLD,
+
+ OPEN,
+ UNLOCK,
+ ITEM,
+ DEWDROP,
+ STEP,
+ WATER,
+ GRASS,
+ TRAMPLE,
+ STURDY,
+
+ HIT,
+ MISS,
+ HIT_SLASH,
+ HIT_STAB,
+ HIT_CRUSH,
+ HIT_MAGIC,
+ HIT_STRONG,
+ HIT_PARRY,
+ HIT_ARROW,
+ ATK_SPIRITBOW,
+ ATK_CROSSBOW,
+ HEALTH_WARN,
+ HEALTH_CRITICAL,
+
+ DESCEND,
+ EAT,
+ READ,
+ LULLABY,
+ DRINK,
+ SHATTER,
+ ZAP,
+ LIGHTNING,
+ LEVELUP,
+ DEATH,
+ CHALLENGE,
+ CURSED,
+ TRAP,
+ EVOKE,
+ TOMB,
+ ALERT,
+ MELD,
+ BOSS,
+ BLAST,
+ PLANT,
+ RAY,
+ BEACON,
+ TELEPORT,
+ CHARMS,
+ MASTERY,
+ PUFF,
+ ROCKS,
+ BURNING,
+ FALLING,
+ GHOST,
+ SECRET,
+ BONES,
+ BEE,
+ DEGRADE,
+ MIMIC,
+ DEBUFF,
+ CHARGEUP,
+ GAS,
+ CHAINS,
+ SCAN,
+ SHEEP,
+ MINE};
+} // namespace Sounds
+
+namespace Splashes {
+static inline std::string WARRIOR = "splashes/warrior.jpg";
+
+static inline std::string MAGE = "splashes/mage.jpg";
+
+static inline std::string ROGUE = "splashes/rogue.jpg";
+
+static inline std::string HUNTRESS = "splashes/huntress.jpg";
+
+static inline std::string DUELIST = "splashes/duelist.jpg";
+
+static inline std::string CLERIC = "splashes/cleric.jpg";
+
+static inline std::string SEWERS = "splashes/sewers.jpg";
+
+static inline std::string PRISON = "splashes/prison.jpg";
+
+static inline std::string CAVES = "splashes/caves.jpg";
+
+static inline std::string CITY = "splashes/city.jpg";
+
+static inline std::string HALLS = "splashes/halls.jpg";
+
+namespace Title {
+static inline std::string ARCHS = "splashes/title/archs.png";
+
+static inline std::string BACK_CLUSTERS = "splashes/title/back_clusters.png";
+
+static inline std::string MID_MIXED = "splashes/title/mid_mixed.png";
+
+static inline std::string FRONT_SMALL = "splashes/title/front_small.png";
+} // namespace Title
+} // namespace Splashes
+
+namespace Sprites {
+static inline std::string ITEMS = "sprites/items.png";
+
+static inline std::string ITEM_ICONS = "sprites/item_icons.png";
+
+static inline std::string WARRIOR = "sprites/warrior.png";
+
+static inline std::string MAGE = "sprites/mage.png";
+
+static inline std::string ROGUE = "sprites/rogue.png";
+
+static inline std::string HUNTRESS = "sprites/huntress.png";
+
+static inline std::string DUELIST = "sprites/duelist.png";
+
+static inline std::string CLERIC = "sprites/cleric.png";
+
+static inline std::string AVATARS = "sprites/avatars.png";
+
+static inline std::string PET = "sprites/pet.png";
+
+static inline std::string AMULET = "sprites/amulet.png";
+
+static inline std::string RAT = "sprites/rat.png";
+
+static inline std::string BRUTE = "sprites/brute.png";
+
+static inline std::string SPINNER = "sprites/spinner.png";
+
+static inline std::string DM300 = "sprites/dm300.png";
+
+static inline std::string WRAITH = "sprites/wraith.png";
+
+static inline std::string UNDEAD = "sprites/undead.png";
+
+static inline std::string KING = "sprites/king.png";
+
+static inline std::string PIRANHA = "sprites/piranha.png";
+
+static inline std::string EYE = "sprites/eye.png";
+
+static inline std::string GNOLL = "sprites/gnoll.png";
+
+static inline std::string CRAB = "sprites/crab.png";
+
+static inline std::string GOO = "sprites/goo.png";
+
+static inline std::string SWARM = "sprites/swarm.png";
+
+static inline std::string SKELETON = "sprites/skeleton.png";
+
+static inline std::string SHAMAN = "sprites/shaman.png";
+
+static inline std::string THIEF = "sprites/thief.png";
+
+static inline std::string TENGU = "sprites/tengu.png";
+
+static inline std::string SHEEP = "sprites/sheep.png";
+
+static inline std::string KEEPER = "sprites/shopkeeper.png";
+
+static inline std::string BAT = "sprites/bat.png";
+
+static inline std::string ELEMENTAL = "sprites/elemental.png";
+
+static inline std::string MONK = "sprites/monk.png";
+
+static inline std::string WARLOCK = "sprites/warlock.png";
+
+static inline std::string GOLEM = "sprites/golem.png";
+
+static inline std::string STATUE = "sprites/statue.png";
+
+static inline std::string SUCCUBUS = "sprites/succubus.png";
+
+static inline std::string SCORPIO = "sprites/scorpio.png";
+
+static inline std::string FISTS = "sprites/yog_fists.png";
+
+static inline std::string YOG = "sprites/yog.png";
+
+static inline std::string LARVA = "sprites/larva.png";
+
+static inline std::string GHOST = "sprites/ghost.png";
+
+static inline std::string MAKER = "sprites/wandmaker.png";
+
+static inline std::string TROLL = "sprites/blacksmith.png";
+
+static inline std::string IMP = "sprites/demon.png";
+
+static inline std::string RATKING = "sprites/ratking.png";
+
+static inline std::string BEE = "sprites/bee.png";
+
+static inline std::string MIMIC = "sprites/mimic.png";
+
+static inline std::string ROT_LASH = "sprites/rot_lasher.png";
+
+static inline std::string ROT_HEART = "sprites/rot_heart.png";
+
+static inline std::string GUARD = "sprites/guard.png";
+
+static inline std::string WARDS = "sprites/wards.png";
+
+static inline std::string GUARDIAN = "sprites/guardian.png";
+
+static inline std::string SLIME = "sprites/slime.png";
+
+static inline std::string SNAKE = "sprites/snake.png";
+
+static inline std::string NECRO = "sprites/necromancer.png";
+
+static inline std::string GHOUL = "sprites/ghoul.png";
+
+static inline std::string RIPPER = "sprites/ripper.png";
+
+static inline std::string SPAWNER = "sprites/spawner.png";
+
+static inline std::string DM100 = "sprites/dm100.png";
+
+static inline std::string PYLON = "sprites/pylon.png";
+
+static inline std::string DM200 = "sprites/dm200.png";
+
+static inline std::string LOTUS = "sprites/lotus.png";
+
+static inline std::string NINJA_LOG = "sprites/ninja_log.png";
+
+static inline std::string SPIRIT_HAWK = "sprites/spirit_hawk.png";
+
+static inline std::string RED_SENTRY = "sprites/red_sentry.png";
+
+static inline std::string CRYSTAL_WISP = "sprites/crystal_wisp.png";
+
+static inline std::string CRYSTAL_GUARDIAN = "sprites/crystal_guardian.png";
+
+static inline std::string CRYSTAL_SPIRE = "sprites/crystal_spire.png";
+
+static inline std::string GNOLL_GUARD = "sprites/gnoll_guard.png";
+
+static inline std::string GNOLL_SAPPER = "sprites/gnoll_sapper.png";
+
+static inline std::string GNOLL_GEOMANCER = "sprites/gnoll_geomancer.png";
+
+static inline std::string FUNGAL_SPINNER = "sprites/fungal_spinner.png";
+
+static inline std::string FUNGAL_SENTRY = "sprites/fungal_sentry.png";
+
+static inline std::string FUNGAL_CORE = "sprites/fungal_core.png";
+} // namespace Sprites
+} // namespace Assets
diff --git a/core/src/cpp/fps_counter.cpp b/core/src/cpp/fps_counter.cpp
new file mode 100644
index 0000000..fe4233d
--- /dev/null
+++ b/core/src/cpp/fps_counter.cpp
@@ -0,0 +1,41 @@
+#include "fps_counter.h"
+
+#include <SDL_render.h>
+
+FPS_Counter::FPS_Counter(SDL_Renderer* renderer)
+ // TODO: cleanup fucking hardcode everywhere
+ : renderer{renderer}
+ , numbers{renderer, "numbers.bmp", 24}
+{
+}
+
+void FPS_Counter::draw()
+{
+ fps_cur++;
+
+ double dt = std::chrono::duration<double>(Clock::now() - timestamp).count();
+
+ // update current FPS reading every 300 ms
+ // TODO: rewrite with type safety
+ if (dt >= .3)
+ {
+ fps_prev = fps_cur / dt;
+ fps_cur = 0;
+ timestamp = Clock::now();
+ }
+
+ // first character will be drawn here
+ SDL_Rect dst = {4, 16, numbers.width, numbers.height};
+
+ // extract individual digits of fps_prev
+ for (const char c : std::to_string(fps_prev))
+ {
+ // crude conversion of numeric characters to int: '7'-'0'=7
+ SDL_Rect src = numbers.rect(c - '0');
+ // draw current digit
+ SDL_RenderCopy(renderer, numbers.texture, &src, &dst);
+ // draw characters left-to-right, +4 for letter spacing
+ // TODO: add padding directly to the .bmp file
+ dst.x += numbers.width + 4;
+ }
+}
diff --git a/core/src/cpp/fps_counter.h b/core/src/cpp/fps_counter.h
new file mode 100644
index 0000000..6ac289e
--- /dev/null
+++ b/core/src/cpp/fps_counter.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "sprite.h"
+
+struct FPS_Counter {
+ explicit FPS_Counter(SDL_Renderer* renderer);
+
+ void draw();
+
+ /*!
+ * \brief fps_cur - the FPS readings are updated once in a while; fps_cur is
+ * the number of draw() calls since the last reading
+ */
+ int fps_cur{0};
+ /*!
+ * \brief fps_prev - and here is the last fps reading
+ */
+ int fps_prev{0};
+ /*!
+ * \brief timestamp - last time fps_prev was updated
+ */
+ TimeStamp timestamp{Clock::now()};
+ /*!
+ * \brief renderer - draw here
+ */
+ SDL_Renderer* renderer{nullptr};
+ /*!
+ * \brief numbers - "font" file
+ */
+ const Sprite numbers;
+};
diff --git a/core/src/cpp/main.cpp b/core/src/cpp/main.cpp
new file mode 100644
index 0000000..915e0ce
--- /dev/null
+++ b/core/src/cpp/main.cpp
@@ -0,0 +1,64 @@
+#include <iostream>
+#include <chrono>
+#include <thread>
+
+#include <SDL.h>
+
+#include "assets.h"
+#include "fps_counter.h"
+#include "map.h"
+#include "player.h"
+
+void main_loop(SDL_Renderer *renderer) {
+ FPS_Counter fps_counter(renderer);
+ Map map(renderer);
+ Player player(renderer);
+ TimeStamp timestamp = Clock::now();
+
+ while (true) { // main game loop
+ SDL_Event event; // handle window closing
+ if (SDL_PollEvent(&event) && (SDL_QUIT==event.type || (SDL_KEYDOWN==event.type && SDLK_ESCAPE==event.key.keysym.sym)))
+ break; // quit
+ player.handle_keyboard(); // no need for the event variable, direct keyboard state polling
+
+ const auto dt = Clock::now() - timestamp;
+ if (dt<std::chrono::milliseconds(20)) { // 50 FPS regulation
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ continue;
+ }
+ timestamp = Clock::now();
+
+ player.update_state(std::chrono::duration<double>(dt).count(), map); // gravity, movements, collision detection etc
+
+ SDL_RenderClear(renderer); // re-draw the window
+ fps_counter.draw();
+ player.draw();
+ map.draw();
+ SDL_RenderPresent(renderer);
+ }
+} // N.B. fps_counter, map and player objects call their destructors here, thus destroying allocated textures, it must be done prior to destroying the renderer
+
+int main() {
+ SDL_SetMainReady(); // tell SDL that we handle main() function ourselves, comes with the SDL_MAIN_HANDLED macro
+ if (SDL_Init(SDL_INIT_VIDEO)) {
+ std::cerr << "Failed to initialize SDL: " << SDL_GetError() << std::endl;
+ return -1;
+ }
+
+ SDL_Window *window = nullptr;
+ SDL_Renderer *renderer = nullptr;
+ if (SDL_CreateWindowAndRenderer(1024, 768, SDL_WINDOW_SHOWN | SDL_WINDOW_INPUT_FOCUS, &window, &renderer)) {
+ std::cerr << "Failed to create window and renderer: " << SDL_GetError() << std::endl;
+ return -1;
+ }
+ SDL_SetWindowTitle(window, "SDL2 game blank");
+ SDL_SetRenderDrawColor(renderer, 210, 255, 179, 255);
+
+ main_loop(renderer); // all interesting things happen here
+
+ SDL_DestroyRenderer(renderer);
+ SDL_DestroyWindow(window);
+ SDL_Quit();
+ return 0;
+}
+
diff --git a/core/src/cpp/map.cpp b/core/src/cpp/map.cpp
new file mode 100644
index 0000000..06548e3
--- /dev/null
+++ b/core/src/cpp/map.cpp
@@ -0,0 +1,43 @@
+#include "map.h"
+
+#include <iostream>
+
+Map::Map(SDL_Renderer* renderer)
+ : renderer{renderer}
+ , textures{renderer, "ground.bmp", 128}
+{
+ // +1 for the null terminated string
+ assert(sizeof(level) == size.w * size.h + 1);
+
+ size2d<int> window_size{0, 0};
+
+ if (!SDL_GetRendererOutputSize(renderer, &window_size.w, &window_size.h))
+ {
+ tile_size = window_size / size;
+ }
+ else
+ {
+ std::cerr << "Failed to get renderer size: " << SDL_GetError()
+ << std::endl;
+ }
+}
+
+void Map::draw()
+{
+ for (int j = 0; j < size.h; j++)
+ {
+ for (int i = 0; i < size.w; i++)
+ {
+ if (is_empty(i, j))
+ {
+ continue;
+ }
+
+ SDL_Rect dst =
+ {tile_size.w * i, tile_size.h * j, tile_size.w, tile_size.h};
+ SDL_Rect src = textures.rect(get(i, j));
+
+ SDL_RenderCopy(renderer, textures.texture, &src, &dst);
+ }
+ }
+}
diff --git a/core/src/cpp/map.h b/core/src/cpp/map.h
new file mode 100644
index 0000000..668305b
--- /dev/null
+++ b/core/src/cpp/map.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <cassert>
+
+#include <SDL.h>
+
+#include "size2d.h"
+#include "sprite.h"
+
+struct Map {
+ explicit Map(SDL_Renderer* renderer);
+
+ // draw the level in the renderer window
+ void draw();
+
+ int get(const int i, const int j) const { // retreive the cell, transform character to texture index
+ assert(i >= 0 && j >= 0 && i < size.w && j < size.h);
+ return level[i + j * size.w] - '0';
+ }
+
+ bool is_empty(const int i, const int j) const {
+ assert(i >= 0 && j >= 0 && i < size.w && j < size.h);
+ return level[i + j * size.w] == ' ';
+ }
+
+ /*!
+ * \brief renderer - draw here
+ */
+ SDL_Renderer* renderer{nullptr};
+ /*!
+ * \brief tile_size - tile size in the renderer window
+ */
+ size2d<int> tile_size{0, 0};
+
+ /*!
+ * \brief textures - textures to be drawn
+ */
+ const Sprite textures;
+
+ /*!
+ * \brief size - overall map dimensions
+ */
+ static constexpr size2d<int> size{16, 12};
+
+ /*!
+ * \brief level - the array level[] has the length
+ * w*h+1 (+1 for the null character), space character for empty tiles,
+ * digits indicate the texture index to be used per tile
+ */
+ static constexpr char level[size.w * size.h + 1] = " 123451234012340"
+ "5 5"
+ "0 0"
+ "5 5 5"
+ "0 0 0"
+ "512340 12345 5"
+ "0 0"
+ "5 51"
+ "0 50 12"
+ "5 51234"
+ "0 12345"
+ "1234012345052500";
+};
+
diff --git a/core/src/cpp/player.cpp b/core/src/cpp/player.cpp
new file mode 100644
index 0000000..ce2b421
--- /dev/null
+++ b/core/src/cpp/player.cpp
@@ -0,0 +1,154 @@
+#include "player.h"
+
+Player::Player(SDL_Renderer* renderer)
+ : renderer{renderer}
+ , sprites{
+ Animation{renderer, "rest.bmp", 256, 1.0, true},
+ Animation{renderer, "takeoff.bmp", 256, 0.3, false},
+ Animation{renderer, "flight.bmp", 256, 1.3, false},
+ Animation{renderer, "landing.bmp", 256, 0.3, false},
+ Animation{renderer, "walk.bmp", 256, 1.0, true},
+ Animation{renderer, "fall.bmp", 256, 1.0, true}
+ }
+{
+}
+
+void Player::set_state(const States& s)
+{
+ timestamp = Clock::now();
+ state = s;
+
+ if (state != States::FLIGHT && state != States::WALK)
+ {
+ speed.x = 0;
+ }
+ else if (state == States::WALK)
+ {
+ speed.x = backwards ? -150 : 150;
+ }
+ else if (state == States::FLIGHT)
+ {
+ speed.y = jump.y;
+ speed.x = backwards ? -jump.x : jump.x;
+ }
+}
+
+void Player::handle_keyboard()
+{
+ const Uint8* kbstate{SDL_GetKeyboardState(nullptr)};
+
+ if (state == States::WALK && !kbstate[SDL_SCANCODE_RIGHT] &&
+ !kbstate[SDL_SCANCODE_LEFT])
+ {
+ set_state(States::REST);
+ }
+
+ if ((state == States::REST || state == States::WALK) &&
+ kbstate[SDL_SCANCODE_UP])
+ {
+ if (kbstate[SDL_SCANCODE_LEFT] || kbstate[SDL_SCANCODE_RIGHT])
+ {
+ jump.x = 200; // long jump
+ jump.y = -200;
+ }
+ else
+ {
+ jump.x = 50; // high jump
+ jump.y = -300;
+ }
+
+ set_state(States::TAKEOFF);
+ }
+
+ if (state == States::REST &&
+ (kbstate[SDL_SCANCODE_LEFT] || kbstate[SDL_SCANCODE_RIGHT]))
+ {
+ backwards = kbstate[SDL_SCANCODE_LEFT];
+ set_state(States::WALK);
+ }
+}
+
+const Animation& Player::sprite(const States& state)
+{
+ return sprites.at(static_cast<int>(state));
+}
+
+void Player::update_state(const double dt, const Map& map)
+{
+ // takeoff -> flight
+ if (state == States::TAKEOFF && sprite(state).animation_ended(timestamp))
+ {
+ set_state(States::FLIGHT);
+ }
+
+ // landing -> rest
+ if (state == States::LANDING && sprite(state).animation_ended(timestamp))
+ {
+ set_state(States::REST);
+ }
+
+ // put free falling sprite if no ground under the feet
+ if (state != States::FLIGHT &&
+ map.is_empty(pos.x / map.tile_size.w, pos.y / map.tile_size.h + 1))
+ {
+ set_state(States::FALL);
+ }
+
+ // prior to collision detection
+ pos.x += dt * speed.x;
+
+ // horizontal collision detection
+ if (!map.is_empty(pos.x / map.tile_size.w, pos.y / map.tile_size.h))
+ {
+ // snap the coorinate to the boundary of last free tile
+ int snap = std::round(pos.x / map.tile_size.w) * map.tile_size.w;
+
+ // be careful to snap to the left or to the right side of the free tile
+ pos.x = snap + (snap > pos.x ? 1 : -1);
+ // stop
+ speed.x = 0;
+ }
+
+ // prior to collision detection
+ pos.y += dt * speed.y;
+ // gravity
+ speed.y += dt * 300;
+
+ // vertical collision detection
+ if (!map.is_empty(pos.x / map.tile_size.w, pos.y / map.tile_size.h))
+ {
+ // snap the coorinate to the boundary of last free tile
+ int snap = std::round(pos.y / map.tile_size.h) * map.tile_size.h;
+
+ // be careful to snap to the top or the bottom side of the free tile
+ pos.y = snap + (snap > pos.y ? 1 : -1);
+ // stop
+ speed.y = 0;
+
+ if (state == States::FLIGHT || state == States::FALL)
+ {
+ set_state(States::LANDING);
+ }
+ }
+}
+
+void Player::draw()
+{
+ const SDL_Rect src{sprite(state).rect(timestamp)};
+ const SDL_Rect dest{
+ static_cast<int>(pos.x) - sprite_size.w / 2,
+ static_cast<int>(pos.y) - sprite_size.h,
+ sprite_size.w,
+ sprite_size.h
+ };
+
+ SDL_RenderCopyEx(
+ renderer,
+ sprite(state).texture,
+ &src,
+ &dest,
+ 0,
+ nullptr,
+ backwards ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE
+ );
+}
diff --git a/core/src/cpp/player.h b/core/src/cpp/player.h
new file mode 100644
index 0000000..f6837bb
--- /dev/null
+++ b/core/src/cpp/player.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "animation.h"
+#include "map.h"
+#include "size2d.h"
+#include "vec2d.h"
+
+struct SDL_Renderer;
+
+struct Player
+{
+ enum class States
+ {
+ REST = 0,
+ TAKEOFF = 1,
+ FLIGHT = 2,
+ LANDING = 3,
+ WALK = 4,
+ FALL = 5
+ };
+
+ explicit Player(SDL_Renderer* renderer);
+
+ void set_state(const States& s);
+
+ void handle_keyboard();
+
+ const Animation& sprite(const States& state);
+
+ void update_state(const double dt, const Map& map);
+
+ void draw();
+
+ // coordinates of the character
+ vec2d<double> pos{150., 200.};
+ // speed of the character
+ vec2d<double> speed{0., 0.};
+
+ /*!
+ * \brief backwards - facing left or right
+ */
+ bool backwards{false};
+ // will be used to differentiate high jump from a long jump
+ vec2d<double> jump{0., 0.};
+
+ /*!
+ * \brief state - current sprite
+ */
+ States state{States::REST};
+ TimeStamp timestamp{Clock::now()};
+ /*!
+ * \brief renderer - draw here
+ */
+ SDL_Renderer* renderer{nullptr};
+
+ // size of the sprite on the screen
+ size2d<int> sprite_size{256, 128};
+
+ /*!
+ * \brief sprites - sprite sequences to be drawn
+ */
+ const std::array<Animation, 6> sprites;
+};
diff --git a/core/src/cpp/size2d.h b/core/src/cpp/size2d.h
new file mode 100644
index 0000000..1d12307
--- /dev/null
+++ b/core/src/cpp/size2d.h
@@ -0,0 +1,15 @@
+#pragma once
+
+template <typename T = int> struct size2d
+{
+ T w{0};
+ T h{0};
+
+ inline size2d<T> operator/(const size2d<T>& other);
+};
+
+template <typename T>
+inline size2d<T> size2d<T>::operator/(const size2d<T>& other)
+{
+ return {w / other.w, h / other.h};
+}
diff --git a/core/src/cpp/sprite.cpp b/core/src/cpp/sprite.cpp
new file mode 100644
index 0000000..afd633d
--- /dev/null
+++ b/core/src/cpp/sprite.cpp
@@ -0,0 +1,49 @@
+#include "sprite.h"
+
+#include <iostream>
+
+#include <SDL_render.h>
+#include <SDL_surface.h>
+
+Sprite::Sprite(
+ SDL_Renderer* renderer,
+ const std::string& filename,
+ const int width
+)
+ : width{width}
+{
+ SDL_Surface* surface =
+ SDL_LoadBMP((std::string(RESOURCES_DIR) + filename).c_str());
+
+ if (!surface)
+ {
+ std::cerr << "Error in SDL_LoadBMP: " << SDL_GetError() << std::endl;
+ return;
+ }
+
+ if (!(surface->w % width) && surface->w / width)
+ { // image width must be a multiple of sprite width
+ height = surface->h;
+ nframes = surface->w / width;
+ texture = SDL_CreateTextureFromSurface(renderer, surface);
+ }
+ else
+ {
+ std::cerr << "Incorrect sprite size" << std::endl;
+ }
+
+ SDL_FreeSurface(surface);
+}
+
+Sprite::~Sprite()
+{
+ if (texture)
+ {
+ SDL_DestroyTexture(texture);
+ }
+}
+
+SDL_Rect Sprite::rect(const int idx) const
+{
+ return {idx * width, 0, width, height};
+}
diff --git a/core/src/cpp/sprite.h b/core/src/cpp/sprite.h
new file mode 100644
index 0000000..8c60060
--- /dev/null
+++ b/core/src/cpp/sprite.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <chrono>
+#include <string>
+
+#include <SDL_rect.h>
+
+using Clock = std::chrono::high_resolution_clock;
+using TimeStamp = std::chrono::time_point<Clock>;
+
+struct SDL_Texture;
+struct SDL_Renderer;
+
+struct Sprite {
+ explicit Sprite(
+ SDL_Renderer* renderer,
+ const std::string& filename,
+ const int width
+ );
+
+ virtual ~Sprite();
+
+public:
+ /*!
+ * \brief rect - choose the sprite rect from the texture by index
+ * \param idx - index of sprite
+ * \return
+ */
+ SDL_Rect rect(const int idx) const;
+
+public:
+ /*!
+ * \brief texture - the image is to be stored here
+ */
+ SDL_Texture* texture{nullptr};
+ /*!
+ * \brief width - single sprite width (texture width = width * nframes)
+ */
+ int width{0};
+ /*!
+ * \brief height - sprite height
+ */
+ int height{0};
+ /*!
+ * \brief nframes - number of frames in the animation sequence
+ */
+ int nframes{0};
+};
diff --git a/core/src/cpp/vec2d.h b/core/src/cpp/vec2d.h
new file mode 100644
index 0000000..9dc1e39
--- /dev/null
+++ b/core/src/cpp/vec2d.h
@@ -0,0 +1,7 @@
+#pragma once
+
+template <typename T = double> struct vec2d
+{
+ T x{0};
+ T y{0};
+};