Basic inventory system working and can pick up items but needs to be reflected in the UI next.

This commit is contained in:
Zed A. Shaw 2025-01-03 13:41:57 -05:00
parent d7353a02df
commit 135d9a128b
14 changed files with 212 additions and 48 deletions

View file

@ -1,22 +1,34 @@
{ {
"TORCH": { "TORCH_BAD": {
"id": "TORCH_BAD",
"name": "Crappy Torch",
"foreground": [24, 205, 189], "foreground": [24, 205, 189],
"background": [230, 20, 120], "background": [230, 20, 120],
"description": "Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. Summus brains sit, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris. Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat cerebella viventium.",
"display": "\u0f08" "display": "\u0f08"
}, },
"SWORD": { "SWORD_RUSTY": {
"id": "SWORD_RUSTY",
"name": "Rusty Junk Sword",
"foreground": [24, 205, 189], "foreground": [24, 205, 189],
"background": [24, 205, 189], "background": [24, 205, 189],
"description": "Shoreditch pickled readymade tousled tumeric. Chicharrones same jawn irony woke echo park jianbing artisan ethical praxis grailed portland. Banjo solarpunk yes plz, offal Brooklyn beard bushwick letterpress celiac sartorial.",
"display":"\u1e37" "display":"\u1e37"
}, },
"CHEST": { "CHEST_SMALL": {
"id": "CHEST_SMALL",
"name": "Small Chest",
"foreground": [24, 205, 189], "foreground": [24, 205, 189],
"background": [24, 205, 189], "background": [24, 205, 189],
"display":"\uaaea" "display":"\uaaea",
"description": "Tote bag sustainable crucifix gentrify kombucha. Try-hard single-origin coffee meh pork belly cliche aesthetic scenester disrupt banjo af."
}, },
"WALL_TORCH": { "WALL_TORCH": {
"id": "WALL_TORCH",
"name": "Basic Wall Torch",
"foreground": [24, 205, 189], "foreground": [24, 205, 189],
"background": [24, 205, 189], "background": [24, 205, 189],
"description": "A torch on a wall you can't pick up.",
"display": "☀" "display": "☀"
} }
} }

View file

@ -2,6 +2,7 @@
#include "dinkyecs.hpp" #include "dinkyecs.hpp"
#include "map.hpp" #include "map.hpp"
#include "combat.hpp" #include "combat.hpp"
#include "inventory.hpp"
#include <deque> #include <deque>
#include "tser.hpp" #include "tser.hpp"
@ -27,12 +28,6 @@ namespace components {
DEFINE_SERIALIZABLE(Loot, amount); DEFINE_SERIALIZABLE(Loot, amount);
}; };
struct Inventory {
int gold;
LightSource light;
DEFINE_SERIALIZABLE(Inventory, gold, light);
};
struct Tile { struct Tile {
std::string chr; std::string chr;
DEFINE_SERIALIZABLE(Tile, chr); DEFINE_SERIALIZABLE(Tile, chr);

View file

@ -240,11 +240,10 @@ void GUI::handle_world_events() {
} }
} break; } break;
case eGUI::LOOT: { case eGUI::LOOT: {
auto &loot = std::any_cast<Loot&>(data); auto &item = std::any_cast<InventoryItem&>(data);
auto inventory = $world.get<Inventory>(player.entity); auto &inventory = $world.get<Inventory>(player.entity);
fmt::println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!! UPDATE INVENTORY HERE.");
$sounds.play("loot_gold"); $sounds.play("loot_gold");
$status_ui.log(format("You found {} gold. You have {} now.",
loot.amount, inventory.gold));
} }
break; break;
default: default:

32
inventory.cpp Normal file
View file

