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:
@@ -11,6 +11,7 @@ extends Node3D
|
||||
signal prompt_changed(text: String)
|
||||
|
||||
const Campfire = preload("res://scripts/building/campfire.gd")
|
||||
const BuildPiece = preload("res://scripts/building/build_piece.gd")
|
||||
const REACH := 3.2
|
||||
const PLACE_REACH := 6.0
|
||||
const SWING_TIME := 0.5
|
||||
@@ -26,6 +27,7 @@ var _ghost_bad_mat: StandardMaterial3D
|
||||
var _ghost_yaw := 0.0
|
||||
var _place_xf := Transform3D()
|
||||
var _place_ok := false
|
||||
var _place_reason := ""
|
||||
var _prompt := ""
|
||||
var _held_shown := "~"
|
||||
var _torch_light: OmniLight3D
|
||||
@@ -123,6 +125,8 @@ func _target() -> Object:
|
||||
if not _ray.is_colliding():
|
||||
return null
|
||||
var c := _ray.get_collider()
|
||||
if c != null and c.has_meta("owner_piece"):
|
||||
c = c.get_meta("owner_piece") # a door's moving panel
|
||||
if c == null or not c.has_method("interact_prompt"):
|
||||
return null
|
||||
if c.get("depleted") == true:
|
||||
@@ -138,7 +142,8 @@ func _physics_process(delta: float) -> void:
|
||||
var def := _held_def()
|
||||
if def.has("place"):
|
||||
_update_ghost(def)
|
||||
_set_prompt("[LMB] Place %s [R] Rotate" % def.name)
|
||||
var why := "" if _place_ok or _place_reason.is_empty() else " (%s)" % _place_reason
|
||||
_set_prompt("[LMB] Place %s [R] Rotate%s" % [def.name, why])
|
||||
return
|
||||
_ghost.visible = false
|
||||
if _torch_light.visible:
|
||||
@@ -146,7 +151,10 @@ func _physics_process(delta: float) -> void:
|
||||
_torch_light.light_energy = float(def.get("light", {}).get("energy", 1.5)) * (0.85 + 0.15 * sin(_t * 17.0) * sin(_t * 5.3))
|
||||
var t := _target()
|
||||
if t != null:
|
||||
_set_prompt(t.interact_prompt(player))
|
||||
var p: String = t.interact_prompt(player)
|
||||
if t.is_in_group("piece") and t.item_id != "grave":
|
||||
p += " [X] Dismantle"
|
||||
_set_prompt(p)
|
||||
elif def.has("food"):
|
||||
_set_prompt("[RMB] Eat %s" % def.name)
|
||||
else:
|
||||
@@ -163,8 +171,16 @@ func _update_ghost(def: Dictionary) -> void:
|
||||
_place_ok = false
|
||||
return
|
||||
var yaw := player.rotation.y + _ghost_yaw # piece's front (+Z) faces the player
|
||||
_place_xf = Transform3D(Basis(Vector3.UP, yaw), hit.position)
|
||||
_place_ok = player.get_parent().placement_valid(def, _place_xf)
|
||||
if BuildPiece.is_build(def):
|
||||
# Building pieces snap to what's already built (build_system.gd).
|
||||
var res: Dictionary = player.get_parent().build.placement(def, hit, yaw)
|
||||
_place_xf = res.xf
|
||||
_place_ok = res.ok
|
||||
_place_reason = res.reason
|
||||
else:
|
||||
_place_xf = Transform3D(Basis(Vector3.UP, yaw), hit.position)
|
||||
_place_ok = player.get_parent().placement_valid(def, _place_xf)
|
||||
_place_reason = "" if _place_ok else "blocked or too steep"
|
||||
_ghost.global_transform = _place_xf
|
||||
_ghost.material_override = _ghost_ok_mat if _place_ok else _ghost_bad_mat
|
||||
_ghost.visible = true
|
||||
@@ -185,7 +201,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
if _held_def().has("food"):
|
||||
Actions.eat(player, player.selected)
|
||||
elif event.is_action_pressed("rotate_piece"):
|
||||
_ghost_yaw += deg_to_rad(22.5)
|
||||
# Building pieces turn in quarter turns (they snap to a grid).
|
||||
_ghost_yaw += deg_to_rad(90.0 if BuildPiece.is_build(_held_def()) else 22.5)
|
||||
elif event.is_action_pressed("dismantle"):
|
||||
var t := _target()
|
||||
if t != null and t.is_in_group("piece"):
|
||||
Actions.dismantle(player, t)
|
||||
elif event.is_action_pressed("interact"):
|
||||
var t := _target()
|
||||
if t != null and t.has_method("interact"):
|
||||
|
||||
@@ -19,6 +19,8 @@ const HUNGER_DRAIN := 100.0 / 1500.0 ## full to empty in ~1.25 in-game days
|
||||
const STAMINA_REGEN := 24.0
|
||||
const STAMINA_DELAY := 1.0
|
||||
const WET_SECONDS := 90.0
|
||||
const REST_AFTER := 20.0 ## seconds sheltered by a fire before you're Rested
|
||||
const REST_SECONDS := 300.0 ## Rested: stamina regenerates 50% faster
|
||||
|
||||
var health := MAX
|
||||
var stamina := MAX
|
||||
@@ -28,6 +30,11 @@ var wet := 0.0 ## seconds of "Wet" left
|
||||
var heat := 0.0 ## 0..1 from nearby fires etc (last step)
|
||||
var statuses: Array[String] = []
|
||||
var dead := false
|
||||
var roofed := false ## a roof overhead (rain can't reach you)
|
||||
var sheltered := false ## roof and walls (the weather barely reaches you)
|
||||
var rested := 0.0 ## seconds of Rested left
|
||||
var _shelter_left := 0.0
|
||||
var _rest_build := 0.0
|
||||
|
||||
var player: CharacterBody3D
|
||||
var world: Node3D
|
||||
@@ -42,7 +49,23 @@ func step(dt: float) -> void:
|
||||
return
|
||||
var p := player.global_position
|
||||
heat = world.heat_at(p)
|
||||
var target: float = world.air_warmth(p) + heat * 70.0
|
||||
_shelter_left -= dt
|
||||
if _shelter_left <= 0.0:
|
||||
_shelter_left = 0.25
|
||||
roofed = world.build.roofed_at(p)
|
||||
sheltered = roofed and world.build.enclosure_at(p) >= 0.74
|
||||
# Rain soaks you unless there's a roof over your head.
|
||||
if not roofed and world.weather.rain_at(p) > 0.25:
|
||||
wet = WET_SECONDS
|
||||
# Resting: a while by a fire with a roof and walls around you.
|
||||
if sheltered and heat > 0.25:
|
||||
_rest_build += dt
|
||||
if _rest_build >= REST_AFTER:
|
||||
rested = maxf(rested, REST_SECONDS)
|
||||
else:
|
||||
_rest_build = 0.0
|
||||
rested = maxf(rested - dt, 0.0)
|
||||
var target: float = world.air_warmth(p, sheltered) + heat * 70.0
|
||||
if wet > 0.0:
|
||||
target -= 22.0
|
||||
wet = maxf(wet - dt * (3.0 if heat > 0.3 else 1.0), 0.0) # fires dry you off
|
||||
@@ -55,7 +78,7 @@ func step(dt: float) -> void:
|
||||
if _stamina_wait > 0.0:
|
||||
_stamina_wait -= dt
|
||||
else:
|
||||
stamina = minf(stamina + STAMINA_REGEN * dt, MAX)
|
||||
stamina = minf(stamina + STAMINA_REGEN * (1.5 if rested > 0.0 else 1.0) * dt, MAX)
|
||||
|
||||
var dmg := 0.0
|
||||
if hunger <= 0.0:
|
||||
@@ -87,6 +110,10 @@ func _update_statuses() -> void:
|
||||
statuses.append("Hungry")
|
||||
if wet > 0.0:
|
||||
statuses.append("Wet")
|
||||
if sheltered:
|
||||
statuses.append("Sheltered")
|
||||
if rested > 0.0:
|
||||
statuses.append("Rested")
|
||||
|
||||
## Spends stamina for an action. Returns false (and spends nothing) if you're
|
||||
## too tired. Continuous costs (sprinting) pass `partial = true`.
|
||||
@@ -105,6 +132,16 @@ func eat(food: Dictionary) -> void:
|
||||
func soak() -> void:
|
||||
wet = WET_SECONDS
|
||||
|
||||
## After a night's sleep: a little hungrier, dry, warm and rested.
|
||||
func after_sleep(seconds: float) -> void:
|
||||
hunger = maxf(hunger - HUNGER_DRAIN * seconds * 0.3, 0.0)
|
||||
wet = 0.0
|
||||
warmth = maxf(warmth, 60.0)
|
||||
stamina = MAX
|
||||
rested = REST_SECONDS * 2.0
|
||||
_update_statuses()
|
||||
changed.emit()
|
||||
|
||||
func reset_after_death() -> void:
|
||||
dead = false
|
||||
health = 50.0
|
||||
@@ -115,7 +152,7 @@ func reset_after_death() -> void:
|
||||
changed.emit()
|
||||
|
||||
func to_data() -> Dictionary:
|
||||
return {"health": health, "stamina": stamina, "hunger": hunger, "warmth": warmth, "wet": wet}
|
||||
return {"health": health, "stamina": stamina, "hunger": hunger, "warmth": warmth, "wet": wet, "rested": rested}
|
||||
|
||||
func load_data(d: Dictionary) -> void:
|
||||
health = float(d.get("health", health))
|
||||
@@ -123,5 +160,6 @@ func load_data(d: Dictionary) -> void:
|
||||
hunger = float(d.get("hunger", hunger))
|
||||
warmth = float(d.get("warmth", warmth))
|
||||
wet = float(d.get("wet", 0.0))
|
||||
rested = float(d.get("rested", 0.0))
|
||||
dead = false
|
||||
changed.emit()
|
||||
|
||||
Reference in New Issue
Block a user