Files
Emberwild/scripts/items/item_meshes.gd
Paul Bailey 4640f5e577 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.
2026-11-04 00:34:46 +00:00

161 lines
6.1 KiB
GDScript

extends RefCounted
## Procedural meshes for items and placeable pieces, keyed by the "mesh" field
## in data/items.json. The same mesh is used for the inventory icon, the tool
## held in your hand and the object placed in the world.
##
## Convention: origin at the grip (tools) or at ground level, centred
## (placeables). Units are metres. Built from the low-poly helpers in
## proc_mesh.gd, so everything is flat-shaded with vertex colours.
const P = preload("res://scripts/world/proc_mesh.gd")
const HANDLE := Color(0.55, 0.40, 0.24)
const STONE := Color(0.55, 0.54, 0.52)
const FLINT := Color(0.22, 0.23, 0.27)
const PLANK := Color(0.62, 0.45, 0.27)
const PLANK_DARK := Color(0.42, 0.29, 0.17)
const IRON := Color(0.30, 0.30, 0.32)
const ASH := Color(0.18, 0.17, 0.16)
const BINDING := Color(0.75, 0.68, 0.48)
static var _cache := {}
static func build(key: String) -> ArrayMesh:
if _cache.has(key):
return _cache[key]
var m: ArrayMesh
match key:
"stick": m = _stick()
"stone": m = _pebble(11, STONE, Vector3(0.14, 0.10, 0.12))
"flint": m = _pebble(23, FLINT, Vector3(0.12, 0.07, 0.15), 0.5)
"wood": m = _wood()
"stone_axe": m = _axe()
"stone_pickaxe": m = _pickaxe()
"club": m = _club()
"campfire": m = _campfire()
"workbench": m = _workbench()
"chest": m = _chest()
_:
push_warning("item_meshes: no mesh for '%s', using a stone" % key)
m = _pebble(1, Color.MAGENTA, Vector3.ONE * 0.15)
_cache[key] = m
return m
static func _rng(seed: int) -> RandomNumberGenerator:
var r := RandomNumberGenerator.new()
r.seed = seed
return r
static func _box(st: SurfaceTool, center: Vector3, size: Vector3, col: Color, basis: Basis = Basis()) -> void:
var h := size * 0.5
var c := [
Vector3(-h.x, -h.y, -h.z), Vector3(h.x, -h.y, -h.z), Vector3(h.x, h.y, -h.z), Vector3(-h.x, h.y, -h.z),
Vector3(-h.x, -h.y, h.z), Vector3(h.x, -h.y, h.z), Vector3(h.x, h.y, h.z), Vector3(-h.x, h.y, h.z),
]
for i in range(8):
c[i] = center + basis * c[i]
var faces := [[0, 1, 2, 3], [5, 4, 7, 6], [4, 0, 3, 7], [1, 5, 6, 2], [3, 2, 6, 7], [4, 5, 1, 0]]
for f in faces:
var a: Vector3 = c[f[0]]
var b: Vector3 = c[f[1]]
var cc: Vector3 = c[f[2]]
var d: Vector3 = c[f[3]]
var out := (a + b + cc + d) * 0.25 - center
P._tri(st, a, b, cc, out, col)
P._tri(st, a, cc, d, out, col)
static func _pebble(seed: int, col: Color, radii: Vector3, sharp: float = 0.3) -> ArrayMesh:
var n := FastNoiseLite.new()
n.seed = seed
n.frequency = 1.4
var st := P._begin()
P._blob(st, Vector3(0, radii.y * 0.6, 0), radii, n, sharp, col, _rng(seed), 1)
return P._commit(st)
static func _stick() -> ArrayMesh:
var st := P._begin()
var r := _rng(5)
P._cylinder(st, Vector3(-0.35, 0.03, 0), Vector3(0.35, 0.03, 0.02), 0.028, 0.02, 5, HANDLE, r, -1.0, PLANK)
P._cylinder(st, Vector3(0.05, 0.03, 0.0), Vector3(0.2, 0.05, 0.1), 0.012, 0.006, 4, HANDLE, r)
return P._commit(st)
static func _wood() -> ArrayMesh:
var st := P._begin()
var r := _rng(9)
P._cylinder(st, Vector3(-0.3, 0.1, -0.06), Vector3(0.3, 0.1, -0.06), 0.1, 0.1, 6, P.BARK, r, -1.0, PLANK)
P._cylinder(st, Vector3(-0.28, 0.1, 0.13), Vector3(0.3, 0.1, 0.13), 0.09, 0.09, 6, P.BARK, r, -1.0, PLANK)
P._cylinder(st, Vector3(-0.29, 0.27, 0.04), Vector3(0.29, 0.27, 0.04), 0.09, 0.09, 6, P.BARK, r, -1.0, PLANK)
return P._commit(st)
## Handle along +Y from the grip at the origin.
static func _handle(st: SurfaceTool, length: float, r: RandomNumberGenerator) -> void:
P._cylinder(st, Vector3(0, -0.1, 0), Vector3(0, length, 0), 0.022, 0.02, 6, HANDLE, r, -1.0, HANDLE)
static func _axe() -> ArrayMesh:
var st := P._begin()
var r := _rng(13)
_handle(st, 0.55, r)
_box(st, Vector3(0, 0.47, 0), Vector3(0.06, 0.07, 0.07), BINDING)
var n := FastNoiseLite.new()
n.seed = 3
n.frequency = 2.0
# Flint head: a flattened wedge pointing along +X.
P._blob(st, Vector3(0.075, 0.47, 0), Vector3(0.085, 0.055, 0.025), n, 0.2, FLINT, r, 1)
return P._commit(st)
static func _pickaxe() -> ArrayMesh:
var st := P._begin()
var r := _rng(17)
_handle(st, 0.6, r)
_box(st, Vector3(0, 0.55, 0), Vector3(0.06, 0.07, 0.07), BINDING)
P._cylinder(st, Vector3(-0.02, 0.56, 0), Vector3(0.22, 0.5, 0), 0.04, 0.008, 5, STONE, r, -1.0, STONE)
P._cylinder(st, Vector3(0.02, 0.56, 0), Vector3(-0.2, 0.51, 0), 0.04, 0.008, 5, STONE, r, -1.0, STONE)
return P._commit(st)
static func _club() -> ArrayMesh:
var st := P._begin()
var r := _rng(21)
P._cylinder(st, Vector3(0, -0.1, 0), Vector3(0, 0.62, 0), 0.025, 0.07, 7, PLANK_DARK, r, -1.0, PLANK)
return P._commit(st)
static func _campfire() -> ArrayMesh:
var st := P._begin()
var r := _rng(31)
var n := FastNoiseLite.new()
n.seed = 31
n.frequency = 1.6
for i in range(9):
var a := TAU * i / 9.0
var c := Vector3(cos(a) * 0.55, 0.06, sin(a) * 0.55)
P._blob(st, c, Vector3(0.15, 0.11, 0.13) * r.randf_range(0.85, 1.15), n, 0.3, P._mul(STONE, r.randf_range(0.8, 1.05)), r, 1)
# Teepee of logs leaning into the middle.
for i in range(5):
var a := TAU * i / 5.0 + 0.3
var base := Vector3(cos(a) * 0.36, 0.0, sin(a) * 0.36)
P._cylinder(st, base, Vector3(0, 0.55, 0), 0.05, 0.035, 5, P.BARK, r, -1.0, PLANK)
P._blob(st, Vector3(0, 0.03, 0), Vector3(0.3, 0.04, 0.3), n, 0.2, ASH, r, 1)
return P._commit(st)
static func _workbench() -> ArrayMesh:
var st := P._begin()
_box(st, Vector3(0, 0.82, 0), Vector3(1.8, 0.1, 0.9), PLANK)
_box(st, Vector3(0, 0.79, 0.47), Vector3(1.82, 0.06, 0.04), PLANK_DARK)
for sx in [-0.8, 0.8]:
for sz in [-0.36, 0.36]:
_box(st, Vector3(sx, 0.38, sz), Vector3(0.1, 0.8, 0.1), PLANK_DARK)
_box(st, Vector3(0, 0.25, 0), Vector3(1.6, 0.06, 0.7), PLANK_DARK)
# A few tools lying on top.
_box(st, Vector3(0.45, 0.89, 0.1), Vector3(0.35, 0.03, 0.05), HANDLE, Basis(Vector3.UP, 0.4))
_box(st, Vector3(0.6, 0.9, 0.18), Vector3(0.09, 0.06, 0.09), FLINT)
_box(st, Vector3(-0.5, 0.92, -0.15), Vector3(0.4, 0.1, 0.22), P.BARK)
return P._commit(st)
static func _chest() -> ArrayMesh:
var st := P._begin()
_box(st, Vector3(0, 0.3, 0), Vector3(1.1, 0.6, 0.7), PLANK)
_box(st, Vector3(0, 0.66, 0), Vector3(1.14, 0.14, 0.74), PLANK_DARK)
for sx in [-0.4, 0.4]:
_box(st, Vector3(sx, 0.36, 0), Vector3(0.07, 0.74, 0.76), IRON)
_box(st, Vector3(0, 0.55, 0.37), Vector3(0.12, 0.14, 0.03), IRON)
return P._commit(st)