class_name Inventory extends RefCounted ## Slot-based inventory with stacking. Backs the player, chests and (later) ## grave bags, so moving items is the same operation everywhere. ## ## Pure data: no nodes, no UI. Each slot is either null or {"id": String, ## "count": int}. UI widgets render it and listen to `changed`; mutations from ## gameplay go through Actions (see docs/SURVIVAL_DESIGN.md ยง3). signal changed var slots: Array = [] func _init(size: int = 40) -> void: slots.resize(size) func size() -> int: return slots.size() func get_slot(i: int): return slots[i] func set_slot(i: int, stack) -> void: slots[i] = _norm(stack) changed.emit() ## Looked up at runtime rather than via the ItemDB global, so this plain data ## class also compiles in contexts that load it before autoloads exist (tests, ## a future headless server tool). static func _stack_max(id: String) -> int: var db := (Engine.get_main_loop() as SceneTree).root.get_node_or_null("ItemDB") return db.stack_size(id) if db != null else 50 static func _norm(stack): if stack == null or int(stack.count) <= 0: return null return {"id": String(stack.id), "count": int(stack.count)} func count(id: String) -> int: var n := 0 for s in slots: if s != null and s.id == id: n += s.count return n ## How many of `id` would fit right now. func space_for(id: String) -> int: var cap := _stack_max(id) var room := 0 for s in slots: if s == null: room += cap elif s.id == id: room += cap - s.count return room ## Adds up to `n` of `id`, topping up existing stacks first. Returns the ## leftover that didn't fit (0 = everything fitted). func add(id: String, n: int) -> int: var cap := _stack_max(id) for s in slots: if n <= 0: break if s != null and s.id == id and s.count < cap: var take := mini(cap - s.count, n) s.count += take n -= take for i in range(slots.size()): if n <= 0: break if slots[i] == null: var take := mini(cap, n) slots[i] = {"id": id, "count": take} n -= take changed.emit() return n ## Removes `n` of `id` (from the last stacks first, so the hotbar keeps its ## items longest). Returns false and changes nothing if there aren't enough. func remove(id: String, n: int) -> bool: if count(id) < n: return false for i in range(slots.size() - 1, -1, -1): if n <= 0: break var s = slots[i] if s != null and s.id == id: var take := mini(s.count, n) s.count -= take n -= take if s.count <= 0: slots[i] = null changed.emit() return true ## Removes `n` from a specific slot. Returns how many were removed. func take_from(i: int, n: int) -> int: var s = slots[i] if s == null: return 0 var take := mini(s.count, n) s.count -= take if s.count <= 0: slots[i] = null changed.emit() return take func has_all(inputs: Dictionary) -> bool: for id in inputs: if count(id) < int(inputs[id]): return false return true ## Moves the stack in slot `i` into `other` (stacking where possible). Whatever ## doesn't fit stays put. Used for shift-click quick-move. func quick_move(i: int, other: Inventory) -> void: var s = slots[i] if s == null: return var left := other.add(s.id, s.count) slots[i] = null if left <= 0 else {"id": s.id, "count": left} changed.emit() ## Clicking slot `i` while holding `cursor` (a stack or null). Returns the new ## cursor stack. Left click: pick up / put down / merge / swap. Right click: ## pick up half, or put down one. func click(i: int, cursor, right: bool): var s = slots[i] var result = cursor if cursor == null: if s == null: return null if right and s.count > 1: var half := int(ceil(s.count / 2.0)) s.count -= half result = {"id": s.id, "count": half} else: result = s slots[i] = null elif s == null: if right: slots[i] = {"id": cursor.id, "count": 1} result = _norm({"id": cursor.id, "count": cursor.count - 1}) else: slots[i] = cursor result = null elif s.id == cursor.id: var cap := _stack_max(s.id) var move: int = 1 if right else cursor.count move = mini(move, cap - s.count) s.count += move result = _norm({"id": cursor.id, "count": cursor.count - move}) else: slots[i] = cursor result = s changed.emit() return result ## Serialisable snapshot (for saves and, later, network replication). func to_data() -> Array: return slots.duplicate(true) func load_data(data: Array) -> void: slots.resize(data.size()) for i in range(data.size()): slots[i] = _norm(data[i]) if data[i] != null else null changed.emit()