Nimbin[12]?SDL & Graphics / mapcreator / src/inventory/inventory.cpp

mapcreator git · master

SDL3 2.5D game and engine using assets, with map editor

sdl3 c++ game engine map-editor cmake · first commit 2025-10-31 · last commit 2026-09-07 (1 month ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-spieleentwicklung/map_creator

C++ 99.3%
git clone https://git.christianimmanuel.de/sdl-graphics/mapcreator.gitwget https://git.christianimmanuel.de/sdl-graphics/mapcreator/archive/mapcreator.tar.gz
src/inventory/inventory.cpp 42.8 KB · 1340 lines raw
#include "inventory.hpp"

#include <string>
#include <sstream>
#include <iomanip>

#include "defaults/icons.hpp"
#include "map/tile.hpp"
#include "defaults/ui.hpp"
#include "character/message.hpp"

static SDL_Color MENU_HOVER_BORDER_C = Default::menu_hovering_color;

static constexpr float INFO_PANEL_WIDTH = 180;
static constexpr float MENU_BORDER = 4.f;
static SDL_FRect menu_inner = {0,0, 0, 0};
 
float inventory_grid_ts          = 48.f;
float inventory_grid_tile_border = 2.f;
float inventory_grid_tile_gap    = 4.f;
float inventory_grid_slot_size   = inventory_grid_ts + inventory_grid_tile_border * 2 + inventory_grid_tile_gap;

int inventory_grid_cols = 5;
int inventory_grid_rows = 5;

float build_menu_width = 64.f;
int   build_menu_rows  = 5;
 


bool Inventory::draw_menu = false;
bool Inventory::clicked_button_craft = false;
int  Inventory::selected_hotbar      = 0;
int  Inventory::selected_hotbar_last = 0;

Equipement     Inventory::hotbar[HOTBAR_SLOTS];
InventoryItem* Inventory::hotbar_hands = nullptr;

ItemType Inventory::selected_build_item = ItemType::NONE;
InventorySelection* Inventory::selected_item = nullptr;




Inventory::Inventory() {
    std::cout << "CREATE INVENTORY!\n";

    selected_hotbar = 0;
    selected_hotbar_last = 0;
    draw_menu = false;
    clicked_button_craft = false;

    selected_build_item = ItemType::NONE;

    hotbar_hands = new InventoryItem(0, ItemType::HAND);
    next_item_id = 1;
    Inventory::selected_item = nullptr;

    for (int i = 1; i < HOTBAR_SLOTS; i++) {
        Inventory::hotbar[i].item_id = 0;
        Inventory::hotbar[i].type    = InventoryObjectType::ITEM;
    }
}


Inventory::Inventory(Inventory&&) {
    std::cout << "[Inventory] Move constructor called!" << std::endl;
}


// EQUIPEMENT

void Inventory::Equip(InventoryItem* item) {
    equipment[item->equipement_slot].item_id = item->id;
    equipment[item->equipement_slot].type    = InventoryObjectType::ITEM;
}

void Inventory::Equip(InventoryVegetable* item) {
    equipment[item->equipement_slot].item_id = item->id;
    equipment[item->equipement_slot].type    = InventoryObjectType::VEGETABLE;
}

void Inventory::Unequip(EquipmentSlot slot) {
    equipment.erase(slot);
}

// VEGETABLE
void
Inventory::add_Vegetable(VegetableData::Type type, size_t amount, Messages* messages) {
    auto [it, inserted] = vegetables.try_emplace(type, InventoryVegetable(next_item_id, type, false));
    if (inserted)
        next_item_id++;

    it->second.count += amount;

    auto* t = new VegetableData::Type(type);
    std::cout << "ADD VEGETABLE TYPE: " << static_cast<int>(*t) << std::endl;
    if (messages)
        messages->Add(MessageKey::ADD_VEGETABLE, static_cast<void*>(t));
}

int
Inventory::remove_Vegetable(VegetableData::Type type, size_t amount) {
    auto it = vegetables.find(type);
    int ret = -1;
    if (it == vegetables.end() || it->second.count < amount)
        return ret;

    it->second.count -= amount;
    ret = it->second.count;
    if (it->second.count == 0) {
        vegetables.erase(it);
        bool setted_hotbar = set_HotbarItem(Inventory::selected_hotbar, Inventory::hotbar_hands);
        if (setted_hotbar)
            Equip(Inventory::hotbar_hands);
    }
    return ret;
}

int
Inventory::get_Vegetable(VegetableData::Type type) const {
    auto it = vegetables.find(type);
    return (it != vegetables.end()) ? it->second.count : 0;
}

// SEED
void
Inventory::add_Seed(VegetableData::Type type, size_t amount, Messages* messages) {
    auto [it, inserted] = seeds.try_emplace(type, InventoryVegetable(next_item_id, type, true));
    if (inserted)
        next_item_id++;

    it->second.count += amount;

    auto* t = new VegetableData::Type(type);
    std::cout << "ADD VEGETABLE TYPE: " << static_cast<int>(*t) << std::endl;
    if (messages)
        messages->Add(MessageKey::ADD_VEGETABLE_SEED, static_cast<void*>(t));
}

bool
Inventory::remove_Seed(VegetableData::Type type, size_t amount) {
    std::cout << "TSCHÖÖ\n";
    auto it = seeds.find(type);
    if (it == seeds.end() || it->second.count < amount)
        return false;

    it->second.count -= amount;
    if (it->second.count == 0) {
        seeds.erase(it);
        bool setted_hotbar = set_HotbarItem(Inventory::selected_hotbar, Inventory::hotbar_hands);
        if (setted_hotbar)
            Equip(Inventory::hotbar_hands);
    }
    return true;
}

int
Inventory::get_Seed(VegetableData::Type type) const {
    auto it = seeds.find(type);
    return (it != seeds.end()) ? it->second.count : 0;
}

// RESOURCE
void
Inventory::add_Resource(ResourceType type, int amount, Messages* messages) {
    if (type == ResourceType::NONE || amount <= 0)
        return;

    resources[type] += amount;

    ResourceType* t = new ResourceType(type);
    std::cout << "ADD RESOURCE TYPE: "<< static_cast<int>(*t) << std::endl;
    messages->Add(MessageKey::ADD_RESOURCE, static_cast<void*>(t));
}
bool
Inventory::remove_Resource(ResourceType type, int amount) {
    auto it = resources.find(type);
    if (it == resources.end() || it->second < amount)
        return false;

    it->second -= amount;
    if (it->second == 0)
        resources.erase(it);
    return true;
}

int
Inventory::get_Resource(ResourceType type) const {
    auto it = resources.find(type);
    return (it != resources.end()) ? it->second : 0;
}


// ITEM
InventoryItem*
Inventory::add_Item(ItemType item, Messages* messages) {
    if (item == ItemType::NONE) return nullptr;

    size_t* id = new size_t(next_item_id++);
    std::cout << "INV ID: " << *id << " " << next_item_id << std::endl;
    items.emplace_back(std::make_unique<InventoryItem>(*id, item));
    if (messages) {
        messages->Add(MessageKey::ADD_INVENTORY, static_cast<void*>(id));
    }



    std::cout << "ADD ITEM MESS\n";
    std::cout << items.size() << "  type: " << Item::to_String(items.back()->type) << std::endl;
    return items.back().get();
}

bool
Inventory::remove_Item(ItemType item) {
    for (auto it = items.begin(); it != items.end(); ++it) {
        if ((*it)->type == item) {
            items.erase(it);
            return true;
        }
    }
    return false;
}

bool
Inventory::has_Item(ItemType item) const {
    for (auto& it : items)
        if (it->type == item)
            return true;
    return false;
}


// DRAW
void
Inventory::draw_BuildMenu(SDL_Renderer* renderer, std::vector<bool> researched_items) {
    const float ts = inventory_grid_ts;
    const float gap = inventory_grid_tile_gap;
    const float border = inventory_grid_tile_border;

    const float column_x = menu_inner.x + menu_inner.w - build_menu_width;
    const float column_w = build_menu_width;
    const float column_h = menu_inner.h;

    SDL_FRect bg{ column_x, menu_inner.y, column_w, column_h };
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderFillRect(renderer, &bg);

    const float icon_x = column_x + (column_w - ts) * 0.5f;

    auto draw_slot = [&](float y, ItemType item) {
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        SDL_FRect slot_bg{ icon_x, y, ts, ts };
        SDL_RenderFillRect(renderer, &slot_bg);

        if (item != ItemType::NONE) {
            auto it = Icon::item_pos.find(item);
            if (it != Icon::item_pos.end()) {
                SDL_FRect src{ it->second.x * 16.f, it->second.y * 16.f, 16.f, 16.f };
                SDL_FRect dst{ icon_x, y, ts, ts };
                SDL_RenderTexture(renderer, Icon::icons_texture, &src, &dst);
            }
        }

        if (item != ItemType::NONE && item == selected_build_item)
            SDL_SetRenderDrawColor(renderer, 40, 140, 40, 255);
        else
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);

        SDL_FRect border_rect{
            icon_x - border, y - border,
            ts + border * 2, ts + border * 2
        };
        SDL_RenderRect(renderer, &border_rect);
    };

    // row 0
    draw_slot(menu_inner.y + 0 * (ts + gap), ItemType::BUILDING_TOOL);
    if (researched_items[static_cast<int>(ItemType::SWORD_WOOD)])
        draw_slot(menu_inner.y + 1 * (ts + gap), ItemType::SWORD_WOOD);
    else
        draw_slot(menu_inner.y + 1 * (ts + gap), ItemType::NONE);

    // rows not set jet
    for (int i = 2; i < build_menu_rows; i++)
        draw_slot(menu_inner.y + i * (ts + gap), ItemType::NONE);
}

