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:
99
scripts/building/campfire.gd
Normal file
99
scripts/building/campfire.gd
Normal file
@@ -0,0 +1,99 @@
|
||||
extends "res://scripts/building/piece.gd"
|
||||
## Campfire: flickering light, flames, embers and smoke. A "campfire" station
|
||||
## (cooking arrives in M1b along with fuel and warmth).
|
||||
|
||||
var _light: OmniLight3D
|
||||
var _t := 0.0
|
||||
var _noise := FastNoiseLite.new()
|
||||
|
||||
func _build() -> void:
|
||||
add_to_group("station:campfire")
|
||||
_noise.frequency = 2.5
|
||||
_light = OmniLight3D.new()
|
||||
_light.light_color = Color(1.0, 0.62, 0.3)
|
||||
_light.light_energy = 2.2
|
||||
_light.omni_range = 11.0
|
||||
_light.omni_attenuation = 1.4
|
||||
_light.shadow_enabled = true
|
||||
_light.position = Vector3(0, 0.7, 0)
|
||||
add_child(_light)
|
||||
|
||||
add_child(_particles("Flames", 26, 0.55, Vector3(0, 0.12, 0), 0.5, 1.0, 0.5, 0.9, _gradient([
|
||||
[0.0, Color(1.0, 0.8, 0.35, 0.0)], [0.15, Color(1.0, 0.62, 0.2, 0.75)],
|
||||
[0.6, Color(0.95, 0.3, 0.06, 0.45)], [1.0, Color(0.5, 0.08, 0.02, 0.0)]]), true, 0.32, 0.12))
|
||||
add_child(_particles("Embers", 10, 2.0, Vector3(0, 0.3, 0), 0.8, 2.0, 0.5, 1.0, _gradient([
|
||||
[0.0, Color(1.0, 0.75, 0.35, 1.0)], [1.0, Color(1.0, 0.3, 0.05, 0.0)]]), true, 0.04, 0.15))
|
||||
var smoke := _particles("Smoke", 10, 5.0, Vector3(0, 1.0, 0), 0.6, 1.1, 0.6, 1.4, _gradient([
|
||||
[0.0, Color(0.55, 0.54, 0.52, 0.0)], [0.25, Color(0.58, 0.57, 0.55, 0.07)],
|
||||
[1.0, Color(0.7, 0.7, 0.7, 0.0)]]), false, 0.9, 0.15)
|
||||
smoke.spread = 25.0
|
||||
smoke.gravity = Vector3(0.35, 0.25, 0.1) # drifts with the wind
|
||||
# TODO(M1b): smoke renders as an opaque dark ball whatever its ramp colour
|
||||
# (alpha-blended unshaded particles; additive flames are fine). Disabled
|
||||
# until that's fixed, likely by moving to a small particle shader.
|
||||
smoke.queue_free()
|
||||
|
||||
func display_name() -> String:
|
||||
return "Campfire"
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_t += delta
|
||||
var f := _noise.get_noise_1d(_t * 10.0)
|
||||
_light.light_energy = 2.2 + f * 0.7
|
||||
_light.position = Vector3(f * 0.05, 0.7 + f * 0.05, 0)
|
||||
|
||||
static func _gradient(stops: Array) -> Gradient:
|
||||
var g := Gradient.new()
|
||||
g.offsets = PackedFloat32Array(stops.map(func(s): return s[0]))
|
||||
g.colors = PackedColorArray(stops.map(func(s): return s[1]))
|
||||
return g
|
||||
|
||||
static func _soft_dot() -> GradientTexture2D:
|
||||
var g := Gradient.new()
|
||||
g.offsets = PackedFloat32Array([0.0, 1.0])
|
||||
g.colors = PackedColorArray([Color(1, 1, 1, 1), Color(1, 1, 1, 0)])
|
||||
var t := GradientTexture2D.new()
|
||||
t.gradient = g
|
||||
t.fill = GradientTexture2D.FILL_RADIAL
|
||||
t.fill_from = Vector2(0.5, 0.5)
|
||||
t.fill_to = Vector2(0.5, 0.0)
|
||||
t.width = 64
|
||||
t.height = 64
|
||||
return t
|
||||
|
||||
func _particles(label: String, amount: int, lifetime: float, pos: Vector3, vmin: float, vmax: float,
|
||||
smin: float, smax: float, ramp: Gradient, additive: bool, quad_size: float, radius: float) -> CPUParticles3D:
|
||||
var p := CPUParticles3D.new()
|
||||
p.name = label
|
||||
p.amount = amount
|
||||
p.lifetime = lifetime
|
||||
p.position = pos
|
||||
p.direction = Vector3.UP
|
||||
p.spread = 12.0
|
||||
p.initial_velocity_min = vmin
|
||||
p.initial_velocity_max = vmax
|
||||
p.gravity = Vector3(0.15, 0.4, 0.05)
|
||||
p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
|
||||
p.emission_sphere_radius = radius
|
||||
p.scale_amount_min = smin
|
||||
p.scale_amount_max = smax
|
||||
p.color_ramp = ramp
|
||||
# Flames shrink as they rise.
|
||||
var curve := Curve.new()
|
||||
curve.add_point(Vector2(0.0, 1.0))
|
||||
curve.add_point(Vector2(1.0, 0.35 if additive else 1.6))
|
||||
p.scale_amount_curve = curve
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2.ONE * quad_size
|
||||
var m := StandardMaterial3D.new()
|
||||
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
m.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
||||
m.vertex_color_use_as_albedo = true
|
||||
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
m.blend_mode = BaseMaterial3D.BLEND_MODE_ADD if additive else BaseMaterial3D.BLEND_MODE_MIX
|
||||
m.albedo_texture = _soft_dot()
|
||||
if additive:
|
||||
m.albedo_color = Color(1.5, 1.5, 1.5) # mildly HDR so the glow picks it up
|
||||
quad.material = m
|
||||
p.mesh = quad
|
||||
return p
|
||||
1
scripts/building/campfire.gd.uid
Normal file
1
scripts/building/campfire.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://di4xtvyvo151n
|
||||
15
scripts/building/chest.gd
Normal file
15
scripts/building/chest.gd
Normal file
@@ -0,0 +1,15 @@
|
||||
extends "res://scripts/building/piece.gd"
|
||||
## Wooden chest: 16 slots of storage. E opens it next to your inventory.
|
||||
|
||||
const SLOTS := 16
|
||||
var inventory := Inventory.new(SLOTS)
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
var used := 0
|
||||
for s in inventory.slots:
|
||||
if s != null:
|
||||
used += 1
|
||||
return "Chest (%d/%d) [E] Open" % [used, SLOTS]
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
player.open_inventory(self)
|
||||
1
scripts/building/chest.gd.uid
Normal file
1
scripts/building/chest.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yma48ens54gt
|
||||
36
scripts/building/piece.gd
Normal file
36
scripts/building/piece.gd
Normal file
@@ -0,0 +1,36 @@
|
||||
extends StaticBody3D
|
||||
## Base for placed pieces (campfire, workbench, chest, later walls/floors).
|
||||
## Builds the visual from the item's procedural mesh and a box collider from
|
||||
## the item's footprint. Subclasses add behaviour.
|
||||
|
||||
var item_id := ""
|
||||
var def: Dictionary = {}
|
||||
|
||||
func setup(d: Dictionary) -> void:
|
||||
def = d
|
||||
item_id = d.id
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.name = "Mesh"
|
||||
mi.mesh = ItemDB.mesh_for(item_id)
|
||||
mi.material_override = ItemDB.item_material()
|
||||
add_child(mi)
|
||||
var aabb := mi.mesh.get_aabb()
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(aabb.size.x, maxf(aabb.size.y, 0.3), aabb.size.z)
|
||||
cs.shape = box
|
||||
cs.position = aabb.get_center()
|
||||
add_child(cs)
|
||||
_build()
|
||||
|
||||
func _build() -> void:
|
||||
pass
|
||||
|
||||
func display_name() -> String:
|
||||
return ItemDB.item_name(item_id)
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
return display_name()
|
||||
|
||||
func interact(_player: Node) -> void:
|
||||
pass
|
||||
1
scripts/building/piece.gd.uid
Normal file
1
scripts/building/piece.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c65x7w4vb84ua
|
||||
11
scripts/building/workbench.gd
Normal file
11
scripts/building/workbench.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
extends "res://scripts/building/piece.gd"
|
||||
## Workbench: unlocks "workbench" recipes within 6 m. E opens crafting.
|
||||
|
||||
func _build() -> void:
|
||||
add_to_group("station:workbench")
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
return "Workbench [E] Craft"
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
player.open_inventory(null)
|
||||
1
scripts/building/workbench.gd.uid
Normal file
1
scripts/building/workbench.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ciu8ajr75fv2a
|
||||
Reference in New Issue
Block a user