Files
Emberwild/scripts/ui/inventory_screen.gd
Paul Bailey fed96ef118 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
2026-11-04 10:16:41 +00:00

273 lines
9.2 KiB
GDScript

extends Control
## Inventory screen (Tab): your 40 slots, the crafting list, and an open
## chest beside them.
##
## Click picks up or puts down a stack, right-click splits it or drops one,
## shift-click quick-moves between you and the chest. Crafting shows every
## recipe; the ones you can't make yet are dimmed, with the missing inputs and
## station in red. All changes go through Actions.
const UITheme = preload("res://scripts/ui/theme.gd")
const SlotScript = preload("res://scripts/ui/slot.gd")
const COLS := 8
var player: CharacterBody3D
var container: Node = null ## open chest, or null
var _player_slots: Array = []
var _chest_slots: Array = []
var _chest_panel: PanelContainer
var _chest_grid: GridContainer
var _chest_title: Label
var _recipe_list: VBoxContainer
var _station_label: Label
var _cursor = null ## stack held on the mouse
var _cursor_view: Control
var _cursor_icon: TextureRect
var _cursor_count: Label
func bind(p: CharacterBody3D) -> void:
player = p
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
visible = false
var dim := ColorRect.new()
dim.color = Color(0, 0, 0, 0.35)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(center)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 16)
center.add_child(row)
# Chest panel (only while a chest is open).
_chest_panel = UITheme.panel()
var cv := VBoxContainer.new()
_chest_title = UITheme.label("Chest", 20)
cv.add_child(_chest_title)
_chest_grid = GridContainer.new()
_chest_grid.columns = 4
cv.add_child(_chest_grid)
_chest_panel.add_child(cv)
row.add_child(_chest_panel)
# Player inventory.
var pp := UITheme.panel()
var pv := VBoxContainer.new()
pv.add_theme_constant_override("separation", 8)
pv.add_child(UITheme.label("Inventory", 20))
var grid := GridContainer.new()
grid.columns = COLS
for i in range(player.inventory.size()):
var s: Panel = SlotScript.new(i, 60, str(i + 1) if i < 8 else "")
s.clicked.connect(_on_player_slot)
grid.add_child(s)
_player_slots.append(s)
if i == 7:
pass
pv.add_child(grid)
pv.add_child(UITheme.label("Row 1 is your hotbar. Shift-click moves stacks to/from a chest.", 13, UITheme.TEXT_DIM))
pp.add_child(pv)
row.add_child(pp)
# Crafting.
var cp := UITheme.panel()
cp.custom_minimum_size = Vector2(340, 0)
var crv := VBoxContainer.new()
crv.add_theme_constant_override("separation", 6)
crv.add_child(UITheme.label("Crafting", 20))
_station_label = UITheme.label("", 13, UITheme.TEXT_DIM)
crv.add_child(_station_label)
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(320, 380)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_recipe_list = VBoxContainer.new()
_recipe_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_recipe_list.add_theme_constant_override("separation", 6)
scroll.add_child(_recipe_list)
crv.add_child(scroll)
cp.add_child(crv)
row.add_child(cp)
# Stack on the cursor.
_cursor_view = Control.new()
_cursor_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
_cursor_view.top_level = true
_cursor_icon = TextureRect.new()
_cursor_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_cursor_icon.size = Vector2(52, 52)
_cursor_icon.position = Vector2(-26, -26)
_cursor_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
_cursor_view.add_child(_cursor_icon)
_cursor_count = UITheme.label("", 15)
_cursor_count.position = Vector2(8, 6)
_cursor_view.add_child(_cursor_count)
add_child(_cursor_view)
player.inventory.changed.connect(_refresh)
ItemDB.icons_ready.connect(_refresh)
func is_open() -> bool:
return visible
func open(chest: Node) -> void:
container = chest
if container != null:
container.inventory.changed.connect(_refresh)
visible = true
player.ui_open = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_rebuild_chest()
_refresh()
func close() -> void:
if _cursor != null:
var left: int = player.inventory.add(_cursor.id, _cursor.count)
if left > 0 and container != null:
left = container.inventory.add(_cursor.id, left)
_cursor = null
if container != null and is_instance_valid(container) and container.inventory.changed.is_connected(_refresh):
container.inventory.changed.disconnect(_refresh)
container = null
visible = false
player.ui_open = false
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event: InputEvent) -> void:
if not visible:
return
if event.is_action_pressed("inventory"): # ESC is handled by the HUD
close()
get_viewport().set_input_as_handled()
elif event is InputEventMouseMotion:
_cursor_view.global_position = event.position
func _process(_d: float) -> void:
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:
for c in _chest_grid.get_children():
c.queue_free()
_chest_slots.clear()
_chest_panel.visible = container != null
if container == null:
return
_chest_title.text = container.display_name()
for i in range(container.inventory.size()):
var s: Panel = SlotScript.new(i, 60)
s.clicked.connect(_on_chest_slot)
_chest_grid.add_child(s)
_chest_slots.append(s)
func _on_player_slot(i: int, right: bool, shift: bool) -> void:
if shift and container != null:
Actions.transfer(player, player.inventory, i, container.inventory, container)
else:
_cursor = player.inventory.click(i, _cursor, right)
_refresh()
func _on_chest_slot(i: int, right: bool, shift: bool) -> void:
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)
else:
_cursor = container.inventory.click(i, _cursor, right)
_refresh()
func _refresh() -> void:
if player == null:
return
for i in range(_player_slots.size()):
_player_slots[i].set_stack(player.inventory.get_slot(i))
_player_slots[i].set_selected(i == player.selected)
if container != null:
for i in range(_chest_slots.size()):
_chest_slots[i].set_stack(container.inventory.get_slot(i))
_cursor_view.visible = _cursor != null
if _cursor != null:
_cursor_icon.texture = ItemDB.icon(_cursor.id)
_cursor_count.text = str(_cursor.count) if _cursor.count > 1 else ""
_rebuild_recipes()
func _rebuild_recipes() -> void:
if not visible:
return
for c in _recipe_list.get_children():
c.queue_free()
var stations: Array[String] = []
for st in ["workbench", "campfire"]:
if Actions.station_near(player, st):
stations.append(st)
_station_label.text = "Nearby: " + (", ".join(stations) if not stations.is_empty() else "nothing (hand crafting only)")
for rid in ItemDB.recipes:
_recipe_list.add_child(_recipe_row(ItemDB.recipes[rid]))
func _recipe_row(r: Dictionary) -> Control:
var ok := Actions.can_craft(player, r.id)
var row := PanelContainer.new()
row.add_theme_stylebox_override("panel", UITheme.box(UITheme.SLOT_BG, UITheme.SELECTED if ok else UITheme.SLOT_BORDER, 1, 5, 6))
var h := HBoxContainer.new()
h.add_theme_constant_override("separation", 8)
row.add_child(h)
var icon := TextureRect.new()
icon.texture = ItemDB.icon(r.output)
icon.custom_minimum_size = Vector2(44, 44)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.modulate = Color.WHITE if ok else Color(1, 1, 1, 0.45)
h.add_child(icon)
var v := VBoxContainer.new()
v.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var title := ItemDB.item_name(r.output) + (" x%d" % r.count if r.count > 1 else "")
v.add_child(UITheme.label(title, 16, UITheme.TEXT if ok else UITheme.TEXT_DIM))
var req := RichTextLabel.new()
req.bbcode_enabled = true
req.fit_content = true
req.scroll_active = false
req.autowrap_mode = TextServer.AUTOWRAP_OFF
req.add_theme_font_size_override("normal_font_size", 13)
var parts: Array[String] = []
for id in r.inputs:
var need := int(r.inputs[id])
var have: int = player.inventory.count(id)
var col := UITheme.OK if have >= need else UITheme.BAD
parts.append("[color=#%s]%s %d/%d[/color]" % [col.to_html(false), ItemDB.item_name(id), have, need])
if not r.station.is_empty():
var col := UITheme.OK if Actions.station_near(player, r.station) else UITheme.BAD
parts.append("[color=#%s]@ %s[/color]" % [col.to_html(false), r.station])
req.text = " ".join(parts)
req.custom_minimum_size = Vector2(200, 0)
v.add_child(req)
h.add_child(v)
var b := Button.new()
b.text = "Craft"
b.disabled = not ok
b.focus_mode = Control.FOCUS_NONE
b.pressed.connect(_craft.bind(r.id))
h.add_child(b)
return row
## Shift-click crafts up to five (handy for walls and floors).
func _craft(recipe_id: String) -> void:
var n := 5 if Input.is_key_pressed(KEY_SHIFT) else 1
for i in range(n):
if not Actions.can_craft(player, recipe_id) or not Actions.craft(player, recipe_id):
break
_refresh()