ItemType
Inventory::hit_BuildMenu(const SDL_FPoint& mouse, SDL_Rect, std::vector<bool> researched_items) {
    const float ts = inventory_grid_ts;
    const float gap = inventory_grid_tile_gap;

    const float column_x = menu_inner.x + menu_inner.w - build_menu_width;
    const float column_w = build_menu_width;

    const float icon_x = column_x + (column_w - ts) * 0.5f;
    const float icon_x_end = icon_x + ts;

    if (mouse.x < icon_x || mouse.x > icon_x_end)
        return ItemType::NONE;

    float rel_y = mouse.y - menu_inner.y;
    if (rel_y < 0)
        return ItemType::NONE;

    int row = (int)(rel_y / (ts + gap));
    if (row < 0 || row >= build_menu_rows)
        return ItemType::NONE;

    switch (row) {
        case 0:
            return ItemType::BUILDING_TOOL;
        case 1:
            if (researched_items[static_cast<size_t>(ItemType::SWORD_WOOD)])
                return ItemType::SWORD_WOOD;
            return ItemType::NONE;
        default:
            return ItemType::NONE;
    }
}

bool
Inventory::cursor_OverCraftButton(const SDL_FPoint& mouse) {
    const float panel_w = INFO_PANEL_WIDTH;
    const float panel_h = menu_inner.h;
    const float panel_x = menu_inner.x + menu_inner.w + 10.f;
    const float panel_y = menu_inner.y;

    const float btn_h = 24.f;
    const float btn_w = panel_w - 12.f;
    const float btn_x = panel_x + 6.f;
    const float btn_y = panel_y + panel_h - btn_h - 6.f;

    // simple AABB check
    return mouse.x >= btn_x && mouse.x <= btn_x + btn_w &&
           mouse.y >= btn_y && mouse.y <= btn_y + btn_h;
}