@ -0,0 +1,32 @@
#include "inventory.hpp"
namespace components {
void Inventory::add(InventoryItem item) {
std::string id = item.data["id"];
if(items.contains(id)) {
auto &slot = items[id];
slot.count += item.count;
} else {
items[id] = item;
}
}
InventoryItem& Inventory::get(std::string id) {
dbc::check(items.contains(id), fmt::format("item id {} is not in inventory", id));
return items[id];
}
bool Inventory::decrease(std::string id, int count) {
dbc::check(items.contains(id), fmt::format("item id {} is not in inventory", id));
auto &slot = items[id];
slot.count -= count;
return slot.count > 0;
}
void Inventory::remove_all(std::string id) {
dbc::check(items.contains(id), fmt::format("item id {} is not in inventory", id));
items.erase(id);
}
}

31
inventory.hpp Normal file
View file

@ -0,0 +1,31 @@
#pragma once
#include "lights.hpp"
#include <nlohmann/json.hpp>
#include <fmt/core.h>
namespace components {
using namespace nlohmann;
using lighting::LightSource;
struct InventoryItem {
int count;
json data;
};
struct Inventory {
int gold;
LightSource light;
std::unordered_map<std::string, InventoryItem> items;
size_t count() { return items.size(); }
void add(InventoryItem item);
bool decrease(std::string id, int count);
InventoryItem& get(std::string id);
void remove_all(std::string id);
};
}

View file

