M1a: survival core, pause menu, title screen and save/load
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.
This commit is contained in:
167
scripts/core/inventory.gd
Normal file
167
scripts/core/inventory.gd
Normal file
@@ -0,0 +1,167 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user