void
Inventory::draw_InfoPanel(SDL_Renderer* renderer,
                          TTF_Font* font,
                          ItemType /*unused*/,
                          bool is_hovering_craftbutton)
{
    const float panel_w = INFO_PANEL_WIDTH;
    const float panel_h = menu_inner.h;
    const float panel_x = menu_inner.x + menu_inner.w + 10.f;
    const float panel_y = menu_inner.y;
    const float border  = 4.f;

    SDL_SetRenderDrawColor(renderer, 60, 60, 70, 255);
    SDL_FRect panel_border{
        panel_x - border,
        panel_y - border,
        panel_w + border * 2.f,
        panel_h + border * 2.f
    };
    SDL_RenderFillRect(renderer, &panel_border);

    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_FRect panel_bg{ panel_x, panel_y, panel_w, panel_h };
    SDL_RenderFillRect(renderer, &panel_bg);

    const float line_h  = 20.f;
    const float spacing = 4.f;
    float y = panel_y + 6.f;

    auto draw_line = [&](const std::string& text)
    {
        SDL_Color white{255,255,255,255};
        SDL_Surface* surf = TTF_RenderText_Solid(font, text.c_str(), text.length(), white);
        if (!surf) return;

        SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
        if (tex)
        {
            SDL_FRect dst{
                panel_x + 6.f,
                y,
                (float)surf->w,
                (float)surf->h
            };
            SDL_RenderTexture(renderer, tex, nullptr, &dst);
            SDL_DestroyTexture(tex);
        }

        y += line_h + spacing;
        SDL_DestroySurface(surf);
    };

    auto resource_name = [](ResourceType type) -> const std::string&
    {
        size_t idx = static_cast<size_t>(type);
        if (idx < ResourceData::names.size())
            return ResourceData::names[idx];
        return ResourceData::names.back();
    };

    const ItemCrafting& ci = Item::crafting_item;

    // ================================================================
    // BUILD ITEM SELECTED
    // ================================================================
    if (selected_build_item != ItemType::NONE)
    {
        const ItemStats& s = Item::get(selected_build_item);

        const bool craftable =
            (s.crafting_duration > 0.f) &&
            (s.cost.size() != 0);

        draw_line("ITEM:");
        draw_line(s.title);
        y += spacing;

        if (craftable)
        {
            draw_line("COST:");
            for (const auto& c : s.cost)
            {
                const std::string& name = resource_name(c.type);
                draw_line(name + ": " + std::to_string(c.amount));
            }
            y += spacing;
        }

        draw_line("STATS:");

        if (s.base_damage > 0)
            draw_line("Damage: " + std::to_string(s.base_damage));

        if (s.base_armor > 0)
            draw_line("Armor: " + std::to_string(s.base_armor));

        draw_line("Health: " + std::to_string(s.base_health));

        if (s.base_speed > 0.f) {
            std::ostringstream ss;
            ss << std::fixed << std::setprecision(1) << s.base_speed;
            draw_line("Speed: " + ss.str());
        }

        if (s.max_level > 0)
            draw_line("Max Lvl: " + std::to_string(s.max_level));

        if (!craftable)
            return;

        const float btn_h = 24.f;
        const float btn_w = panel_w - 12.f;
        const float btn_x = panel_x + 6.f;
        const float btn_y = panel_y + panel_h - btn_h - 6.f;

        SDL_SetRenderDrawColor(renderer, 40, 140, 40, 255);
        SDL_FRect craft_btn{ btn_x - 1.f, btn_y - 1.f, btn_w + 2.f, btn_h + 2.f };
        SDL_RenderFillRect(renderer, &craft_btn);

        if (is_hovering_craftbutton)
            SDL_SetRenderDrawColor(renderer,
                                   MENU_HOVER_BORDER_C.r,
                                   MENU_HOVER_BORDER_C.g,
                                   MENU_HOVER_BORDER_C.b,
                                   MENU_HOVER_BORDER_C.a);
        else
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);

        SDL_RenderRect(renderer, &craft_btn);

        if (ci.is_crafting && ci.stats.type == s.type)
        {
            float p = ci.duration / s.crafting_duration;
            if (p < 0.f) p = 0.f;
            if (p > 1.f) p = 1.f;

            SDL_SetRenderDrawColor(renderer, 80, 200, 80, 255);
            SDL_FRect fill{ btn_x, btn_y, btn_w * p, btn_h };
            SDL_RenderFillRect(renderer, &fill);
        }

        SDL_Surface* surf =
            TTF_RenderText_Solid(Ui::font_menu_bold, "Craft", 5, SDL_Color{255,255,255,255});

        if (surf)
        {
            SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
            if (tex)
            {
                SDL_FRect dst{
                    btn_x + btn_w * 0.5f - surf->w * 0.5f,
                    btn_y + btn_h * 0.5f - surf->h * 0.5f,
                    (float)surf->w,
                    (float)surf->h
                };
                SDL_RenderTexture(renderer, tex, nullptr, &dst);
                SDL_DestroyTexture(tex);
            }
            SDL_DestroySurface(surf);
        }

        return;
    }

    // ================================================================
    // IF CRAFTING BUT NOTHING SELECTED -> SHOW CRAFTING ITEM
    // ================================================================
    if (ci.is_crafting)
    {
        const ItemStats& s = ci.stats;

        draw_line("CRAFTING:");
        draw_line(s.title);
        y += spacing;

        const float btn_h = 24.f;
        const float btn_w = panel_w - 12.f;
        const float btn_x = panel_x + 6.f;
        const float btn_y = panel_y + panel_h - btn_h - 6.f;

        float p = 0.f;
        if (s.crafting_duration > 0.f)
            p = ci.duration / s.crafting_duration;

        if (p < 0.f) p = 0.f;
        if (p > 1.f) p = 1.f;

        SDL_SetRenderDrawColor(renderer, 80, 200, 80, 255);
        SDL_FRect fill{ btn_x, btn_y, btn_w * p, btn_h };
        SDL_RenderFillRect(renderer, &fill);

        return;
    }

    // ================================================================
    // INVENTORY SELECTION
    // ================================================================
    if (selected_item && selected_item->is_valid)
    {
        draw_line("ITEM:");

        if (selected_item->resource != ResourceType::NONE)
        {
            draw_line(resource_name(selected_item->resource));
            return;
        }

        if (selected_item->vegetable != VegetableData::Type::NONE)
        {
            const auto& def =
                VegetableData::crop_defs[static_cast<uint32_t>(selected_item->vegetable)];
            draw_line(def.name);
            return;
        }

        if (selected_item->seed != VegetableData::Type::NONE)
        {
            const auto& def =
                VegetableData::crop_defs[static_cast<uint32_t>(selected_item->seed)];
            std::string name = std::string(def.name) + " seed";
            draw_line(name);
            return;
        }

        if (selected_item->item)
        {
            auto* inv_item =
                static_cast<InventoryItem*>(selected_item->item);
            const ItemStats& s = Item::get(inv_item->type);
            draw_line(s.title);
            draw_line("STATS:");

            if (s.base_damage > 0)
                draw_line("Damage: " + std::to_string(s.base_damage));

            if (s.base_armor > 0)
                draw_line("Armor: " + std::to_string(s.base_armor));

            draw_line("Health: " + std::to_string(s.base_health));

            if (s.base_speed > 0.f)
                draw_line("Speed: " + std::to_string(s.base_speed));

            if (s.max_level > 0)
                draw_line("Max Lvl: " + std::to_string(s.max_level));
            return;
        }
    }
}

