Files

444 lines
13 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "graphics/AmbientOcclusion.h"
#include "graphics/AntiAliasing.h"
#include "graphics/Canvas.h"
#include "graphics/DrawItem2D.h"
#include "graphics/Font.h"
#include "graphics/GBuffer.h"
#include "graphics/GlobalIllumination.h"
#include "graphics/Graphics.h"
#include "graphics/Grass.h"
#include "graphics/Light.h"
#include "graphics/Material.h"
#include "graphics/Mesh.h"
#include "graphics/Outline.h"
#include "graphics/Quad.h"
#include "graphics/RenderControl.h"
#include "graphics/RenderSystem.h"
#include "graphics/ScreenSpaceReflection.h"
#include "graphics/Shader.h"
#include "graphics/Texture.h"
#include "graphics/Volumetric.h"
#include "graphics/Water.h"
#include "graphics/Waterfall.h"
#include "inventory/Bag.h"
#include "inventory/Equipment.h"
#include "inventory/Inventory.h"
#include "inventory/InventorySystem.h"
#include "inventory/Item.h"
#include "ui/UI.h"
#include "ui/UISystem.h"
#include "ui/Widget.h"
#include "window/Window.h"
#include <SDL2/SDL.h>
#include <cmath>
#include <string>
#include <vector>
// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
using eve::graphics::Color;
using namespace eve::inventory;
using namespace eve::graphics;
using namespace eve::ui;
namespace {
bool approxEq(float a, float b, float eps = 1e-5f) { return std::fabs(a - b) < eps; }
} // namespace
TEST_CASE("inventory.itemRegistry.registerAndJson") {
ItemRegistry::clear();
ItemDefinition potion;
potion.id = "potion.hp";
potion.displayName = "Heal Potion";
potion.maxStack = 20;
potion.weight = 0.2f;
potion.tags = {"consumable", "potion"};
potion.extra["icon"] = "ui/potion.png";
ItemRegistry::registerItem(potion);
CHECK(ItemRegistry::count() == 1);
CHECK(ItemRegistry::find("potion.hp") != nullptr);
CHECK_EQ(ItemRegistry::find("potion.hp")->maxStack, 20);
CHECK(ItemRegistry::find("potion.hp")->hasTag("potion"));
CHECK_EQ(ItemRegistry::find("potion.hp")->getExtra("icon"), "ui/potion.png");
int n = ItemRegistry::loadFromJson(R"([
{"id":"ore.iron","displayName":"Iron Ore","maxStack":50,"weight":1.0,"tags":["material"]},
{"id":"sword.iron","displayName":"Iron Sword","maxStack":1,"weight":3.5,
"equipSlot":"weapon","tags":["weapon","melee"],"extra":{"damage":"10"}}
])");
CHECK_EQ(n, 2);
CHECK_EQ(ItemRegistry::count(), 3);
CHECK_EQ(ItemRegistry::find("sword.iron")->equipSlot, "weapon");
ItemRegistry::clear();
CHECK_EQ(ItemRegistry::count(), 0);
}
TEST_CASE("inventory.bag.addStackRemove") {
ItemRegistry::clear();
InventorySystem::clearEvents();
InventorySystem::ensureBuiltins();
ItemDefinition coin;
coin.id = "coin";
coin.maxStack = 99;
coin.weight = 0.01f;
ItemRegistry::registerItem(coin);
Bag bag(5);
bag.setId("player");
bag.setMaxWeight(100.f);
CHECK(bag.canAddItem("coin", 10));
// NOTE: zeroerr CHECK_EQ evaluates the LHS twice (compare + print), so
// side-effecting calls must be stored in a local first.
int added = bag.addItem("coin", 150);
CHECK_EQ(added, 150); // 99 + 51 across two slots
CHECK_EQ(bag.countItem("coin"), 150);
CHECK_EQ(bag.getUsedSlotCount(), 2);
CHECK_EQ(bag.getSlotQuantity(0), 99);
CHECK_EQ(bag.getSlotQuantity(1), 51);
int removed = bag.removeItem("coin", 60);
CHECK_EQ(removed, 60);
CHECK_EQ(bag.countItem("coin"), 90);
CHECK_EQ(bag.getSlotQuantity(0), 39); // removed from first stack first
CHECK_EQ(bag.getSlotQuantity(1), 51);
ItemRegistry::clear();
}
TEST_CASE("inventory.bag.acceptTagsAndReject") {
ItemRegistry::clear();
InventorySystem::ensureBuiltins();
ItemDefinition quest;
quest.id = "quest.letter";
quest.tags = {"quest"};
ItemRegistry::registerItem(quest);
ItemDefinition junk;
junk.id = "junk";
junk.tags = {"trash"};
ItemRegistry::registerItem(junk);
Bag questBag(4);
questBag.addAcceptTag("quest");
CHECK(questBag.canAddItem("quest.letter", 1));
CHECK(!questBag.canAddItem("junk", 1));
CHECK_EQ(questBag.canAddItemReason("junk", 1), "accept_tag_mismatch");
Bag general(4);
general.addRejectTag("quest");
CHECK(!general.canAddItem("quest.letter", 1));
CHECK_EQ(general.canAddItemReason("quest.letter", 1), "rejected_tag");
CHECK(general.canAddItem("junk", 1));
ItemRegistry::clear();
}
TEST_CASE("inventory.bag.weightCapacity") {
ItemRegistry::clear();
InventorySystem::ensureBuiltins();
ItemDefinition ore;
ore.id = "ore";
ore.maxStack = 10;
ore.weight = 2.f;
ItemRegistry::registerItem(ore);
Bag bag(10);
bag.setMaxWeight(5.f);
bag.setCapacityPolicy("slotsAndWeight");
int added = bag.addItem("ore", 3);
CHECK_EQ(added, 2); // 2*2=4 <=5, third would be 6
CHECK(approxEq(bag.getUsedWeight(), 4.f));
CHECK(!bag.canAddItem("ore", 1));
CHECK_EQ(bag.canAddItemReason("ore", 1), "over_weight");
ItemRegistry::clear();
}
TEST_CASE("inventory.bag.moveSwapSplit") {
ItemRegistry::clear();
InventorySystem::ensureBuiltins();
ItemDefinition gem;
gem.id = "gem";
gem.maxStack = 10;
ItemRegistry::registerItem(gem);
Bag bag(4);
bag.addItem("gem", 8);
CHECK(bag.splitStack(0, 3, 1));
CHECK_EQ(bag.getSlotQuantity(0), 5);
CHECK_EQ(bag.getSlotQuantity(1), 3);
CHECK(bag.moveSlot(1, 2));
CHECK(bag.isSlotEmpty(1));
CHECK_EQ(bag.getSlotQuantity(2), 3);
CHECK(bag.swapSlots(0, 2));
CHECK_EQ(bag.getSlotQuantity(0), 3);
CHECK_EQ(bag.getSlotQuantity(2), 5);
// merge via move
CHECK(bag.moveSlot(0, 2));
CHECK(bag.isSlotEmpty(0));
CHECK_EQ(bag.getSlotQuantity(2), 8);
ItemRegistry::clear();
}
TEST_CASE("inventory.bag.transferBetweenBags") {
ItemRegistry::clear();
InventorySystem::clearEvents();
InventorySystem::ensureBuiltins();
ItemDefinition herb;
herb.id = "herb";
herb.maxStack = 20;
herb.weight = 0.1f;
ItemRegistry::registerItem(herb);
Bag a(3);
a.setId("a");
Bag b(3);
b.setId("b");
a.addItem("herb", 15);
int moved = InventorySystem::transfer(&a, &b, "herb", 10);
CHECK_EQ(moved, 10);
CHECK_EQ(a.countItem("herb"), 5);
CHECK_EQ(b.countItem("herb"), 10);
ItemRegistry::clear();
}
TEST_CASE("inventory.extension.customAcceptAndStack") {
ItemRegistry::clear();
InventorySystem::ensureBuiltins();
ItemDefinition a;
a.id = "scroll.a";
a.maxStack = 5;
a.extra["level"] = "3";
ItemRegistry::registerItem(a);
InventorySystem::registerAcceptRule("minLevel3", [](const Bag &, const ItemDefinition &def,
int, std::string *reason) {
int level = 0;
try {
level = std::stoi(def.getExtra("level", "0"));
} catch (...) {
}
if (level < 3) {
if (reason) *reason = "level_too_low";
return false;
}
return true;
});
InventorySystem::registerStackRule("neverSame",
[](const ItemStack &, const ItemStack &,
const ItemDefinition &) { return false; });
Bag bag(4);
bag.setAcceptRule("minLevel3");
bag.setStackRule("neverSame");
CHECK(bag.canAddItem("scroll.a", 1));
int added = bag.addItem("scroll.a", 3);
CHECK_EQ(added, 3);
// never stack -> each unit occupies its own slot
CHECK_EQ(bag.getUsedSlotCount(), 3);
InventorySystem::unregisterAcceptRule("minLevel3");
InventorySystem::unregisterStackRule("neverSame");
ItemRegistry::clear();
}
TEST_CASE("inventory.equipment.equipUnequip") {
ItemRegistry::clear();
InventorySystem::clearEvents();
InventorySystem::ensureBuiltins();
ItemDefinition sword;
sword.id = "sword.iron";
sword.maxStack = 1;
sword.equipSlot = "weapon";
sword.tags = {"weapon"};
ItemRegistry::registerItem(sword);
ItemDefinition helm;
helm.id = "helm.iron";
helm.maxStack = 1;
helm.equipSlot = "head";
helm.tags = {"armor"};
ItemRegistry::registerItem(helm);
Bag bag(4);
bag.setId("player");
bag.addItem("sword.iron", 1);
bag.addItem("helm.iron", 1);
EquipmentSet eq;
eq.setId("eq");
eq.defineSlot("weapon");
eq.addSlotAllowedTag("weapon", "weapon");
eq.defineSlot("head");
int swordSlot = bag.findItem("sword.iron");
CHECK(swordSlot >= 0);
CHECK(eq.equipFromBag("weapon", &bag, swordSlot));
CHECK_EQ(eq.getSlotItemId("weapon"), "sword.iron");
CHECK_EQ(bag.countItem("sword.iron"), 0);
// wrong slot
int helmSlot = bag.findItem("helm.iron");
CHECK(!eq.equipFromBag("weapon", &bag, helmSlot));
CHECK(eq.unequipToBag("weapon", &bag));
CHECK(eq.isSlotEmpty("weapon"));
CHECK_EQ(bag.countItem("sword.iron"), 1);
ItemRegistry::clear();
}
TEST_CASE("inventory.events.andFacade") {
ItemRegistry::clear();
InventorySystem::clearEvents();
Inventory *inv = Inventory::create();
CHECK(inv != nullptr);
CHECK_EQ(inv->getName(), "Inventory");
int n = inv->registerItemsFromJson(
R"([{"id":"apple","maxStack":10,"weight":0.2,"tags":["food"]}])");
CHECK_EQ(n, 1);
CHECK(inv->hasItemDefinition("apple"));
CHECK(inv->hasAcceptRule("default"));
CHECK(inv->hasCapacityPolicy("slotsAndWeight"));
CHECK(inv->hasStackRule("sameItem"));
Bag *bag = inv->newBag(4);
bag->setId("pack");
int added = bag->addItem("apple", 4);
CHECK_EQ(added, 4);
CHECK(inv->getChangeEventCount() >= 1);
bool sawAdd = false;
for (int i = 0; i < inv->getChangeEventCount(); ++i) {
if (inv->getChangeEventAction(i) == "add" && inv->getChangeEventItemId(i) == "apple")
sawAdd = true;
}
CHECK(sawAdd);
inv->clearChangeEvents();
CHECK_EQ(inv->getChangeEventCount(), 0);
bag->destroy();
inv->clearItemDefinitions();
}
TEST_CASE("inventory.bag.uiPreview") {
ItemRegistry::clear();
InventorySystem::clearEvents();
InventorySystem::ensureBuiltins();
ItemRegistry::loadFromJson(R"([
{"id":"potion.hp","displayName":"Heal Potion","maxStack":20,"weight":0.2,"tags":["consumable"]},
{"id":"ore.iron","displayName":"Iron Ore","maxStack":50,"weight":1.0,"tags":["material"]},
{"id":"sword.iron","displayName":"Iron Sword","maxStack":1,"weight":3.5,
"equipSlot":"weapon","tags":["weapon"]},
{"id":"coin","displayName":"Gold Coin","maxStack":99,"weight":0.01,"tags":["currency"]}
])");
Bag bag(8);
bag.setId("adventurer");
bag.setMaxWeight(40.f);
bag.addItem("potion.hp", 7);
bag.addItem("ore.iron", 23);
bag.addItem("sword.iron", 1);
bag.addItem("coin", 120);
auto *win = eve::window::Window::create();
auto *gfx = Graphics::create();
auto *ui = UI::create();
REQUIRE(win != nullptr);
REQUIRE(gfx != nullptr);
REQUIRE(ui != nullptr);
eve::window::WindowSettings s;
s.width = 520;
s.height = 420;
s.centered = true;
REQUIRE(win->setWindowSettings(s));
REQUIRE(ui->initBackend());
gfx->setBackgroundColor(Color(0.09f, 0.10f, 0.14f, 1.f));
auto *cam = Camera2D::createCamera();
cam->data()->r = 0.09f;
cam->data()->g = 0.10f;
cam->data()->b = 0.14f;
// Soft bag silhouette behind the panel.
auto *panel = Renderable2D::create();
panel->transform()->x = 40;
panel->transform()->y = 50;
panel->sprite()->width = 280;
panel->sprite()->height = 320;
panel->sprite()->r = 0.18f;
panel->sprite()->g = 0.22f;
panel->sprite()->b = 0.30f;
panel->sprite()->visible = true;
int highlighted = 0;
for (int frame = 0; frame < 90; ++frame) {
if (frame % 18 == 0) highlighted = (highlighted + 1) % bag.getSlotCount();
std::vector<WidgetDesc> slots;
slots.push_back(text("Adventurer Pack", "title"));
slots.push_back(separator("sep"));
slots.push_back(text("weight " + std::to_string(int(bag.getUsedWeight())) + " / " +
std::to_string(int(bag.getMaxWeight())),
"weight"));
slots.push_back(progress(bag.getUsedWeight() / bag.getMaxWeight(), "wbar", "load"));
slots.push_back(separator("sep2"));
for (int i = 0; i < bag.getSlotCount(); ++i) {
const std::string id = bag.getSlotItemId(i);
std::string label = "[" + std::to_string(i) + "] empty";
if (!id.empty()) {
const ItemDefinition *def = ItemRegistry::find(id);
const std::string name = def ? def->displayName : id;
label = "[" + std::to_string(i) + "] " + name + " x" +
std::to_string(bag.getSlotQuantity(i));
if (i == highlighted) label = "> " + label;
}
slots.push_back(text(label, "s" + std::to_string(i)));
}
REQUIRE(UIHost::resolve(ui->remountAs("bag", window("Inventory", slots, "root"))).has_value());
ui->beginFrameAndRender();
RenderSystem::render(*gfx);
ui->dispatchEvents();
SDL_Event e;
while (SDL_PollEvent(&e)) {
ui->processEvent(&e);
if (e.type == SDL_QUIT) break;
}
SDL_Delay(16);
}
CHECK_EQ(bag.countItem("coin"), 120);
panel->sprite()->visible = false;
ItemRegistry::clear();
win->close();
}