init
This commit is contained in:
commit
87bff164f1
416 files changed
+279512
No files matched your search
@@ -0,0 +1,216 @@
|
||||
#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 <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<int, kWands> wands{}; // entity ids, 0 = empty slot
|
||||
std::array<int, kItems> items{};
|
||||
std::array<int, kFull> 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<int> spells(int wand) {
|
||||
std::vector<int> 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<int> permanent_spells(int wand) {
|
||||
std::vector<int> 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);
|
||||
(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<void(int wand)> 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
|
||||
Reference in new issue
Block a user