void
Inventory::draw_InventoryGrid(SDL_Renderer* renderer) {
    auto draw_icon = [&](float x, float y, Icon::TilePos pos, int count, bool highlight) {
        const float src_ts = 16.f;

        // border
        if (highlight)
            SDL_SetRenderDrawColor(renderer, MENU_HOVER_BORDER_C.r, MENU_HOVER_BORDER_C.g, MENU_HOVER_BORDER_C.b, MENU_HOVER_BORDER_C.a);
        else
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_FRect b{
            x - inventory_grid_tile_border,
            y - inventory_grid_tile_border,
            inventory_grid_ts + inventory_grid_tile_border * 2,
            inventory_grid_ts + inventory_grid_tile_border * 2
        };
        SDL_RenderFillRect(renderer, &b);


        // infill
        SDL_SetRenderDrawColor(renderer, 60, 60, 70, 255);
        SDL_FRect i{
            x,
            y,
            inventory_grid_ts,
            inventory_grid_ts
        };
        SDL_RenderFillRect(renderer, &i);

        // icon
        SDL_FRect src{ pos.x * src_ts, pos.y * src_ts, src_ts, src_ts };
        SDL_FRect dst{ x, y, inventory_grid_ts, inventory_grid_ts };
        SDL_RenderTexture(renderer, Icon::icons_texture, &src, &dst);

        // text
        if (count > 0) {
            SDL_Color white{255,255,255,255};
            std::string t = std::to_string(count);

            SDL_Surface* surf = TTF_RenderText_Solid(Ui::font_tiny, t.c_str(), t.length(), white);
            if (!surf) return;

            SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
            if (tex) {
                SDL_FRect r{
                    x + inventory_grid_ts - surf->w,
                    y + inventory_grid_ts - surf->h,
                    (float)surf->w,
                    (float)surf->h
                };
                SDL_RenderTexture(renderer, tex, nullptr, &r);
                SDL_DestroyTexture(tex);
            }
            SDL_DestroySurface(surf);
        }
    };

    float inv_left = menu_inner.x;
    float inv_top  = menu_inner.y;

    auto get_tile_position = [&](int idx) {
        int col = idx % inventory_grid_cols;
        int row = idx / inventory_grid_cols;
        float x = inv_left + col * inventory_grid_slot_size + inventory_grid_tile_border;
        float y = inv_top  + row * inventory_grid_slot_size + inventory_grid_tile_border;
        return std::pair<float,float>(x,y);
    };

    int index = 0;

    // RESOURCE
    for (auto& pair : resources) {
        if (pair.second <= 0) continue;

        //std::cout << "ITM: " << (int)pair.first << " " << pair.second << std::endl;
        if (index >= inventory_grid_cols * inventory_grid_rows) break;

        auto it = Icon::resource_pos.find(pair.first);
        if (it == Icon::resource_pos.end()) continue;

        auto pos = get_tile_position(index);
        bool highlight = (selected_inventory.is_valid &&
                          selected_inventory.resource == pair.first);

        draw_icon(pos.first, pos.second, it->second, pair.second, highlight);
        index++;
    }

    // VEGETABLE
    for (auto& pair : vegetables) {
        if (pair.second.count <= 0) continue;

        //std::cout << "ITM: " << (int)pair.first << " " << pair.second << std::endl;
        if (index >= inventory_grid_cols * inventory_grid_rows) break;


        VegetableData::Type type = pair.first;
        const VegetableData::CropDefinition* data = VegetableData::get_DataByType(type);

        auto pos = get_tile_position(index);
        bool highlight = (selected_inventory.is_valid &&
                          selected_inventory.vegetable == pair.first);

        //std::cout << "DAT: " <<data.icon_tile_x << " " <<  data.icon_tile_y << std::endl;
        draw_icon(pos.first, pos.second, {data->icon_tile_x, data->icon_tile_y}, pair.second.count, highlight);
        index++;
    }

    // SEEDS
    for (auto& pair : seeds) {
        if (pair.second.count <= 0) continue;

        //std::cout << "ITM: " << (int)pair.first << " " << pair.second << std::endl;
        if (index >= inventory_grid_cols * inventory_grid_rows) break;


        VegetableData::Type type = pair.first;
        const VegetableData::CropDefinition* data = VegetableData::get_DataByType(type);

        auto pos = get_tile_position(index);
        bool highlight = (selected_inventory.is_valid &&
                          selected_inventory.seed == pair.first);

        //std::cout << "DAT: " <<data.icon_tile_x << " " <<  data.icon_tile_y << std::endl;
        draw_icon(pos.first, pos.second, {data->icon_tile_x-1, data->icon_tile_y}, pair.second.count, highlight);
        index++;
    }

    // ITEM -> TOOL/WEAPON
    for (auto& inv_item : items) {
        if (index >= inventory_grid_cols * inventory_grid_rows) break;

        auto it = Icon::item_pos.find(inv_item->type);
        if (it == Icon::item_pos.end()) continue;

        auto pos = get_tile_position(index);
        bool highlight = (selected_inventory.item &&
                          selected_inventory.is_valid &&
                          selected_inventory.item->id == inv_item->id);

        draw_icon(pos.first, pos.second, it->second, 1, highlight);
        index++;
    }
}

