Add Cooler, Bag, and compact hotbar systems

This commit is contained in:
Alexander Sellite 2026-07-25 20:12:29 -04:00
parent 8cd3e83285
commit 49e7faaf84
32 changed files with 1613 additions and 99 deletions

View file

@ -10,6 +10,10 @@ const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const PlayerMenuType = preload("res://ui/player_menu.gd")
const PlayerType = preload("res://player/player.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const HotbarUIType = preload("res://ui/hotbar.gd")
const TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.gd")
@ -26,10 +30,12 @@ const PauseMenuType = preload("res://ui/pause_menu.gd")
@onready var _screen_fade: ScreenFade = %ScreenFade
@onready var _title_screen: TitleScreenType = %TitleScreen
@onready var _pause_menu: PauseMenuType = %PauseMenu
@onready var _hotbar_ui: HotbarUIType = %Hotbar
var _showcase_active: bool = false
var _player_menu_open: bool = false
var _gameplay_ui_enabled: bool = false
var _fishing_spot: FishingSpotType
func setup(
@ -41,7 +47,11 @@ func setup(
default_buyer: FishBuyerProfileType,
catalog: FishPoolType,
fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void:
_fishing_spot = fishing_spot
fishing_spot.status_changed.connect(_on_fishing_status_changed)
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
fishing_spot.showcase_changed.connect(_on_showcase_changed)
@ -56,8 +66,12 @@ func setup(
sale_service,
default_buyer,
catalog,
fishing_spot
fishing_spot,
bag,
hotbar,
item_catalog
)
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot)
func close_player_menu() -> void:
@ -89,10 +103,22 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
if not enabled:
close_player_menu_for_session_end()
_fishing_panel.visible = false
_hotbar_ui.hide()
_hotbar_ui.set_gameplay_input_enabled(false)
else:
_hotbar_ui.show()
_hotbar_ui.set_gameplay_input_enabled(true)
_refresh_fishing_panel_visibility()
func set_system_menu_open(is_open: bool) -> void:
_hotbar_ui.visible = _gameplay_ui_enabled and not is_open
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled and not is_open and not _player_menu_open
)
_hotbar_ui.set_drag_enabled(_player_menu_open and not is_open)
func get_screen_fade() -> ScreenFade:
return _screen_fade
@ -104,6 +130,8 @@ func get_title_screen() -> TitleScreenType:
func _on_fishing_status_changed(status: String) -> void:
if _showcase_active:
return
if _fishing_spot != null and _fishing_spot.is_fighting():
return
_set_fishing_status(status)
@ -125,7 +153,6 @@ func _refresh_fishing_panel_visibility() -> void:
_fishing_panel.visible = (
_gameplay_ui_enabled
and has_content
and not _player_menu_open
)
@ -138,17 +165,26 @@ func _on_catch_display_changed(
active_barrier_index: int,
visible: bool,
) -> void:
var encounter_visible: bool = (
visible
and _fishing_spot != null
and _fishing_spot.is_fighting()
)
_hotbar_ui.set_item_name_suppressed(encounter_visible)
_green_catch_progress.value = progress * 100.0
_red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0
_catch_track.visible = visible
_barrier_summary.visible = visible
if not visible:
_catch_track.visible = encounter_visible
_barrier_summary.visible = encounter_visible
_barrier_health.visible = encounter_visible
if not encounter_visible:
_barrier_health.visible = false
_clear_barrier_markers()
_barrier_summary.text = ""
_barrier_health.text = ""
_refresh_fishing_panel_visibility()
return
_status_label.text = ""
_status_label.visible = false
_update_barrier_markers(
barrier_positions,
@ -183,7 +219,7 @@ func _on_catch_display_changed(
]
)
else:
_barrier_health.text = ""
_barrier_health.text = "Hold left click to reel"
var chase_gap: float = progress - chase_progress
if chase_gap <= 0.05:
@ -194,10 +230,6 @@ func _on_catch_display_changed(
"\nRed is closing in!",
]
).strip_edges()
_barrier_health.visible = (
visible
and (active_barrier_index >= 0 or chase_gap <= 0.05)
)
_refresh_fishing_panel_visibility()
@ -262,4 +294,8 @@ func _on_showcase_changed(
func _on_player_menu_visibility_changed(is_open: bool) -> void:
_player_menu_open = is_open
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled and not is_open
)
_hotbar_ui.set_drag_enabled(is_open)
_refresh_fishing_panel_visibility()

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=11 format=3]
[gd_scene load_steps=12 format=3]
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
@ -6,6 +6,7 @@
[ext_resource type="Script" path="res://ui/screen_fade.gd" id="4_fade"]
[ext_resource type="PackedScene" path="res://ui/title_screen.tscn" id="5_title"]
[ext_resource type="PackedScene" path="res://ui/pause_menu.tscn" id="6_pause"]
[ext_resource type="PackedScene" path="res://ui/hotbar.tscn" id="7_hotbar"]
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
bg_color = Color(0.055, 0.105, 0.125, 1)
@ -37,35 +38,36 @@ script = ExtResource("1_ui")
[node name="FishingPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
visible = false
z_index = 60
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -210.0
offset_top = -130.0
offset_right = 210.0
offset_bottom = -24.0
offset_left = -205.0
offset_top = -216.0
offset_right = 205.0
offset_bottom = -128.0
grow_horizontal = 2
grow_vertical = 0
mouse_filter = 2
theme = ExtResource("3_theme")
[node name="MarginContainer" type="MarginContainer" parent="FishingPanel"]
theme_override_constants/margin_left = 18
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 18
theme_override_constants/margin_bottom = 12
theme_override_constants/margin_left = 14
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 14
theme_override_constants/margin_bottom = 8
[node name="VBoxContainer" type="VBoxContainer" parent="FishingPanel/MarginContainer"]
theme_override_constants/separation = 6
theme_override_constants/separation = 4
[node name="StatusLabel" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
visible = false
text = ""
horizontal_alignment = 1
theme_override_font_sizes/font_size = 17
theme_override_font_sizes/font_size = 16
[node name="ShowcaseDetails" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
@ -75,7 +77,7 @@ horizontal_alignment = 1
[node name="CatchTrack" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(0, 22)
custom_minimum_size = Vector2(0, 20)
[node name="GreenCatchProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"]
unique_name_in_owner = true
@ -128,6 +130,9 @@ horizontal_alignment = 1
[node name="PlayerMenu" parent="." instance=ExtResource("2_menu")]
unique_name_in_owner = true
[node name="Hotbar" parent="." instance=ExtResource("7_hotbar")]
unique_name_in_owner = true
[node name="ScreenFade" type="ColorRect" parent="."]
unique_name_in_owner = true
visible = false

185
ui/hotbar.gd Normal file
View file

@ -0,0 +1,185 @@
class_name HotbarUI
extends Control
const ItemCatalogType = preload("res://items/item_catalog.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const HotbarSlotType = preload("res://ui/hotbar_slot.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
@onready var _slot_row: HBoxContainer = %SlotRow
@onready var _selected_item_label: Label = %SelectedItemLabel
@onready var _item_name_timer: Timer = %ItemNameTimer
var _hotbar: PlayerHotbarType
var _bag: PlayerBagType
var _catalog: ItemCatalogType
var _fishing_spot: FishingSpotType
var _slots: Array[HotbarSlotType] = []
var _gameplay_input_enabled: bool = false
var _drag_enabled: bool = false
var _hovered_slot_index: int = -1
var _item_name_suppressed: bool = false
func _ready() -> void:
_item_name_timer.timeout.connect(_on_item_name_timer_timeout)
func setup(
hotbar: PlayerHotbarType,
bag: PlayerBagType,
catalog: ItemCatalogType,
fishing_spot: FishingSpotType,
) -> void:
_hotbar = hotbar
_bag = bag
_catalog = catalog
_fishing_spot = fishing_spot
if not _hotbar.slots_changed.is_connected(_refresh):
_hotbar.slots_changed.connect(_refresh)
if not _hotbar.selected_slot_changed.is_connected(
_on_selected_slot_changed
):
_hotbar.selected_slot_changed.connect(_on_selected_slot_changed)
if not _bag.contents_changed.is_connected(_refresh):
_bag.contents_changed.connect(_refresh)
_build_slots()
_refresh()
func set_gameplay_input_enabled(enabled: bool) -> void:
_gameplay_input_enabled = enabled
func set_drag_enabled(enabled: bool) -> void:
_drag_enabled = enabled
for slot: HotbarSlotType in _slots:
slot.set_drag_enabled(enabled)
if not enabled:
_hovered_slot_index = -1
_hide_item_name()
func set_item_name_suppressed(suppressed: bool) -> void:
_item_name_suppressed = suppressed
if suppressed:
_hovered_slot_index = -1
_hide_item_name()
func _unhandled_input(event: InputEvent) -> void:
if (
not _gameplay_input_enabled
or _hotbar == null
or _fishing_spot == null
or not _fishing_spot.can_change_hotbar_selection()
):
return
if event is InputEventKey and event.pressed and not event.echo:
for index: int in range(PlayerHotbarType.SLOT_COUNT):
if event.is_action_pressed("hotbar_%d" % (index + 1)):
_hotbar.select_slot(index)
get_viewport().set_input_as_handled()
return
if (
event is InputEventMouseButton
and event.pressed
and not event.shift_pressed
):
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
_hotbar.cycle_selection(-1)
get_viewport().set_input_as_handled()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_hotbar.cycle_selection(1)
get_viewport().set_input_as_handled()
func _build_slots() -> void:
for child: Node in _slot_row.get_children():
child.queue_free()
_slots.clear()
for index: int in range(PlayerHotbarType.SLOT_COUNT):
var slot := HotbarSlotType.new()
slot.toggle_mode = true
slot.setup(index, _hotbar, _bag, _catalog)
slot.set_drag_enabled(_drag_enabled)
slot.item_hovered.connect(_on_slot_item_hovered)
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
slot.item_drag_started.connect(_on_slot_drag_started)
slot.item_drag_finished.connect(_on_slot_drag_finished)
_slot_row.add_child(slot)
_slots.append(slot)
func _refresh() -> void:
for slot: HotbarSlotType in _slots:
slot.refresh()
func _on_selected_slot_changed(
_slot_index: int,
_item_id: StringName,
) -> void:
_refresh()
if _hovered_slot_index < 0:
_show_selected_item_briefly()
func _on_slot_item_hovered(
slot_index: int,
item_id: StringName,
) -> void:
if _item_name_suppressed:
return
_hovered_slot_index = slot_index
_item_name_timer.stop()
_show_item_name(item_id)
func _on_slot_item_hover_ended(slot_index: int) -> void:
if slot_index != _hovered_slot_index:
return
_hovered_slot_index = -1
_show_selected_item_briefly()
func _on_slot_drag_started() -> void:
_hovered_slot_index = -1
_hide_item_name()
func _on_slot_drag_finished() -> void:
_show_selected_item_briefly()
func _show_selected_item_briefly() -> void:
if _item_name_suppressed or _hotbar == null:
_hide_item_name()
return
_show_item_name(_hotbar.get_selected_item_id())
if _selected_item_label.visible:
_item_name_timer.start()
func _show_item_name(item_id: StringName) -> void:
if item_id.is_empty() or _catalog == null:
_hide_item_name()
return
var item := _catalog.get_item_by_id(item_id)
if item == null:
_hide_item_name()
return
_selected_item_label.text = item.display_name
_selected_item_label.visible = true
func _hide_item_name() -> void:
_item_name_timer.stop()
_selected_item_label.text = ""
_selected_item_label.visible = false
func _on_item_name_timer_timeout() -> void:
if _hovered_slot_index < 0:
_hide_item_name()

1
ui/hotbar.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://642w0srh4vwj

74
ui/hotbar.tscn Normal file
View file

@ -0,0 +1,74 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://ui/hotbar.gd" id="1_script"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
[node name="Hotbar" type="Control"]
unique_name_in_owner = true
visible = false
z_index = 20
layout_mode = 3
anchors_preset = 12
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -84.0
offset_bottom = -8.0
grow_horizontal = 2
grow_vertical = 0
mouse_filter = 2
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="Panel" type="PanelContainer" parent="Center"]
layout_mode = 2
mouse_filter = 2
[node name="Margin" type="MarginContainer" parent="Center/Panel"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="SlotRow" type="HBoxContainer" parent="Center/Panel/Margin"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 4
[node name="SelectedItemLabel" type="Label" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 10
anchor_left = 0.5
anchor_right = 0.5
offset_left = -150.0
offset_top = -28.0
offset_right = 150.0
offset_bottom = -8.0
grow_horizontal = 2
mouse_filter = 2
clip_text = true
text_overrun_behavior = 3
horizontal_alignment = 1
vertical_alignment = 1
theme_override_colors/font_color = Color(0.925, 0.953, 0.965, 1)
theme_override_colors/font_outline_color = Color(0.015, 0.02, 0.03, 0.95)
theme_override_constants/outline_size = 2
theme_override_font_sizes/font_size = 12
[node name="ItemNameTimer" type="Timer" parent="."]
unique_name_in_owner = true
wait_time = 1.5
one_shot = true

187
ui/hotbar_slot.gd Normal file
View file

@ -0,0 +1,187 @@
class_name HotbarSlot
extends Button
signal item_hovered(slot_index: int, item_id: StringName)
signal item_hover_ended(slot_index: int)
signal item_drag_started
signal item_drag_finished
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
var slot_index: int = 0
var _hotbar: PlayerHotbarType
var _bag: PlayerBagType
var _catalog: ItemCatalogType
var _drag_enabled: bool = false
var _drag_in_progress: bool = false
var _slot_number_label: Label
var _quantity_label: Label
func setup(
index: int,
hotbar: PlayerHotbarType,
bag: PlayerBagType,
catalog: ItemCatalogType,
) -> void:
slot_index = index
_hotbar = hotbar
_bag = bag
_catalog = catalog
custom_minimum_size = Vector2(50, 50)
expand_icon = true
focus_mode = Control.FOCUS_ALL
_slot_number_label = _create_corner_label(str(slot_index + 1))
_quantity_label = _create_corner_label("", true)
pressed.connect(_select_slot)
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
refresh()
func set_drag_enabled(enabled: bool) -> void:
_drag_enabled = enabled
mouse_filter = (
Control.MOUSE_FILTER_STOP
if enabled
else Control.MOUSE_FILTER_IGNORE
)
func refresh() -> void:
if _hotbar == null:
return
var item_id: StringName = _hotbar.get_item_id(slot_index)
var item: ItemDataType = (
_catalog.get_item_by_id(item_id)
if _catalog != null and not item_id.is_empty()
else null
)
icon = item.icon if item != null else null
var quantity: int = (
_bag.get_quantity(item_id)
if _bag != null and not item_id.is_empty()
else 0
)
var quantity_text: String = (
"×%d" % quantity
if item != null and item.stackable and quantity > 1
else ""
)
text = ""
_quantity_label.text = quantity_text
_quantity_label.visible = not quantity_text.is_empty()
tooltip_text = (
item.display_name if item != null else "Empty hotbar slot"
)
button_pressed = slot_index == _hotbar.get_selected_slot()
func _create_corner_label(
label_text: String,
bottom_right: bool = false,
) -> Label:
var label := Label.new()
label.text = label_text
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", 11)
label.add_theme_color_override("font_color", UIPalette.TEXT)
if bottom_right:
label.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
label.position = Vector2(-22.0, -20.0)
else:
label.position = Vector2(5.0, 2.0)
add_child(label)
return label
func _gui_input(event: InputEvent) -> void:
if (
_drag_enabled
and event is InputEventMouseButton
and event.button_index == MOUSE_BUTTON_RIGHT
and event.pressed
and _hotbar != null
):
_hotbar.clear_slot(slot_index)
accept_event()
func _get_drag_data(_at_position: Vector2) -> Variant:
if not _drag_enabled or _hotbar == null:
return null
var item_id: StringName = _hotbar.get_item_id(slot_index)
if item_id.is_empty():
return null
var item: ItemDataType = _catalog.get_item_by_id(item_id)
var preview := TextureRect.new()
preview.custom_minimum_size = Vector2(44, 44)
preview.texture = item.icon if item != null else null
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
set_drag_preview(preview)
_drag_in_progress = true
item_drag_started.emit()
return {
"kind": "hotbar_slot",
"slot_index": slot_index,
"item_id": String(item_id),
}
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
if not _drag_enabled or typeof(data) != TYPE_DICTIONARY:
return false
var payload: Dictionary = data
var kind: String = str(payload.get("kind", ""))
if kind == "hotbar_slot":
var source_index: int = int(payload.get("slot_index", -1))
return (
source_index >= 0
and source_index < PlayerHotbarType.SLOT_COUNT
and source_index != slot_index
)
if kind != "bag_item":
return false
var item_id: StringName = StringName(str(payload.get("item_id", "")))
if _bag == null or not _bag.owns_item(item_id) or _catalog == null:
return false
var item: ItemDataType = _catalog.get_item_by_id(item_id)
return item != null and item.is_valid() and item.hotbar_allowed
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if not _can_drop_data(Vector2.ZERO, data) or _hotbar == null:
return
var payload: Dictionary = data
if str(payload.get("kind", "")) == "hotbar_slot":
_hotbar.swap_slots(int(payload["slot_index"]), slot_index)
else:
_hotbar.assign_item(
slot_index,
StringName(str(payload["item_id"]))
)
func _select_slot() -> void:
if _hotbar != null:
_hotbar.select_slot(slot_index)
func _on_mouse_entered() -> void:
if _hotbar != null:
item_hovered.emit(slot_index, _hotbar.get_item_id(slot_index))
func _on_mouse_exited() -> void:
item_hover_ended.emit(slot_index)
func _notification(what: int) -> void:
if what == NOTIFICATION_DRAG_END and _drag_in_progress:
_drag_in_progress = false
item_drag_finished.emit()

1
ui/hotbar_slot.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://swcob5q48f8e

46
ui/item_drag_source.gd Normal file
View file

@ -0,0 +1,46 @@
class_name ItemDragSource
extends Button
var item_id: StringName
var item_icon: Texture2D
var item_name: String
func setup(
new_item_id: StringName,
new_item_name: String,
new_icon: Texture2D,
) -> void:
item_id = new_item_id
item_name = new_item_name
item_icon = new_icon
func _get_drag_data(_at_position: Vector2) -> Variant:
if item_id.is_empty() or disabled:
return null
set_drag_preview(_create_drag_preview())
return {
"kind": "bag_item",
"item_id": String(item_id),
}
func _create_drag_preview() -> Control:
var preview := VBoxContainer.new()
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview.add_theme_constant_override("separation", 2)
var texture := TextureRect.new()
texture.custom_minimum_size = Vector2(44, 44)
texture.texture = item_icon
texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
texture.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview.add_child(texture)
var label := Label.new()
label.text = item_name
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", 12)
preview.add_child(label)
return preview

View file

@ -0,0 +1 @@
uid://cfv418ixq3eeb

View file

@ -13,6 +13,7 @@ const FishingSpotType = preload("res://fishing/fishing_spot.gd")
signal return_to_title_requested
signal reset_progress_requested
signal quit_requested
signal menu_visibility_changed(is_open: bool)
enum ConfirmationAction {
NONE,
@ -100,6 +101,7 @@ func open_menu() -> void:
_confirmation_action = ConfirmationAction.NONE
show()
%ResumeButton.grab_focus()
menu_visibility_changed.emit(true)
func resume() -> void:
@ -138,6 +140,7 @@ func close_menu(
else:
_control_snapshot_stored = false
_apply_mouse_close_policy(reason)
menu_visibility_changed.emit(false)
func handle_escape() -> bool:

View file

@ -13,11 +13,18 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const PlayerType = preload("res://player/player.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const ItemDragSourceType = preload("res://ui/item_drag_source.gd")
signal menu_visibility_changed(is_open: bool)
enum Section {
INVENTORY,
COOLER,
BAG,
LOGBOOK,
}
@ -37,11 +44,18 @@ enum CloseReason {
}
@onready var _inventory_tab: Button = %InventoryTab
@onready var _bag_tab: Button = %BagTab
@onready var _logbook_tab: Button = %LogbookTab
@onready var _close_button: Button = %CloseButton
@onready var _wallet_balance: Label = %WalletBalance
@onready var _inventory_section: Control = %InventorySection
@onready var _bag_section: Control = %BagSection
@onready var _logbook_section: Control = %LogbookSection
@onready var _bag_empty: Label = %BagEmpty
@onready var _bag_grid: GridContainer = %BagGrid
@onready var _bag_detail_texture: TextureRect = %BagDetailTexture
@onready var _bag_detail_name: Label = %BagDetailName
@onready var _bag_detail_data: Label = %BagDetailData
@onready var _sort_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection
@onready var _held_value: Label = %HeldValue
@ -69,10 +83,14 @@ var _sale_service: FishSaleServiceType
var _default_buyer: FishBuyerProfileType
var _catalog: FishPoolType
var _fishing_spot: FishingSpotType
var _current_section: Section = Section.INVENTORY
var _bag: PlayerBagType
var _hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _current_section: Section = Section.COOLER
var _sort_mode: SortMode = SortMode.CATCH_ORDER
var _sort_descending: bool = true
var _selected_catch_id: StringName
var _selected_bag_item_id: StringName
var _prior_movement_enabled: bool = true
var _prior_camera_input_enabled: bool = true
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
@ -87,8 +105,9 @@ var _sale_in_progress: bool = false
func _ready() -> void:
_inventory_tab.pressed.connect(
_show_section.bind(Section.INVENTORY)
_show_section.bind(Section.COOLER)
)
_bag_tab.pressed.connect(_show_section.bind(Section.BAG))
_logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK)
)
@ -116,6 +135,9 @@ func setup(
default_buyer: FishBuyerProfileType,
catalog: FishPoolType,
fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void:
_player = player
_inventory = inventory
@ -125,6 +147,9 @@ func setup(
_default_buyer = default_buyer
_catalog = catalog
_fishing_spot = fishing_spot
_bag = bag
_hotbar = hotbar
_item_catalog = item_catalog
if not _inventory.catches_changed.is_connected(_on_inventory_changed):
_inventory.catches_changed.connect(_on_inventory_changed)
if not _collection_log.fish_discovered.is_connected(_on_fish_discovered):
@ -133,6 +158,8 @@ func setup(
_wallet.balance_changed.connect(_on_wallet_balance_changed)
if not _fishing_spot.bite_activated.is_connected(_on_bite_activated):
_fishing_spot.bite_activated.connect(_on_bite_activated)
if not _bag.contents_changed.is_connected(_on_bag_changed):
_bag.contents_changed.connect(_on_bag_changed)
_refresh_all()
@ -194,6 +221,7 @@ func close_menu(
) -> void:
if not visible:
return
get_viewport().gui_cancel_drag()
_close_sale_confirmation()
var closing_generation: int = _menu_generation
visible = false
@ -268,17 +296,21 @@ func _show_section(section: Section) -> void:
_current_section = section
if not is_node_ready():
return
_inventory_section.visible = section == Section.INVENTORY
_inventory_section.visible = section == Section.COOLER
_bag_section.visible = section == Section.BAG
_logbook_section.visible = section == Section.LOGBOOK
_inventory_tab.button_pressed = section == Section.INVENTORY
_inventory_tab.button_pressed = section == Section.COOLER
_bag_tab.button_pressed = section == Section.BAG
_logbook_tab.button_pressed = section == Section.LOGBOOK
if visible:
_focus_current_section()
func _focus_current_section() -> void:
if _current_section == Section.INVENTORY:
if _current_section == Section.COOLER:
_inventory_tab.grab_focus()
elif _current_section == Section.BAG:
_bag_tab.grab_focus()
else:
_logbook_tab.grab_focus()
@ -307,9 +339,99 @@ func _update_sort_direction_text() -> void:
func _refresh_all() -> void:
_refresh_economy_summary()
_refresh_inventory()
_refresh_bag()
_refresh_logbook()
func _on_bag_changed() -> void:
_refresh_bag()
func _refresh_bag() -> void:
if not is_node_ready():
return
_clear_container(_bag_grid)
var owned_items: Array[OwnedItemType] = (
_bag.get_all_items() if _bag != null else []
)
owned_items.sort_custom(_sort_bag_items)
_bag_empty.visible = owned_items.is_empty()
if (
not _selected_bag_item_id.is_empty()
and (_bag == null or not _bag.owns_item(_selected_bag_item_id))
):
_selected_bag_item_id = StringName()
for owned: OwnedItemType in owned_items:
var item: ItemDataType = _item_catalog.get_item_by_id(owned.item_id)
if item == null:
continue
var card := ItemDragSourceType.new()
card.custom_minimum_size = Vector2(140, 88)
card.expand_icon = true
card.icon = item.icon
card.text = "%s%s\n%s" % [
item.display_name,
" ×%d" % owned.quantity if owned.quantity > 1 else "",
item.get_category_name(),
]
card.tooltip_text = (
"Drag to a hotbar slot."
if item.hotbar_allowed
else "This item cannot be assigned to the hotbar."
)
card.setup(item.item_id, item.display_name, item.icon)
card.pressed.connect(_select_bag_item.bind(item.item_id))
_bag_grid.add_child(card)
_update_bag_detail()
func _sort_bag_items(a: OwnedItemType, b: OwnedItemType) -> bool:
var item_a: ItemDataType = _item_catalog.get_item_by_id(a.item_id)
var item_b: ItemDataType = _item_catalog.get_item_by_id(b.item_id)
if item_a == null:
return false
if item_b == null:
return true
if item_a.category != item_b.category:
return item_a.category < item_b.category
return item_a.display_name.naturalnocasecmp_to(item_b.display_name) < 0
func _select_bag_item(item_id: StringName) -> void:
_selected_bag_item_id = item_id
_update_bag_detail()
func _update_bag_detail() -> void:
var item: ItemDataType = (
_item_catalog.get_item_by_id(_selected_bag_item_id)
if _item_catalog != null and not _selected_bag_item_id.is_empty()
else null
)
var quantity: int = (
_bag.get_quantity(_selected_bag_item_id)
if _bag != null and item != null
else 0
)
_bag_detail_texture.texture = item.icon if item != null else null
_bag_detail_name.text = item.display_name if item != null else ""
_bag_detail_data.text = (
"%s\nQuantity: %d\n%s\n%s"
% [
item.get_category_name(),
quantity,
item.description,
(
"Can be assigned to the hotbar."
if item.hotbar_allowed
else "Cannot be assigned to the hotbar."
),
]
if item != null
else "Select a Bag item for details."
)
func _on_inventory_changed() -> void:
_refresh_economy_summary()
_revalidate_confirmation()
@ -384,7 +506,7 @@ func _compare_catches(left: FishCatchType, right: FishCatchType) -> bool:
func _create_inventory_card(fish_catch: FishCatchType) -> Button:
var card := Button.new()
card.theme_type_variation = &"CardButton"
card.custom_minimum_size = Vector2(152.0, 140.0)
card.custom_minimum_size = Vector2(132.0, 118.0)
card.toggle_mode = true
card.button_pressed = fish_catch.catch_id == _selected_catch_id
card.pressed.connect(_select_catch.bind(fish_catch.catch_id))
@ -400,7 +522,7 @@ func _create_inventory_card(fish_catch: FishCatchType) -> Button:
card.add_child(content)
content.add_child(_create_texture_frame(
fish_catch.fish.display_texture,
Vector2(124.0, 78.0)
Vector2(108.0, 62.0)
))
var name_label := Label.new()
name_label.text = fish_catch.fish.display_name

View file

@ -5,17 +5,19 @@
[node name="PlayerMenu" type="Control"]
visible = false
z_index = 30
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
mouse_filter = 2
theme = ExtResource("2_theme")
script = ExtResource("1_menu")
[node name="Dimmer" type="ColorRect" parent="."]
z_index = -20
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@ -27,23 +29,23 @@ color = Color(0.015, 0.02, 0.03, 0.72)
[node name="MenuPanel" type="PanelContainer" parent="."]
layout_mode = 1
anchor_left = 0.055
anchor_top = 0.045
anchor_right = 0.945
anchor_bottom = 0.955
anchor_left = 0.07
anchor_top = 0.055
anchor_right = 0.93
anchor_bottom = 0.72
grow_horizontal = 2
grow_vertical = 2
[node name="Margin" type="MarginContainer" parent="MenuPanel"]
layout_mode = 2
theme_override_constants/margin_left = 22
theme_override_constants/margin_top = 18
theme_override_constants/margin_right = 22
theme_override_constants/margin_bottom = 20
theme_override_constants/margin_left = 14
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 14
theme_override_constants/margin_bottom = 12
[node name="Layout" type="VBoxContainer" parent="MenuPanel/Margin"]
layout_mode = 2
theme_override_constants/separation = 12
theme_override_constants/separation = 8
[node name="Header" type="HBoxContainer" parent="MenuPanel/Margin/Layout"]
layout_mode = 2
@ -53,7 +55,7 @@ theme_override_constants/separation = 8
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
theme_override_font_sizes/font_size = 26
theme_override_font_sizes/font_size = 22
text = "Player Menu"
[node name="WalletBalance" type="Label" parent="MenuPanel/Margin/Layout/Header"]
@ -66,21 +68,28 @@ theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
unique_name_in_owner = true
layout_mode = 2
toggle_mode = true
text = "Inventory"
custom_minimum_size = Vector2(104, 36)
text = "Cooler"
custom_minimum_size = Vector2(88, 30)
[node name="BagTab" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
toggle_mode = true
text = "Bag"
custom_minimum_size = Vector2(76, 30)
[node name="LogbookTab" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
toggle_mode = true
text = "Logbook"
custom_minimum_size = Vector2(104, 36)
custom_minimum_size = Vector2(88, 30)
[node name="CloseButton" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
text = "Close"
custom_minimum_size = Vector2(76, 36)
custom_minimum_size = Vector2(68, 30)
[node name="Separator" type="HSeparator" parent="MenuPanel/Margin/Layout"]
layout_mode = 2
@ -91,10 +100,10 @@ layout_mode = 2
text = ""
horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
theme_override_font_sizes/font_size = 16
theme_override_font_sizes/font_size = 14
[node name="Content" type="Control" parent="MenuPanel/Margin/Layout"]
custom_minimum_size = Vector2(0, 390)
custom_minimum_size = Vector2(0, 260)
layout_mode = 2
size_flags_vertical = 3
@ -106,11 +115,11 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 10
theme_override_constants/separation = 7
[node name="SortBar" type="HBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"]
layout_mode = 2
theme_override_constants/separation = 8
theme_override_constants/separation = 6
[node name="SortLabel" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
layout_mode = 2
@ -119,12 +128,12 @@ text = "Sort:"
[node name="SortOption" type="OptionButton" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(145, 36)
custom_minimum_size = Vector2(130, 30)
[node name="SortDirection" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(125, 36)
custom_minimum_size = Vector2(112, 30)
text = "Newest first"
[node name="SortSpacer" type="Control" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
@ -140,28 +149,28 @@ theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
[node name="InventoryBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"]
layout_mode = 2
size_flags_vertical = 3
split_offset = 560
theme_override_constants/separation = 12
split_offset = 510
theme_override_constants/separation = 8
[node name="InventoryList" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"]
custom_minimum_size = Vector2(400, 0)
custom_minimum_size = Vector2(350, 0)
layout_mode = 2
[node name="InventoryListMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList"]
layout_mode = 2
theme_override_constants/margin_left = 12
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 12
theme_override_constants/margin_bottom = 12
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="InventoryStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin"]
layout_mode = 2
theme_override_constants/separation = 10
theme_override_constants/separation = 6
[node name="InventoryEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
unique_name_in_owner = true
layout_mode = 2
text = "No fish in your inventory."
text = "Your Cooler is empty."
horizontal_alignment = 1
[node name="InventoryScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
@ -173,28 +182,28 @@ horizontal_scroll_mode = 0
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 7
columns = 3
[node name="DetailPanel" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"]
custom_minimum_size = Vector2(240, 0)
custom_minimum_size = Vector2(210, 0)
layout_mode = 2
[node name="DetailMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel"]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 14
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 14
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 9
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 9
[node name="DetailStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin"]
layout_mode = 2
theme_override_constants/separation = 10
theme_override_constants/separation = 6
[node name="DetailTexture" type="TextureRect" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
custom_minimum_size = Vector2(208, 140)
custom_minimum_size = Vector2(178, 100)
layout_mode = 2
expand_mode = 1
stretch_mode = 5
@ -204,7 +213,7 @@ texture_filter = 1
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
theme_override_font_sizes/font_size = 22
theme_override_font_sizes/font_size = 19
horizontal_alignment = 1
[node name="DetailData" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
@ -234,6 +243,98 @@ autowrap_mode = 2
horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.702, 0.278, 1)
[node name="BagSection" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 7
[node name="BagHint" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection"]
layout_mode = 2
text = "Drag hotbar-compatible items onto a slot below. Right-click a slot to clear it."
horizontal_alignment = 1
[node name="BagBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/BagSection"]
layout_mode = 2
size_flags_vertical = 3
split_offset = 540
theme_override_constants/separation = 8
[node name="BagList" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody"]
custom_minimum_size = Vector2(360, 0)
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList"]
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="Stack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="BagEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
text = "Your Bag is empty."
horizontal_alignment = 1
[node name="BagScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="BagGrid" type="GridContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack/BagScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 7
columns = 3
[node name="BagDetail" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody"]
custom_minimum_size = Vector2(220, 0)
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 9
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 9
[node name="Stack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="BagDetailTexture" type="TextureRect" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
custom_minimum_size = Vector2(180, 96)
layout_mode = 2
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="BagDetailName" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 19
horizontal_alignment = 1
[node name="BagDetailData" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
text = "Select a Bag item for details."
horizontal_alignment = 1
autowrap_mode = 2
[node name="LogbookSection" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true
visible = false
@ -243,7 +344,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 10
theme_override_constants/separation = 7
[node name="LogbookEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/LogbookSection"]
unique_name_in_owner = true
@ -260,13 +361,14 @@ horizontal_scroll_mode = 0
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 10
theme_override_constants/v_separation = 10
theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 7
columns = 4
[node name="SaleConfirmation" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true
visible = false
z_index = 40
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5