@ -13,6 +13,7 @@
#include "ftxui/screen/terminal.hpp" // for SetColorSupport, Color, TrueColor #include "ftxui/screen/terminal.hpp" // for SetColorSupport, Color, TrueColor
#include <filesystem> #include <filesystem>
#include <fcntl.h> #include <fcntl.h>
#include <fmt/core.h>
using namespace ftxui; using namespace ftxui;
using namespace components; using namespace components;
@ -33,8 +34,15 @@ void configure_world(DinkyECS::World &world, Map &game_map) {
world.set<Motion>(player.entity, {0, 0}); world.set<Motion>(player.entity, {0, 0});
world.set<Combat>(player.entity, {100, 10}); world.set<Combat>(player.entity, {100, 10});
world.set<Tile>(player.entity, {config.enemies["PLAYER_TILE"]["display"]}); world.set<Tile>(player.entity, {config.enemies["PLAYER_TILE"]["display"]});
world.set<Inventory>(player.entity, {5});
world.set<LightSource>(player.entity, {70,1.0}); world.set<LightSource>(player.entity, {70,1.0});
world.set<Inventory>(player.entity, {5});
auto sword = world.entity();
auto pos = game_map.place_entity(1);
world.set<Position>(sword, {pos.x+1, pos.y+1});
world.set<Tile>(sword, {config.items["SWORD_RUSTY"]["display"]});
world.set<InventoryItem>(sword, {1, config.items["SWORD_RUSTY"]});
world.set<Weapon>(sword, {20});
auto enemy = world.entity(); auto enemy = world.entity();
world.set<Position>(enemy, {game_map.place_entity(1)}); world.set<Position>(enemy, {game_map.place_entity(1)});
@ -52,26 +60,12 @@ void configure_world(DinkyECS::World &world, Map &game_map) {
auto gold = world.entity(); auto gold = world.entity();
world.set<Position>(gold, {game_map.place_entity(3)}); world.set<Position>(gold, {game_map.place_entity(3)});
world.set<Loot>(gold, {100}); world.set<Loot>(gold, {100});
world.set<Tile>(gold, {config.items["CHEST"]["display"]}); world.set<Tile>(gold, {config.items["CHEST_SMALL"]["display"]});
auto wall_torch = world.entity(); auto wall_torch = world.entity();
world.set<Position>(wall_torch, {game_map.place_entity(4)}); world.set<Position>(wall_torch, {game_map.place_entity(4)});
world.set<LightSource>(wall_torch, {90,3.0f}); world.set<LightSource>(wall_torch, {90,3.0f});
world.set<Tile>(wall_torch, {config.items["WALL_TORCH"]["display"]}); world.set<Tile>(wall_torch, {config.items["WALL_TORCH"]["display"]});
auto torch = world.entity();
Point at = game_map.place_entity(2);
world.set<Position>(torch, {{at.x+1, at.y+1}});
world.set<Loot>(torch, {{0}});
world.set<LightSource>(torch, {70,1.5f});
world.set<Tile>(torch, {config.items["TORCH"]["display"]});
auto sword = world.entity();
at = game_map.place_entity(1);
world.set<Position>(sword, {at.x+1, at.y+1});
world.set<Weapon>(sword, {.damage=20});
world.set<Loot>(sword, {{0}});
world.set<Tile>(sword, {config.items["SWORD"]["display"]});
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {

View file

@ -11,8 +11,7 @@ sfml = dependency('sfml')
freetype2 = dependency('freetype2') freetype2 = dependency('freetype2')
dependencies = [ dependencies = [
catch2, fmt, fmt, ftxui_screen, ftxui_dom,
ftxui_screen, ftxui_dom,
ftxui_component, json, ftxui_component, json,
sfml, freetype2 sfml, freetype2
] ]
@ -36,6 +35,7 @@ runtests = executable('runtests', [
'systems.cpp', 'systems.cpp',
'gui.cpp', 'gui.cpp',
'worldbuilder.cpp', 'worldbuilder.cpp',
'inventory.cpp',
'tests/tilemap.cpp', 'tests/tilemap.cpp',
'tests/matrix.cpp', 'tests/matrix.cpp',
'tests/fsm.cpp', 'tests/fsm.cpp',
@ -54,8 +54,9 @@ runtests = executable('runtests', [
'tests/lighting.cpp', 'tests/lighting.cpp',
'tests/gui.cpp', 'tests/gui.cpp',
'tests/worldbuilder.cpp', 'tests/worldbuilder.cpp',
'tests/inventory.cpp',
], ],
dependencies: dependencies) dependencies: dependencies + catch2)
roguish = executable('roguish', [ roguish = executable('roguish', [
'dbc.cpp', 'dbc.cpp',
@ -77,6 +78,7 @@ roguish = executable('roguish', [
'pathing.cpp', 'pathing.cpp',
'lights.cpp', 'lights.cpp',
'worldbuilder.cpp', 'worldbuilder.cpp',
'inventory.cpp',
], ],
dependencies: dependencies) dependencies: dependencies)

View file

@ -30,7 +30,7 @@ void save::to_file(fs::path path, DinkyECS::World &world, Map &map) {
extract<Combat>(world, save_data.combat); extract<Combat>(world, save_data.combat);
extract<Motion>(world, save_data.motion); extract<Motion>(world, save_data.motion);
extract<Tile>(world, save_data.tile); extract<Tile>(world, save_data.tile);
extract<Inventory>(world, save_data.inventory); // extract<Inventory>(world, save_data.inventory);
archive.save(save_data); archive.save(save_data);
std::string_view archive_view = archive.get_buffer(); std::string_view archive_view = archive.get_buffer();
@ -72,7 +72,7 @@ void save::from_file(fs::path path, DinkyECS::World &world_out, Map &map_out) {
inject<Combat>(world_out, save_data.combat); inject<Combat>(world_out, save_data.combat);
inject<Motion>(world_out, save_data.motion); inject<Motion>(world_out, save_data.motion);
inject<Tile>(world_out, save_data.tile); inject<Tile>(world_out, save_data.tile);
inject<Inventory>(world_out, save_data.inventory); // inject<Inventory>(world_out, save_data.inventory);
size_t width = save_data.map.width; size_t width = save_data.map.width;
size_t height = save_data.map.height; size_t height = save_data.map.height;

View file

@ -33,9 +33,9 @@ namespace save {
std::map<DinkyECS::Entity, components::Motion> motion; std::map<DinkyECS::Entity, components::Motion> motion;
std::map<DinkyECS::Entity, components::Combat> combat; std::map<DinkyECS::Entity, components::Combat> combat;
std::map<DinkyECS::Entity, components::Tile> tile; std::map<DinkyECS::Entity, components::Tile> tile;
std::map<DinkyECS::Entity, components::Inventory> inventory; // std::map<DinkyECS::Entity, components::Inventory> inventory;
DEFINE_SERIALIZABLE(SaveData, facts, map, position, motion, combat, tile, inventory); DEFINE_SERIALIZABLE(SaveData, facts, map, position, motion, combat, tile);
}; };
void to_file(fs::path path, DinkyECS::World &world, Map &map); void to_file(fs::path path, DinkyECS::World &world, Map &map);

View file

@ -1,5 +1,7 @@
TODAY'S GOAL: TODAY'S GOAL:
* Make Map::place_entity handle entity overlap and also walls.
* Config loader should setup the "id" based on the key to avoid errors.
* Colision fails when you place two entities on the same square, but the init_positions adds them and one deletes the other. * Colision fails when you place two entities on the same square, but the init_positions adds them and one deletes the other.
* Config needs to do asserts that the key exists * Config needs to do asserts that the key exists
* Create a move function for iterators that recalculates their position to make it easy to move them inside the matrix. This can then be used in lighting. Just make an iterator once, and move it around after. * Create a move function for iterators that recalculates their position to make it easy to move them inside the matrix. This can then be used in lighting. Just make an iterator once, and move it around after.

View file

@ -62,7 +62,7 @@ void System::init_positions(DinkyECS::World &world) {
} }
}); });
world.query<Position, Loot>([&](const auto &ent, auto &pos, auto &loot) { world.query<Position, InventoryItem>([&](const auto &ent, auto &pos, auto &item) {
collider.insert(pos.location, ent); collider.insert(pos.location, ent);
}); });
} }
@ -134,10 +134,12 @@ void System::collision(DinkyECS::World &world, Player &player) {
}; };
world.send<Events::GUI>(Events::GUI::COMBAT, entity, result); world.send<Events::GUI>(Events::GUI::COMBAT, entity, result);
} else if(world.has<Loot>(entity)) { } else if(world.has<InventoryItem>(entity)) {
auto loot = world.get<Loot>(entity); auto& item = world.get<InventoryItem>(entity);
auto &loot_pos = world.get<Position>(entity); auto& item_pos = world.get<Position>(entity);
auto &inventory = world.get<Inventory>(player.entity); auto& inventory = world.get<Inventory>(player.entity);
inventory.add(item);
if(world.has<LightSource>(entity)) { if(world.has<LightSource>(entity)) {
auto &new_light = world.get<LightSource>(entity); auto &new_light = world.get<LightSource>(entity);
@ -148,15 +150,12 @@ void System::collision(DinkyECS::World &world, Player &player) {
auto &weapon = world.get<Weapon>(entity); auto &weapon = world.get<Weapon>(entity);
player_combat.damage = weapon.damage; player_combat.damage = weapon.damage;
world.remove<Weapon>(entity); world.remove<Weapon>(entity);
} else {
// it's just gold
inventory.gold += loot.amount;
} }
collider.remove(loot_pos.location); collider.remove(item_pos.location);
world.remove<Tile>(entity); world.remove<Tile>(entity);
world.remove<Loot>(entity); world.remove<InventoryItem>(entity);
world.send<Events::GUI>(Events::GUI::LOOT, entity, loot); world.send<Events::GUI>(Events::GUI::LOOT, entity, item);
} else { } else {
println("UNKNOWN COLLISION TYPE {}", entity); println("UNKNOWN COLLISION TYPE {}", entity);
} }
@ -183,3 +182,10 @@ void System::draw_entities(DinkyECS::World &world, Map &game_map, const Matrix &
} }
}); });
} }
void System::pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item) {
auto& inventory = world.get<Inventory>(actor);
auto& invitem = world.get<InventoryItem>(item);
inventory.add(invitem);
}