InventorySelection
Inventory::hit_InventoryGrid(const SDL_FPoint& mouse) {
    InventorySelection sel;

    float inv_left = menu_inner.x;
    float inv_top  = menu_inner.y;

    int index = 0;

    auto rect_for_slot = [&](int idx) {
        int col = idx % inventory_grid_cols;
        int row = idx / inventory_grid_cols;

        float x = inv_left + col * inventory_grid_slot_size + inventory_grid_tile_border;
        float y = inv_top  + row * inventory_grid_slot_size + inventory_grid_tile_border;

        return SDL_FRect{ x, y, inventory_grid_ts, inventory_grid_ts };
    };

    for (auto& pair : resources) {
        if (pair.second <= 0) continue;

        if (index >= inventory_grid_cols * inventory_grid_rows) break;

        auto it = Icon::resource_pos.find(pair.first);
        if (it == Icon::resource_pos.end()) continue;

        SDL_FRect r = rect_for_slot(index);

        if (mouse.x >= r.x && mouse.x <= r.x + r.w &&
            mouse.y >= r.y && mouse.y <= r.y + r.h) {
            sel.is_valid  = true;
            sel.resource  = pair.first;
            sel.vegetable = VegetableData::Type::NONE;
            sel.item      = nullptr;
            sel.tile      = it->second;
            return sel;
        }

        index++;
    }
    for (auto& pair : vegetables) {
        if (pair.second.count <= 0) continue;

        if (index >= inventory_grid_cols * inventory_grid_rows) break;


        VegetableData::Type type = pair.first;
        const VegetableData::CropDefinition* data = VegetableData::get_DataByType(type);

        SDL_FRect r = rect_for_slot(index);

        if (mouse.x >= r.x && mouse.x <= r.x + r.w &&
            mouse.y >= r.y && mouse.y <= r.y + r.h) {
            sel.is_valid  = true;
            sel.vegetable = type;
            sel.resource  = ResourceType::NONE;
            sel.item      = nullptr;
            sel.tile      = {data->icon_tile_x, data->icon_tile_y};
            return sel;
        }

        index++;
    }
    for (auto& pair : seeds) {
        if (pair.second.count <= 0) continue;

        if (index >= inventory_grid_cols * inventory_grid_rows) break;


        VegetableData::Type type = pair.first;
        const VegetableData::CropDefinition* data = VegetableData::get_DataByType(type);

        SDL_FRect r = rect_for_slot(index);

        if (mouse.x >= r.x && mouse.x <= r.x + r.w &&
            mouse.y >= r.y && mouse.y <= r.y + r.h) {
            sel.is_valid  = true;
            sel.seed      = type;
            sel.vegetable = VegetableData::Type::NONE;
            sel.resource  = ResourceType::NONE;
            sel.item      = nullptr;
            sel.tile      = {data->icon_tile_x, data->icon_tile_y};
            return sel;
        }

        index++;
    }


    for (auto &inv_item : items) {
        if (index >= inventory_grid_cols * inventory_grid_rows) break;

        auto it = Icon::item_pos.find(inv_item->type);
        if (it == Icon::item_pos.end()) continue;

        SDL_FRect r = rect_for_slot(index);

        if (mouse.x >= r.x && mouse.x <= r.x + r.w &&
                mouse.y >= r.y && mouse.y <= r.y + r.h) {
            sel.is_valid  = true;
            sel.item      = inv_item.get();
            sel.resource  = ResourceType::NONE;
            sel.vegetable = VegetableData::Type::NONE;
            sel.tile      = it->second;
            return sel;
        }

        index++;
    }

    return sel;
}

bool
Inventory::cursor_OverInventoryGrid(const SDL_FPoint& mouse) {
    const float grid_x = menu_inner.x;
    const float grid_y = menu_inner.y;

    const float grid_w = inventory_grid_cols * inventory_grid_slot_size;
    const float grid_h = inventory_grid_rows * inventory_grid_slot_size;

    return mouse.x >= grid_x &&
           mouse.x <  grid_x + grid_w &&
           mouse.y >= grid_y &&
           mouse.y <  grid_y + grid_h;
}

bool
Inventory::cursor_OverBuildMenu(const SDL_FPoint& mouse) {
    const float column_x = menu_inner.x + menu_inner.w - build_menu_width;
    const float column_w = build_menu_width;

    return mouse.x >= column_x &&
           mouse.x <= column_x + column_w &&
           mouse.y >= menu_inner.y &&
           mouse.y <= menu_inner.y + menu_inner.h;
}

