Design: docs/SURVIVAL_DESIGN.md (core loop, systems, architecture, phases). Survival core (M1a): - Items/recipes as data (data/*.json) in the addon SDK's define_item / define_recipe shape; procedural item meshes and offscreen-rendered icons - Inventory class (40 slots, hotbar = row 1) shared by player and chests; click / right-click split / shift-click quick-move - Actions autoload: the intent layer (craft, pickup, harvest_hit, place, transfer) that validates every change; becomes the RPC surface in M2 - Gathering: ground pickups (sticks, stones, flint); trees chopped with an axe fall and leave stumps; boulders mined with a pickaxe shrink per hit - Crafting screen with station checks (workbench); placement mode with green/red ghost, rotation and overlap/slope validation - Pieces: campfire (light, flames, embers), workbench, chest (16 slots) - HUD: hotbar, held tool view model with swing, prompts, toasts Menus and saving: - ESC pause menu: resume, controls, fullscreen, mouse sensitivity, quit - Title screen over a flyover of the island: Continue / New World (name + seed) / Load World (with delete) / Quit - Saves autoload: one folder per world under user://worlds, atomic writes with a .bak; stores only player changes (player, pieces, chest contents, harvested resources, taken pickups) and regenerates the rest from the seed - Autosave every 2 min, Save & Quit to title/desktop, save on window close Fixes: UI screens now fill the viewport (were pinned top-left); title backdrop no longer captures the mouse. Tests: test_survival_core (31 checks), test_save_load (round trip + slots), test_terrain_edges now checks 8 seeds for deep pits / unbanked water.
168 lines
4.4 KiB
GDScript
168 lines
4.4 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
|
|
|
|
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()
|