#pragma once // The inventory of the player: the quick inventory (4 wand slots, 4 item slots), the full inventory (16 spell slots) and the spell slots of the wands, // with the rules of the original engine for what may be dropped where (recovered from noita.exe: FUN_00b5ee40 and the predicates it calls, see // docs/INVENTORY_UI.md). Items are runtime entities; where they are is kept in `ItemComponent.inventory_slot.x / .y` like in the game, the owner of a spell is // its parent (the wand); the gun reads a wand's spells by slot. #include #include #include #include #include #include "entity/runtime.h" #include "gun/gun.h" namespace ng { class Inventory { public: static constexpr int kWands = 4, kItems = 4, kFull = 16; enum Kind { None = 0, QuickWand, QuickItem, Full, WandSpell }; struct Loc { Kind kind = None; int wand = 0; // WandSpell: the wand entity int index = 0; // slot number inside the container bool operator==(const Loc& o) const { return kind == o.kind && wand == o.wand && index == o.index; } }; Inventory(EntityRuntime& rt, GunRuntime& gun) : rt_(rt), gun_(gun) {} std::array wands{}; // entity ids, 0 = empty slot std::array items{}; std::array full{}; bool editing = false; // the wands may be edited (inside a workshop, or the perk EDIT_WANDS_EVERYWHERE): DAT_0122250a of the engine // ---- predicates of the engine (FUN_00b5db10 and friends) ---- bool has(int e, const char* comp) { return e && !rt_.components(e, comp, "", true).empty(); } bool is_wand(int e) { return has(e, "AbilityComponent") && attr(e, "AbilityComponent", "use_gun_script") == "1"; } // FUN_00b5db10: AbilityComponent.use_gun_script bool is_spell(int e) { return has(e, "ItemActionComponent"); } // FUN_00b5dbf0 bool is_pure_spell(int e) { return is_spell(e) && !has(e, "AbilityComponent"); } // FUN_00b5dd60 bool is_flask(int e) { return has(e, "MaterialInventoryComponent"); } // FUN_00b5e0d0 std::string preferred(int e) { return attr(e, "ItemComponent", "preferred_inventory"); } // ItemComponent.preferred_inventory: DEFAULT / QUICK / FULL int capacity(int wand) { // FUN_00b5d7d0: the slots of a wand = its deck capacity return (int)std::atof(attr(wand, "AbilityComponent", "gun_config.deck_capacity").c_str()); } int slot_x(int item) { return (int)std::atof(attr(item, "ItemComponent", "inventory_slot.x").c_str()); } void set_slot(int item, int x, int y = 0) { for (int cid : rt_.components(item, "ItemComponent", "", true)) { rt_.comp(cid)->attrs["inventory_slot.x"] = std::to_string(x); rt_.comp(cid)->attrs["inventory_slot.y"] = std::to_string(y); } } // The spells of a wand by slot (0 = empty), `capacity` entries; the always-cast ones are not in slots. std::vector spells(int wand) { std::vector r(std::max(capacity(wand), 0), 0); for (int k : rt_.children(wand)) { if (!is_spell(k) || permanent(k)) continue; const int x = slot_x(k); if (x >= 0 && x < (int)r.size() && !r[x]) r[x] = k; } return r; } std::vector permanent_spells(int wand) { std::vector r; for (int k : rt_.children(wand)) if (is_spell(k) && permanent(k)) r.push_back(k); return r; } bool permanent(int spell) { // (AddGunActionPermanent sets the flag on the ItemComponent, the data files on the ItemActionComponent) for (int cid : rt_.components(spell, "ItemActionComponent", "", true)) if (rt_.comp(cid)->attrs["permanently_attached"] == "1") return true; for (int cid : rt_.components(spell, "ItemComponent", "", true)) if (rt_.comp(cid)->attrs["permanently_attached"] == "1") return true; return false; } Loc where(int item) { for (int i = 0; i < kWands; ++i) if (wands[i] == item) return {QuickWand, 0, i}; for (int i = 0; i < kItems; ++i) if (items[i] == item) return {QuickItem, 0, i}; for (int i = 0; i < kFull; ++i) if (full[i] == item) return {Full, 0, i}; if (RtEntity* e = rt_.ent(item)) if (e->parent && isWandLike(e->parent)) { const int x = slot_x(item); return {WandSpell, e->parent, x}; } return {}; } int at(const Loc& l) { switch (l.kind) { case QuickWand: return l.index >= 0 && l.index < kWands ? wands[l.index] : 0; case QuickItem: return l.index >= 0 && l.index < kItems ? items[l.index] : 0; case Full: return l.index >= 0 && l.index < kFull ? full[l.index] : 0; case WandSpell: { auto s = spells(l.wand); return l.index >= 0 && l.index < (int)s.size() ? s[l.index] : 0; } default: return 0; } } // ---- the rules: may `item` be dropped into the container of `target`? (FUN_00b5ee40) ---- bool can_drop(int item, const Loc& target) { if (!item || target.kind == None) return false; const bool to_quick = target.kind == QuickWand || target.kind == QuickItem, to_full = target.kind == Full, to_wand = target.kind == WandSpell; if (is_wand(item) && !to_quick) return false; if (target.kind == QuickWand && !is_wand(item)) return false; // the wands row takes wands, the items row everything else of the quick inventory if (target.kind == QuickItem && is_wand(item)) return false; // wands only live in the quick inventory const Loc cur = where(item); if (same_container(cur, target)) return true; // moving inside a container is always allowed if (to_wand && target.wand == item) return false; if (to_wand && !editing) return false; // a wand takes spells only while the wands may be edited if (preferred(item) == "FULL" && to_full && !is_spell(item)) return true; // (the sampo and other items that live in the full inventory) if (to_full && !editing) return false; // spells only enter the full inventory while editing if (is_spell(item) && to_wand) return !is_inside(item, target.wand); // a wand that is not yet holding it (the container is a wand here) if (is_spell(item) && to_quick) return false; // spells never go into the quick inventory if (is_flask(item) && to_wand) return false; if (!is_spell(item)) { if (to_full) return false; return to_quick; } // other items: the quick inventory only (unless they prefer the full one) return to_full; // spells: the full inventory } // Free place for an item that is picked up (the engine's pick-up goes to the preferred container of the item): returns the location or {}. Loc free_place(int item) { if (is_spell(item) || preferred(item) == "FULL") { for (int i = 0; i < kFull; ++i) if (!full[i]) return {Full, 0, i}; return {}; } if (is_wand(item)) { for (int i = 0; i < kWands; ++i) if (!wands[i]) return {QuickWand, 0, i}; return {}; } for (int i = 0; i < kItems; ++i) if (!items[i]) return {QuickItem, 0, i}; return {}; } bool can_pick(int item) { return free_place(item).kind != None; } // The item enters the inventory at its preferred place (picking it up never needs the editing rights). bool add(int item) { const Loc l = free_place(item); if (l.kind == None) return false; put(item, l); return true; } // Moves an item to the slot `target`: into an empty slot when the rules allow, onto an occupied slot as a swap when both may go to the other's container. // Returns true when something changed. bool drop_on(int item, const Loc& target) { const Loc from = where(item); if (from.kind == None || target.kind == None || from == target) return false; const int other = at(target); if (!can_drop(item, target)) return false; if (!other) { take(item); put(item, target); return true; } if (other == item) return false; if (!can_drop(other, from)) return false; // a swap: the other one must be allowed to go where the item came from take(item); take(other); put(item, target); put(other, from); return true; } // The item leaves the inventory (thrown, dropped, consumed). void remove(int item) { const Loc l = where(item); take(item); if (item && gun_.equipped() == item) gun_.equip(0); (void)l; } // First free spell slot of a wand (-1: full). int free_spell_slot(int wand) { auto s = spells(wand); for (int i = 0; i < (int)s.size(); ++i) if (!s[i]) return i; return -1; } // Shift-click: a spell of the full inventory goes to the first free slot of `wand` (editing only), a spell of a wand to the first free full slot. bool quick_move(int item, int wand) { const Loc from = where(item); if (from.kind == Full && wand) { const int s = free_spell_slot(wand); return s >= 0 && drop_on(item, {WandSpell, wand, s}); } if (from.kind == WandSpell) { for (int i = 0; i < kFull; ++i) if (!full[i]) return drop_on(item, {Full, 0, i}); } return false; } // after any change of the spells of a wand the gun has to read them again std::function on_wand_changed; private: std::string attr(int e, const char* comp, const char* key) { for (int cid : rt_.components(e, comp, "", true)) { RtComponent* c = rt_.comp(cid); auto it = c->attrs.find(key); if (it != c->attrs.end()) return it->second; } return ""; } bool isWandLike(int e) { return is_wand(e); } bool is_inside(int item, int wand) { RtEntity* e = rt_.ent(item); return e && e->parent == wand; } static bool same_container(const Loc& a, const Loc& b) { if (a.kind == None || b.kind == None) return false; const bool qa = a.kind == QuickWand || a.kind == QuickItem, qb = b.kind == QuickWand || b.kind == QuickItem; if (qa && qb) return true; if (a.kind == Full && b.kind == Full) return true; if (a.kind == WandSpell && b.kind == WandSpell) return a.wand == b.wand; return false; } void take(int item) { for (auto& v : wands) if (v == item) v = 0; for (auto& v : items) if (v == item) v = 0; for (auto& v : full) if (v == item) v = 0; if (RtEntity* e = rt_.ent(item)) if (e->parent && is_wand(e->parent) && is_spell(item)) { const int w = e->parent; e->parent = rt_.player(); if (on_wand_changed) on_wand_changed(w); } } void put(int item, const Loc& l) { RtEntity* e = rt_.ent(item); switch (l.kind) { case QuickWand: wands[l.index] = item; if (e) e->parent = rt_.player(); set_slot(item, l.index); break; case QuickItem: items[l.index] = item; if (e) e->parent = rt_.player(); set_slot(item, kWands + l.index); break; case Full: full[l.index] = item; if (e) e->parent = rt_.player(); set_slot(item, l.index); break; case WandSpell: if (e) e->parent = l.wand; set_slot(item, l.index); if (on_wand_changed) on_wand_changed(l.wand); break; default: break; } } EntityRuntime& rt_; GunRuntime& gun_; }; } // namespace ng