View file

@ -16,4 +16,5 @@ namespace System {
void enemy_pathing(DinkyECS::World &world, Map &game_map, Player &player); void enemy_pathing(DinkyECS::World &world, Map &game_map, Player &player);
void draw_entities(DinkyECS::World &world, Map &game_map, const Matrix &lighting, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y); void draw_entities(DinkyECS::World &world, Map &game_map, const Matrix &lighting, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y);
void init_positions(DinkyECS::World &world); void init_positions(DinkyECS::World &world);
void pickup(DinkyECS::World &world, DinkyECS::Entity actor, DinkyECS::Entity item);
} }

61
tests/inventory.cpp Normal file
View file

@ -0,0 +1,61 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "rand.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#include "components.hpp"
#include "dinkyecs.hpp"
#include "save.hpp"
#include "systems.hpp"
using namespace nlohmann;
using namespace fmt;
using std::string;
using namespace components;
DinkyECS::Entity add_items(DinkyECS::World &world, GameConfig &config) {
auto sword = world.entity();
world.set<InventoryItem>(sword, {1, config.items["SWORD_RUSTY"]});
world.set<Tile>(sword, {config.items["SWORD_RUSTY"]["display"]});
return sword;
}
TEST_CASE("basic inventory test", "[inventory]") {
DinkyECS::World world;
save::load_configs(world);
auto& config = world.get_the<GameConfig>();
auto sword = add_items(world, config);
auto player = world.entity();
world.set<Inventory>(player, {});
auto &inventory = world.get<Inventory>(player);
System::pickup(world, player, sword);
REQUIRE(inventory.count() == 1);
// get the item and confirm there is 1
auto &item1 = inventory.get("SWORD_RUSTY");
REQUIRE(item1.count == 1);
System::pickup(world, player, sword);
System::pickup(world, player, sword);
System::pickup(world, player, sword);
REQUIRE(inventory.count() == 1);
REQUIRE(item1.count == 4);
inventory.decrease("SWORD_RUSTY", 1);
REQUIRE(item1.count == 3);
inventory.decrease("SWORD_RUSTY", 2);
REQUIRE(item1.count == 1);
bool active = inventory.decrease("SWORD_RUSTY", 1);
REQUIRE(item1.count == 0);
REQUIRE(active == false);
inventory.remove_all("SWORD_RUSTY");
REQUIRE(inventory.count() == 0);
}