bool
Inventory::cursor_OverInfoPanel(const SDL_FPoint& mouse) {
    const float panel_x = menu_inner.x + menu_inner.w + 10.f;
    const float panel_w = INFO_PANEL_WIDTH;

    return mouse.x >= panel_x &&
           mouse.x <= panel_x + panel_w &&
           mouse.y >= menu_inner.y &&
           mouse.y <= menu_inner.y + menu_inner.h;
}

void
Inventory::draw_Menu(SDL_Renderer* renderer, SDL_Rect win_size, TTF_Font* font, std::vector<bool> researched_items, bool is_hovering_craftbutton) {
    // real grid sizes
    float grid_w = inventory_grid_cols * inventory_grid_slot_size;
    float grid_h = inventory_grid_rows * inventory_grid_slot_size;

    // inner area: grid + build menu on the right
    float inner_w = grid_w + build_menu_width;
    float inner_h = grid_h + 40.f;

    float total_w = inner_w + MENU_BORDER * 2;
    float total_h = inner_h + MENU_BORDER * 2;

    float start_x = win_size.w * 0.5f - total_w * 0.5f;
    float start_y = win_size.h * 0.5f - total_h * 0.5f;

    SDL_FRect outer{
        start_x,
        start_y,
        total_w,
        total_h
    };

    menu_inner = {
        start_x + MENU_BORDER,
        start_y + MENU_BORDER,
        inner_w,
        inner_h
    };

    SDL_SetRenderDrawColor(renderer, 60, 60, 70, 255);
    SDL_RenderFillRect(renderer, &outer);

    SDL_SetRenderDrawColor(renderer, 120, 120, 140, 255);
    SDL_RenderFillRect(renderer, &menu_inner);

    draw_InventoryGrid(renderer);
    draw_BuildMenu(renderer, researched_items);
    draw_InfoPanel(renderer, font, selected_build_item, is_hovering_craftbutton);
}


void
Inventory::draw_Hotbar(SDL_Renderer* renderer, TTF_Font* font) {
    const float slot_size = 48.f;
    const float spacing = 4.f;
    const float start_x = 20.f;
    const float start_y = 20.f;

    SDL_Color number_color = {255, 255, 255, 255}; // white numbers

    for (int i = 1; i < HOTBAR_SLOTS; ++i) {
        float x = start_x + i * (slot_size + spacing);
        float y = start_y;

        // Slot background
        SDL_FRect bg{ x, y, slot_size, slot_size };
        SDL_SetRenderDrawColor(renderer, 50, 50, 60, 255);
        SDL_RenderFillRect(renderer, &bg);

        // Border
        if (i == selected_hotbar)
            SDL_SetRenderDrawColor(renderer, 255, 255, 100, 255);
        else
            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
        SDL_RenderRect(renderer, &bg);

        // Draw item icon if exists
        if (hotbar[i].item_id != 0) {
            if (hotbar[i].type == InventoryObjectType::ITEM) {
                ItemType item = get_ItemById(hotbar[i].item_id)->type;
                if (item != ItemType::NONE) {
                    auto it = Icon::item_pos.find(item);
                    if (it != Icon::item_pos.end()) {
                        SDL_FRect src{ it->second.x * 16.f, it->second.y * 16.f, 16.f, 16.f };
                        SDL_FRect dst{ x, y, slot_size, slot_size };
                        SDL_RenderTexture(renderer, Icon::icons_texture, &src, &dst);
                    }
                }
            } else
            if (hotbar[i].type == InventoryObjectType::VEGETABLE) {
                InventoryVegetable& item = get_VegetableById(hotbar[i].item_id);
                const VegetableData::CropDefinition*
                    data = get_DataByType(item.type);
                SDL_FRect src{ data->icon_tile_x * 16.f, data->icon_tile_y * 16.f, 16.f, 16.f };
                SDL_FRect dst{ x, y, slot_size, slot_size };
                SDL_RenderTexture(renderer, Icon::icons_texture, &src, &dst);
            } else
            if (hotbar[i].type == InventoryObjectType::SEED) {
                InventoryVegetable& item = get_SeedById(hotbar[i].item_id);
                const VegetableData::CropDefinition*
                    data = get_DataByType(item.type);
                SDL_FRect src{ (data->icon_tile_x-1) * 16.f, data->icon_tile_y * 16.f, 16.f, 16.f };
                SDL_FRect dst{ x, y, slot_size, slot_size };
                SDL_RenderTexture(renderer, Icon::icons_texture, &src, &dst);
            }
        }

        // Draw slot number in bottom-right corner
        std::string number = std::to_string(i);
        SDL_Surface* surf = TTF_RenderText_Solid(font, number.c_str(), number.length(), number_color);
        if (surf) {
            SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, surf);
            if (tex) {
                SDL_FRect dst{
                    x + slot_size - surf->w - 2.f,
                      y + slot_size - surf->h - 2.f,
                      (float)surf->w,
                      (float)surf->h
                };
                SDL_RenderTexture(renderer, tex, nullptr, &dst);
                SDL_DestroyTexture(tex);
            }
            SDL_DestroySurface(surf);
        }
    }
}

bool Inventory::set_HotbarItem(int slot, Inventory_s* item) {
    if (slot < 0 || slot >= HOTBAR_SLOTS || !item) return false;
    hotbar[slot].item_id = item->id;
    selected_hotbar_last = selected_hotbar;
    hotbar[slot].type    = item->object_type;
    return true;
}

Inventory_s* Inventory::get_SelectedHotbarItem() {
    Equipement eq = hotbar[selected_hotbar];
    Inventory_s* item = nullptr;
    if (eq.type == InventoryObjectType::ITEM) 
        item = get_ItemById(hotbar[selected_hotbar].item_id);
    else
    if (eq.type == InventoryObjectType::VEGETABLE) 
        item = &get_VegetableById(hotbar[selected_hotbar].item_id);
    else
    if (eq.type == InventoryObjectType::SEED) 
        item = &get_SeedById(hotbar[selected_hotbar].item_id);

    if (selected_hotbar == 0 || !item) {
        // std::cout << "Something is broken,... " << selected_hotbar << " " << hotbar[selected_hotbar].item_id << std::endl;
        return hotbar_hands;
    }
    return item;
}

