M1b: survival stats, day/night, fire fuel, cooking, foraging, torches, death + grave
- Health, stamina, hunger, warmth and wet with HUD bars, status tags and clock - 20-minute day/night cycle with moon, stars and lit clouds - Campfire fuel (2 min start, 15 min max), heat radius, cooking needs a lit fire - Berry bushes (regrow 10 min), mushrooms, roasted mushroom, torch - Stamina costs for sprint, jump and harvest hits - Death respawns at island start and leaves a grave bag - Saves include stats, time of day, fire fuel, regrowth timers and graves - New test_survival_stats suite; tests fail if a script error aborts them
This commit is contained in:
@@ -15,6 +15,9 @@ var _hotbar: Array = []
|
||||
var _toasts: VBoxContainer
|
||||
var _screen: Control
|
||||
var _menu: Control
|
||||
var _bars: Dictionary = {}
|
||||
var _status: Label
|
||||
var _clock: Label
|
||||
|
||||
func bind(p: CharacterBody3D) -> void:
|
||||
player = p
|
||||
@@ -74,6 +77,8 @@ func bind(p: CharacterBody3D) -> void:
|
||||
_toasts.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(_toasts)
|
||||
|
||||
_build_stats(root)
|
||||
|
||||
_screen = InventoryScreen.new()
|
||||
root.add_child(_screen)
|
||||
_screen.bind(player)
|
||||
@@ -92,6 +97,63 @@ func bind(p: CharacterBody3D) -> void:
|
||||
ItemDB.icons_ready.connect(_refresh)
|
||||
_refresh()
|
||||
|
||||
## Bars bottom-left (health, stamina, food, warmth), status tags above them,
|
||||
## clock top-right.
|
||||
func _build_stats(root: Control) -> void:
|
||||
var box := VBoxContainer.new()
|
||||
box.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
box.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||
box.offset_left = 22
|
||||
box.offset_bottom = -22
|
||||
box.add_theme_constant_override("separation", 5)
|
||||
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(box)
|
||||
_status = UITheme.label("", 15, UITheme.TEXT)
|
||||
box.add_child(_status)
|
||||
for spec in [["health", "Health", Color(0.85, 0.22, 0.2)], ["stamina", "Stamina", Color(0.9, 0.78, 0.25)],
|
||||
["hunger", "Food", Color(0.85, 0.5, 0.2)], ["warmth", "Warmth", Color(0.95, 0.55, 0.3)]]:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var name_l := UITheme.label(spec[1], 14, UITheme.TEXT_DIM)
|
||||
name_l.custom_minimum_size = Vector2(62, 0)
|
||||
row.add_child(name_l)
|
||||
var bar := ProgressBar.new()
|
||||
bar.custom_minimum_size = Vector2(190, 14)
|
||||
bar.show_percentage = false
|
||||
bar.max_value = 100.0
|
||||
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
bar.add_theme_stylebox_override("background", UITheme.box(Color(0, 0, 0, 0.55), UITheme.SLOT_BORDER, 1, 4, 0))
|
||||
bar.add_theme_stylebox_override("fill", UITheme.box(spec[2], spec[2].lightened(0.2), 1, 4, 0))
|
||||
row.add_child(bar)
|
||||
box.add_child(row)
|
||||
_bars[spec[0]] = bar
|
||||
_clock = UITheme.label("", 18)
|
||||
_clock.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||||
_clock.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
_clock.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_clock.offset_right = -22
|
||||
_clock.offset_top = 14
|
||||
root.add_child(_clock)
|
||||
|
||||
func _process(_d: float) -> void:
|
||||
var s = player.survival
|
||||
for k in _bars:
|
||||
_bars[k].value = s.get(k)
|
||||
# Warmth bar shifts from icy blue (cold) to orange (warm).
|
||||
var w: float = s.warmth / 100.0
|
||||
var wc := Color(0.35, 0.6, 0.95).lerp(Color(0.95, 0.55, 0.3), w)
|
||||
(_bars["warmth"].get_theme_stylebox("fill") as StyleBoxFlat).bg_color = wc
|
||||
_status.text = " ".join(s.statuses)
|
||||
var dn = player.get_parent().day_night
|
||||
if dn != null:
|
||||
_clock.text = dn.clock_text()
|
||||
|
||||
## Closes the inventory / menu (e.g. on death).
|
||||
func close_screens() -> void:
|
||||
if _screen.is_open():
|
||||
_screen.close()
|
||||
|
||||
func _on_inventory_requested(container: Node) -> void:
|
||||
if not _screen.is_open():
|
||||
_screen.open(container)
|
||||
|
||||
@@ -132,7 +132,7 @@ func close() -> void:
|
||||
if left > 0 and container != null:
|
||||
left = container.inventory.add(_cursor.id, left)
|
||||
_cursor = null
|
||||
if container != null and container.inventory.changed.is_connected(_refresh):
|
||||
if container != null and is_instance_valid(container) and container.inventory.changed.is_connected(_refresh):
|
||||
container.inventory.changed.disconnect(_refresh)
|
||||
container = null
|
||||
visible = false
|
||||
@@ -147,12 +147,17 @@ func _input(event: InputEvent) -> void:
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion:
|
||||
_cursor_view.global_position = event.position
|
||||
# Walking away from a chest closes it.
|
||||
if container != null and player.global_position.distance_to(container.global_position) > 5.0:
|
||||
close()
|
||||
|
||||
|
||||
func _process(_d: float) -> void:
|
||||
if visible and container != null and player.global_position.distance_to(container.global_position) > 5.0:
|
||||
if not visible or container == null:
|
||||
return
|
||||
# Containers can vanish while open (an emptied grave crumbles).
|
||||
if not is_instance_valid(container) or container.is_queued_for_deletion():
|
||||
container = null
|
||||
_rebuild_chest()
|
||||
return
|
||||
if player.global_position.distance_to(container.global_position) > 5.0:
|
||||
close()
|
||||
|
||||
func _rebuild_chest() -> void:
|
||||
@@ -177,7 +182,7 @@ func _on_player_slot(i: int, right: bool, shift: bool) -> void:
|
||||
_refresh()
|
||||
|
||||
func _on_chest_slot(i: int, right: bool, shift: bool) -> void:
|
||||
if not Actions._in_reach(player, container, 1.0):
|
||||
if container == null or not is_instance_valid(container) or not Actions._in_reach(player, container, 1.0):
|
||||
return
|
||||
if shift:
|
||||
Actions.transfer(player, container.inventory, i, player.inventory, container)
|
||||
|
||||
@@ -7,7 +7,8 @@ const UITheme = preload("res://scripts/ui/theme.gd")
|
||||
|
||||
const CONTROLS := [
|
||||
["W A S D", "Move"], ["Shift", "Sprint"], ["Space", "Jump / swim up"],
|
||||
["Mouse", "Look"], ["E", "Interact / pick up"], ["Left click", "Use tool / place"],
|
||||
["Mouse", "Look"], ["E", "Interact / pick up / add fuel"], ["Left click", "Use tool / place"],
|
||||
["Right click (holding food)", "Eat"],
|
||||
["R", "Rotate piece"], ["1 - 8, wheel", "Hotbar"], ["Tab", "Inventory & crafting"],
|
||||
["Shift + click", "Move stack to/from a chest"], ["Right click", "Split stack"],
|
||||
["Esc", "This menu (game autosaves every 2 min)"],
|
||||
|
||||
Reference in New Issue
Block a user