M1c: weather, snap building, shelter, beds and regrowth
- Seeded weather (clear/cloudy/rain/storm, snow and blizzards on cold days and above the snowline), lightning, synthesised rain/wind/thunder sound - Rain/snow particles stopped by roofs; wet ground, settled snow and wind via global shader uniforms - Snap-together wood floor, wall, door, roof and gable with structural support, collapse and X to dismantle - Shelter detection: dry under a roof, warmer and Rested inside with a fire; rain burns an uncovered campfire faster - Bed sets the spawn point and lets you sleep through the night - Trees, boulders and ground pickups regrow; timers saved - Grass cleared under floors; shorter HUD hint - New test_weather_building suite (62 checks) and tests/run_all.sh, which also fails on script errors
This commit is contained in:
@@ -42,6 +42,12 @@ var _resources: Node3D ## harvestable trees/boulders (each its own body)
|
||||
var resource_nodes: Dictionary = {} ## uid -> resource node
|
||||
var pickups: Dictionary = {} ## uid -> pickup
|
||||
var forage: Dictionary = {} ## uid -> berry bush (forage.gd)
|
||||
## Regrowth: uid -> seconds until a felled tree, mined boulder or picked-up
|
||||
## item comes back. Saved with the world. Bushes keep their own timers.
|
||||
var regrow: Dictionary = {}
|
||||
const REGROW_SECONDS := {"tree": 2400.0, "rock": 3600.0, "pickup": 900.0}
|
||||
const REGROW_RETRY := 120.0 ## blocked (player or a building nearby): try again later
|
||||
var _regrow_tick := 0.0
|
||||
|
||||
var _bark_mat: StandardMaterial3D
|
||||
var _leaf_mat: ShaderMaterial
|
||||
@@ -101,12 +107,83 @@ func place_on(t: Node3D, player: Node3D, spawn: Vector3) -> void:
|
||||
func _process(delta: float) -> void:
|
||||
if _player == null:
|
||||
return
|
||||
_regrow_tick += delta
|
||||
if _regrow_tick >= 1.0:
|
||||
skip_time(_regrow_tick)
|
||||
_regrow_tick = 0.0
|
||||
_grass_timer -= delta
|
||||
if _grass_timer > 0.0:
|
||||
return
|
||||
_grass_timer = 0.2
|
||||
_update_grass(grass_tiles_per_update)
|
||||
|
||||
# --- regrowth ---------------------------------------------------------------
|
||||
|
||||
func _regrow_kind(uid: String) -> String:
|
||||
if pickups.has(uid):
|
||||
return "pickup"
|
||||
return resource_nodes[uid].kind
|
||||
|
||||
func _on_depleted(node: Node) -> void:
|
||||
regrow[node.uid] = REGROW_SECONDS[node.kind]
|
||||
|
||||
func _on_taken(pk: Node) -> void:
|
||||
regrow[pk.uid] = REGROW_SECONDS["pickup"]
|
||||
|
||||
## Advances regrowth timers by `secs` (every second while playing, or in one
|
||||
## go when you sleep).
|
||||
func skip_time(secs: float) -> void:
|
||||
for uid in regrow.keys():
|
||||
regrow[uid] -= secs
|
||||
if regrow[uid] <= 0.0:
|
||||
_try_regrow(uid)
|
||||
for uid in forage:
|
||||
var f = forage[uid]
|
||||
if secs > 1.5 and f.regrow_left > 0.0:
|
||||
f._process(secs)
|
||||
|
||||
func _try_regrow(uid: String) -> void:
|
||||
var node: Node3D = pickups.get(uid, resource_nodes.get(uid))
|
||||
if node == null:
|
||||
regrow.erase(uid)
|
||||
return
|
||||
var r := 0.5 if pickups.has(uid) else 1.4
|
||||
if _player != null and _player.global_position.distance_to(node.global_position) < r + 3.0 \
|
||||
or _built_over(node.global_position, r):
|
||||
regrow[uid] = REGROW_RETRY
|
||||
return
|
||||
regrow.erase(uid)
|
||||
node.regrow()
|
||||
|
||||
## Something built (or being built) where a tree or rock would come back.
|
||||
func _built_over(pos: Vector3, r: float) -> bool:
|
||||
var sph := SphereShape3D.new()
|
||||
sph.radius = r
|
||||
var q := PhysicsShapeQueryParameters3D.new()
|
||||
q.shape = sph
|
||||
q.transform = Transform3D(Basis(), pos + Vector3(0, r, 0))
|
||||
q.collision_mask = 4 # building pieces (build_piece.gd BUILD_LAYER)
|
||||
if not get_world_3d().direct_space_state.intersect_shape(q, 1).is_empty():
|
||||
return true
|
||||
# Chests, fires and beds aren't on the building layer.
|
||||
for p in get_tree().get_nodes_in_group("piece"):
|
||||
if (p as Node3D).global_position.distance_to(pos) < r + 0.8:
|
||||
return true
|
||||
return false
|
||||
|
||||
## From a save. Anything gone without a saved timer (older saves) starts a
|
||||
## fresh one.
|
||||
func restore_regrow(saved: Dictionary) -> void:
|
||||
for uid in saved:
|
||||
if pickups.has(uid) or resource_nodes.has(uid):
|
||||
regrow[uid] = float(saved[uid])
|
||||
for uid in resource_nodes:
|
||||
if resource_nodes[uid].depleted and not regrow.has(uid):
|
||||
regrow[uid] = REGROW_SECONDS[resource_nodes[uid].kind]
|
||||
for uid in pickups:
|
||||
if pickups[uid].taken and not regrow.has(uid):
|
||||
regrow[uid] = REGROW_SECONDS["pickup"]
|
||||
|
||||
# --- materials --------------------------------------------------------------
|
||||
|
||||
func _make_materials() -> void:
|
||||
@@ -205,6 +282,7 @@ func _resource_node(kind: String, xf: Transform3D) -> StaticBody3D:
|
||||
node.drops_final = {"stone": [3, 5], "flint": [1, 2]}
|
||||
node.chip_color = Color(0.55, 0.54, 0.52)
|
||||
node.hp = node.max_hp
|
||||
node.depleted_now.connect(_on_depleted)
|
||||
_resources.add_child(node)
|
||||
node.global_position = xf.origin
|
||||
return node
|
||||
@@ -539,6 +617,7 @@ func _scatter_pickups() -> void:
|
||||
pk.setup(id, mesh, ItemDB.item_material(), radius[id])
|
||||
pk.uid = "p%d" % pickups.size()
|
||||
pickups[pk.uid] = pk
|
||||
pk.taken_now.connect(_on_taken)
|
||||
root.add_child(pk)
|
||||
pk.global_transform = Transform3D(_ground_basis(n, 1.0, yaw, 1.0), Vector3(x, h - 0.02, z))
|
||||
placed += 1
|
||||
@@ -627,6 +706,12 @@ func _make_grass_tile(k: Vector2i) -> Node3D:
|
||||
var flower_xf: Array = []
|
||||
var flower_col: Array = []
|
||||
var steps := int(GRASS_TILE / grass_spacing)
|
||||
# Floors that overlap this tile (grass doesn't grow through them).
|
||||
var cuts: Array = []
|
||||
var tile := Rect2(ox, oz, GRASS_TILE, GRASS_TILE).grow(2.0)
|
||||
for c in _grass_cuts:
|
||||
if tile.has_point(Vector2(c[1].x, c[1].z)):
|
||||
cuts.append(c)
|
||||
for iz in range(steps):
|
||||
for ix in range(steps):
|
||||
var x := ox + (ix + rng.randf()) * grass_spacing
|
||||
@@ -641,6 +726,8 @@ func _make_grass_tile(k: Vector2i) -> Node3D:
|
||||
var n: Vector3 = terrain.sample_normal(x, z)
|
||||
if n.y < 0.72:
|
||||
continue
|
||||
if not cuts.is_empty() and _under_cut(cuts, x, z):
|
||||
continue
|
||||
var meadow := _meadow_noise.get_noise_2d(x, z)
|
||||
var dens := smoothstep(-0.45, 0.1, meadow) * (1.0 - smoothstep(0.85, 0.72, n.y) * 0.7)
|
||||
if keep > dens:
|
||||
@@ -666,6 +753,38 @@ func _make_grass_tile(k: Vector2i) -> Node3D:
|
||||
_grass_root.add_child(node)
|
||||
return node
|
||||
|
||||
## Floor footprints to keep clear of grass: [world->local transform, centre,
|
||||
## half width]. Set by world.gd whenever floors are built or removed; only the
|
||||
## grass tiles around old and new floors are rebuilt.
|
||||
var _grass_cuts: Array = []
|
||||
|
||||
func set_grass_cuts(xforms: Array, half: float) -> void:
|
||||
var old := _grass_cuts
|
||||
_grass_cuts = []
|
||||
for xf: Transform3D in xforms:
|
||||
_grass_cuts.append([xf.affine_inverse(), xf.origin, half])
|
||||
var touched := {}
|
||||
for c in old + _grass_cuts:
|
||||
var p: Vector3 = c[1]
|
||||
for dz in [-3.0, 0.0, 3.0]:
|
||||
for dx in [-3.0, 0.0, 3.0]:
|
||||
touched[Vector2i(floori((p.x + dx) / GRASS_TILE), floori((p.z + dz) / GRASS_TILE))] = true
|
||||
for k in touched:
|
||||
if _grass_tiles.has(k):
|
||||
var node = _grass_tiles[k]
|
||||
if node != null and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_grass_tiles.erase(k)
|
||||
if _player != null:
|
||||
_update_grass(touched.size())
|
||||
|
||||
static func _under_cut(cuts: Array, x: float, z: float) -> bool:
|
||||
for c in cuts:
|
||||
var l: Vector3 = (c[0] as Transform3D) * Vector3(x, (c[1] as Vector3).y, z)
|
||||
if absf(l.x) < c[2] and absf(l.z) < c[2]:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _fade(gi: GeometryInstance3D, dist: float) -> void:
|
||||
gi.visibility_range_end = dist
|
||||
gi.visibility_range_end_margin = 10.0
|
||||
|
||||
Reference in New Issue
Block a user