InventoryItem* Inventory::get_ItemById(size_t id) {
    for (auto &item : items)
        if (item->id == id)
            return item.get();
    return nullptr;
}

InventoryVegetable& Inventory::get_VegetableById(size_t id) {
    for (auto &item : vegetables)
        if (item.second.id == id)
            return item.second;
    throw std::out_of_range("Vegetable not found");
}

InventoryVegetable& Inventory::get_SeedById(size_t id) {
    for (auto &item : seeds)
        if (item.second.id == id)
            return item.second;
    throw std::out_of_range("Vegetable not found");
}










static void WriteItem(std::ofstream &file, const InventoryItem &it) {
    file.write(reinterpret_cast<const char*>(&it.object_type), sizeof(it.object_type));
    file.write(reinterpret_cast<const char*>(&it.id), sizeof(it.id));
    file.write(reinterpret_cast<const char*>(&it.equipement_slot), sizeof(it.equipement_slot));

    file.write(reinterpret_cast<const char*>(&it.type), sizeof(it.type));
    file.write(reinterpret_cast<const char*>(&it.health_current), sizeof(it.health_current));
    file.write(reinterpret_cast<const char*>(&it.health_max), sizeof(it.health_max));

    file.write(reinterpret_cast<const char*>(&it.speed), sizeof(it.speed));
    file.write(reinterpret_cast<const char*>(&it.level), sizeof(it.level));
}

static void WriteVegetable(std::ofstream &file, const InventoryVegetable &veg) {
    file.write(reinterpret_cast<const char*>(&veg.object_type), sizeof(veg.object_type));

    file.write(reinterpret_cast<const char*>(&veg.id), sizeof(veg.id));
    file.write(reinterpret_cast<const char*>(&veg.equipement_slot), sizeof(veg.equipement_slot));
    file.write(reinterpret_cast<const char*>(&veg.type), sizeof(veg.type));
    file.write(reinterpret_cast<const char*>(&veg.count), sizeof(veg.count));

}

static void WriteSeed(std::ofstream &file, const InventoryVegetable &veg) {
    file.write(reinterpret_cast<const char*>(&veg.object_type), sizeof(veg.object_type));

    file.write(reinterpret_cast<const char*>(&veg.id), sizeof(veg.id));
    file.write(reinterpret_cast<const char*>(&veg.equipement_slot), sizeof(veg.equipement_slot));
    file.write(reinterpret_cast<const char*>(&veg.type), sizeof(veg.type));
    file.write(reinterpret_cast<const char*>(&veg.count), sizeof(veg.count));
}

bool Inventory::Save(const std::string &path) const {
    std::ofstream file(path + "/inventory", std::ios::binary | std::ios::trunc);
    std::cout << "Save Inventory\n";
    if (!file) {
        std::cout << "No file found at: " <<  path + "/inventory" << std::endl;
        return false;
    }

    // resources
    {
        uint32_t count = resources.size();
        file.write(reinterpret_cast<const char*>(&count), sizeof(count));

        for (const auto &kv : resources) {
            file.write(reinterpret_cast<const char*>(&kv.first), sizeof(kv.first));
            file.write(reinterpret_cast<const char*>(&kv.second), sizeof(kv.second));
        }
    }

    // items
    {
        uint32_t count = items.size();
        std::cout << "ITEM COUNT WRITE: " << count << std::endl;
        file.write(reinterpret_cast<const char*>(&count), sizeof(count));

        for (const auto &item : items)
            WriteItem(file, *item);

    // vegetable
        count = vegetables.size();
        std::cout << "VEGETABLE COUNT WRITE: " << count << std::endl;
        file.write(reinterpret_cast<const char*>(&count), sizeof(count));

        for (const auto &pair : vegetables)
            WriteVegetable(file, pair.second);

    // seed
        count = seeds.size();
        std::cout << "Seeds COUNT WRITE: " << count << std::endl;
        file.write(reinterpret_cast<const char*>(&count), sizeof(count));

        for (const auto &pair : seeds)
            WriteSeed(file, pair.second);
    }

    // equipment slot item_id
    {
        uint32_t count = equipment.size();
        file.write(reinterpret_cast<const char*>(&count), sizeof(count));

        for (const auto &kv : equipment) {
            EquipmentSlot slot = kv.first;
            size_t id = kv.second.item_id;
            std::cout << "EQ " << id << std::endl;

            file.write(reinterpret_cast<const char*>(&slot), sizeof(slot));
            file.write(reinterpret_cast<const char*>(&id), sizeof(id));
        }
    }

    // hotbar
    for (int i = 0; i < HOTBAR_SLOTS; ++i) {
        std::cout << "WRITE " << hotbar[i].item_id << std::endl;
        file.write(reinterpret_cast<const char*>(&hotbar[i].item_id), sizeof(hotbar[i].item_id));
        file.write(reinterpret_cast<const char*>(&hotbar[i].type),    sizeof(hotbar[i].type));
    }

    // selected hotbars
    std::cout << "WRITE SEL HOTBAR " << selected_hotbar << std::endl;
    file.write(reinterpret_cast<const char*>(&selected_hotbar), sizeof(selected_hotbar));
    file.write(reinterpret_cast<const char*>(&selected_hotbar_last), sizeof(selected_hotbar_last));

    return file.good();
}


