Files
Emberwild/scripts/core/inventory.gd

185 lines
5.0 KiB
GDScript

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
## A stack can carry "dur": uses left on a tool or weapon that wears out
## (ItemDB "durability"). No "dur" means as new.
static func _norm(stack):
if stack == null or int(stack.count) <= 0:
return null
var out := {"id": String(stack.id), "count": int(stack.count)}
if stack.has("dur"):
out.dur = int(stack.dur)
return out
## Adds a whole stack, keeping what's special about it (a worn tool's "dur").
## Returns how many didn't fit.
func add_stack(stack: Dictionary) -> int:
if not stack.has("dur"):
return add(String(stack.id), int(stack.count))
for i in range(slots.size()):
if slots[i] == null:
slots[i] = _norm(stack)
changed.emit()
return 0
return 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_stack(s)
slots[i] = null if left <= 0 else _norm({"id": s.id, "count": left, "dur": s.dur} if s.has("dur") 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 and cursor.count > 1:
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()