View file

@ -252,6 +252,7 @@ TEST_CASE("prototype circle algorithm", "[matrix:circle]") {
size_t height = Random::uniform<size_t>(10, 15); size_t height = Random::uniform<size_t>(10, 15);
int pos_mod = Random::uniform<int>(-3,3); int pos_mod = Random::uniform<int>(-3,3);
Map map(width,height); Map map(width,height);
// create a target for the paths // create a target for the paths
Point start{.x=map.width() / 2 + pos_mod, .y=map.height()/2 + pos_mod}; Point start{.x=map.width() / 2 + pos_mod, .y=map.height()/2 + pos_mod};
@ -275,3 +276,31 @@ TEST_CASE("prototype circle algorithm", "[matrix:circle]") {
} }
} }
} }
TEST_CASE("viewport iterator", "[matrix:viewport]") {
size_t width = Random::uniform<size_t>(20, 22);
size_t height = Random::uniform<size_t>(21, 25);
Map map(width,height);
WorldBuilder builder(map);
builder.generate();
size_t view_width = width/2;
size_t view_height = height/2;
Point player = map.place_entity(1);
Point start = map.center_camera(player, view_width, view_height);
size_t end_x = std::min(view_width, map.width() - start.x);
size_t end_y = std::min(view_height, map.height() - start.y);
matrix::viewport it{map.walls(), start, int(view_width), int(view_height)};
for(size_t y = 0; y < end_y; ++y) {
for(size_t x = 0; x < end_x && it.next(); ++x) {
println("view x/y={},{}; w/h={},{}; start={},{}",
it.x, it.y, it.width, it.height, it.start.x, it.start.y);
println("orig x/y={},{}; w/h={},{}; start={},{}\n",
x+start.x, y+start.y, view_width, view_height, start.x, start.y);
}
}
}