static void ReadBase(std::ifstream &file, Inventory_s *obj) {
    // file.read(reinterpret_cast<char*>(&obj->object_type), sizeof(obj->object_type));  // remove
    file.read(reinterpret_cast<char*>(&obj->id), sizeof(obj->id));
    file.read(reinterpret_cast<char*>(&obj->equipement_slot), sizeof(obj->equipement_slot));
}
static InventoryItem* ReadItem(std::ifstream &file) {
    auto *it = new InventoryItem(0, ItemType::HAND);
    ReadBase(file, it);

    file.read(reinterpret_cast<char*>(&it->type), sizeof(it->type));
    file.read(reinterpret_cast<char*>(&it->health_current), sizeof(it->health_current));
    file.read(reinterpret_cast<char*>(&it->health_max), sizeof(it->health_max));

    file.read(reinterpret_cast<char*>(&it->speed), sizeof(it->speed));
    file.read(reinterpret_cast<char*>(&it->level), sizeof(it->level));

    return it;
}
static InventoryVegetable* ReadVegetable(std::ifstream &file) {
    auto *veg = new InventoryVegetable(0, VegetableData::Type::NONE, false);
    ReadBase(file, veg);

    file.read(reinterpret_cast<char*>(&veg->type), sizeof(veg->type));
    file.read(reinterpret_cast<char*>(&veg->count), sizeof(veg->count));

    return veg;
}
static InventoryVegetable* ReadSeed(std::ifstream &file) {
    auto *veg = new InventoryVegetable(0, VegetableData::Type::NONE, true);
    ReadBase(file, veg);

    file.read(reinterpret_cast<char*>(&veg->type), sizeof(veg->type));
    file.read(reinterpret_cast<char*>(&veg->count), sizeof(veg->count));

    return veg;
}
static Inventory_s* ReadObject(std::ifstream &file)
{
    InventoryObjectType ot;
    file.read(reinterpret_cast<char*>(&ot), sizeof(ot));

    switch (ot) {
        case InventoryObjectType::ITEM: {
            auto* it = ReadItem(file);
            it->object_type = ot;
            return it;
        }
        case InventoryObjectType::VEGETABLE: {
             auto* veg = ReadVegetable(file);
             veg->object_type = ot;
             return veg;
         }
        case InventoryObjectType::SEED: {
             auto* veg = ReadSeed(file);
             veg->object_type = ot;
             return veg;
         }

        default:
            return nullptr;
    }
}

bool Inventory::Load(const std::string &path) {
    std::cout << "Load Inventory\n";
    std::ifstream file(path + "/inventory", std::ios::binary);
    if (!file) {
        std::cout << "No file found at: " <<  path + "/inventory" << std::endl;
        return false;
    }

    resources.clear();
    items.clear();
    vegetables.clear();
    equipment.clear();

    // resources
    {
        uint32_t count = 0;
        file.read(reinterpret_cast<char*>(&count), sizeof(count));

        for (uint32_t i = 0; i < count; ++i)
        {
            ResourceType key;
            int value;

            file.read(reinterpret_cast<char*>(&key), sizeof(key));
            file.read(reinterpret_cast<char*>(&value), sizeof(value));

            resources[key] = value;
            std::cout << "RES SIZ " << resources.size() << std::endl; 
        }
    }

    // items
    {
        uint32_t count = 0;
        file.read(reinterpret_cast<char*>(&count), sizeof(count));
        next_item_id = 0;
        for (uint32_t i = 0; i < count; ++i) {
            Inventory_s *obj = ReadObject(file);

            if (obj->id > next_item_id)
                next_item_id = obj->id;
            items.push_back(std::unique_ptr<InventoryItem>(
                        static_cast<InventoryItem*>(obj)
                        ));
            std::cout << "ITM SIZ " << items.size() << std::endl; 
        }

        count = 0;
        file.read(reinterpret_cast<char*>(&count), sizeof(count));
        for (uint32_t i = 0; i < count; ++i) {
            Inventory_s *obj = ReadObject(file);
            InventoryVegetable* veg = static_cast<InventoryVegetable*>(obj);

            vegetables.try_emplace(veg->type, *veg);
            if (veg->id > next_item_id)
                next_item_id = veg->id;

            std::cout << "VEG SIZ " << vegetables.size() << std::endl; 
            // we no longer need the allocated pointer after copying
            delete veg;
        }
        count = 0;
        file.read(reinterpret_cast<char*>(&count), sizeof(count));
        for (uint32_t i = 0; i < count; ++i) {
            Inventory_s *obj = ReadObject(file);
            InventoryVegetable* veg = static_cast<InventoryVegetable*>(obj);

            seeds.try_emplace(veg->type, *veg);
            if (veg->id > next_item_id)
                next_item_id = veg->id;

            std::cout << "SEED SIZ " << seeds.size() << std::endl; 
            // we no longer need the allocated pointer after copying
            delete veg;
        }
        next_item_id++;
    }

    // rebuild equipment map using item IDs
    {
        uint32_t count = 0;
        file.read(reinterpret_cast<char*>(&count), sizeof(count));

        for (uint32_t i = 0; i < count; ++i) {
            EquipmentSlot slot;
            size_t id;

            file.read(reinterpret_cast<char*>(&slot), sizeof(slot));
            file.read(reinterpret_cast<char*>(&id), sizeof(id));
            std::cout << "EQ " << id << std::endl;

            equipment[slot].item_id = id;
        }
    }

    // hotbar
    for (int i = 0; i < HOTBAR_SLOTS; ++i) {
        file.read(reinterpret_cast<char*>(&hotbar[i].item_id), sizeof(hotbar[i].item_id));
        //std::cout << "READ " << hotbar[i].item_id << std::endl;
        file.read(reinterpret_cast<char*>(&hotbar[i].type), sizeof(hotbar[i].type));
    }

    // selected hotbar
    file.read(reinterpret_cast<char*>(&selected_hotbar), sizeof(selected_hotbar));
    std::cout << "READ SEL HOTBAR " << selected_hotbar << std::endl;
    file.read(reinterpret_cast<char*>(&selected_hotbar_last), sizeof(selected_hotbar_last));

    return file.good();
}