185 lines
5.0 KiB
GDScript
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()
|