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

@ -9,6 +9,10 @@ const CatchControllerType = preload("res://fishing/catch_controller.gd")
const FishingContextType = preload("res://fishing/fishing_context.gd")
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
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")
const PlayerType = preload("res://player/player.gd")
const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd"
@ -31,6 +35,7 @@ signal showcase_changed(
visible: bool,
)
signal bite_activated
signal ready_for_equipment_refresh
enum FishingState {
READY,
@ -84,6 +89,9 @@ var _local_menu_input_owners: Dictionary[StringName, bool] = {}
var _local_player: PlayerType
var _local_inventory: FishInventoryType
var _local_collection_log: CollectionLogType
var _local_bag: PlayerBagType
var _local_hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _active_player: PlayerType
var _state_time_remaining: float = 0.0
var _cast_charge: float = 0.0
@ -121,10 +129,16 @@ func setup(
local_player: PlayerType,
local_inventory: FishInventoryType,
local_collection_log: CollectionLogType,
local_bag: PlayerBagType,
local_hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void:
_local_player = local_player
_local_inventory = local_inventory
_local_collection_log = local_collection_log
_local_bag = local_bag
_local_hotbar = local_hotbar
_item_catalog = item_catalog
func can_open_player_menu() -> bool:
@ -151,6 +165,15 @@ func can_open_system_menu() -> bool:
)
func can_change_hotbar_selection() -> bool:
return (
_gameplay_input_enabled
and not _external_input_blocked
and _local_menu_input_owners.is_empty()
and state == FishingState.READY
)
func handle_escape_action() -> bool:
if not _gameplay_input_enabled or _external_input_blocked:
return false
@ -301,6 +324,10 @@ func _unhandled_input(event: InputEvent) -> void:
FishingState.READY:
if not _new_cast_press_armed:
return
if not has_active_fishing_rod():
status_changed.emit("Select a fishing rod to cast.")
get_viewport().set_input_as_handled()
return
_begin_aiming(_local_player)
FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = true
@ -354,6 +381,35 @@ func _begin_aiming(player: PlayerType) -> void:
)
func has_active_fishing_rod() -> bool:
if (
_local_bag == null
or _local_hotbar == null
or _item_catalog == null
):
return false
var item_id: StringName = _local_hotbar.get_selected_item_id()
if item_id.is_empty() or not _local_bag.owns_item(item_id):
return false
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
return (
item != null
and item.is_valid()
and item.category == ItemDataType.Category.ROD
and item.usable
and item.equippable
)
func is_fighting() -> bool:
return state == FishingState.FIGHTING
func refresh_active_item_status() -> void:
if state == FishingState.READY and has_active_fishing_rod():
status_changed.emit("")
func _update_cast_charge(delta: float) -> void:
if state != FishingState.AIMING_CAST:
return
@ -581,20 +637,6 @@ func _on_catch_encounter_updated(
_bobber_water_position,
Input.is_action_pressed("fish_primary")
)
if (
active_barrier_index >= 0
and active_barrier_index < barrier_health.size()
and active_barrier_index < barrier_max_health.size()
):
status_changed.emit(
"Barrier: %d / %d"
% [
barrier_health[active_barrier_index],
barrier_max_health[active_barrier_index],
]
)
else:
status_changed.emit("Hold left click to reel")
func _on_catch_completed() -> void:
@ -731,6 +773,7 @@ func _cleanup_attempt(
state = FishingState.READY
_cooldown_status = ""
status_changed.emit("")
ready_for_equipment_refresh.emit()
else:
state = FishingState.COOLDOWN
_state_time_remaining = cooldown_duration
@ -743,6 +786,7 @@ func _return_to_ready() -> void:
_state_time_remaining = 0.0
_cooldown_status = ""
status_changed.emit("")
ready_for_equipment_refresh.emit()
func _cancel_attempt() -> void:

104
inventory/player_bag.gd Normal file
View file

@ -0,0 +1,104 @@
class_name PlayerBag
extends Node
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
signal contents_changed
var _catalog: ItemCatalogType
var _items: Array[OwnedItemType] = []
func setup(catalog: ItemCatalogType) -> void:
_catalog = catalog
func add_item(item_id: StringName, quantity: int = 1) -> bool:
var item: ItemDataType = _resolve_valid_item(item_id)
if item == null or quantity <= 0:
return false
var existing: OwnedItemType = get_owned_item(item_id)
if existing != null:
if not item.stackable:
return false
if existing.quantity + quantity > item.max_stack:
return false
existing.quantity += quantity
else:
if quantity > (item.max_stack if item.stackable else 1):
return false
var owned := OwnedItemType.new()
owned.item_id = item_id
owned.quantity = quantity
_items.append(owned)
contents_changed.emit()
return true
func remove_item(item_id: StringName, quantity: int = 1) -> bool:
if quantity <= 0:
return false
for index: int in range(_items.size()):
var owned: OwnedItemType = _items[index]
if owned.item_id != item_id or owned.quantity < quantity:
continue
owned.quantity -= quantity
if owned.quantity == 0:
_items.remove_at(index)
contents_changed.emit()
return true
return false
func get_quantity(item_id: StringName) -> int:
var owned: OwnedItemType = get_owned_item(item_id)
return owned.quantity if owned != null else 0
func owns_item(item_id: StringName) -> bool:
return get_quantity(item_id) > 0
func get_owned_item(item_id: StringName) -> OwnedItemType:
if item_id.is_empty():
return null
for owned: OwnedItemType in _items:
if owned != null and owned.item_id == item_id:
return owned
return null
func get_all_items() -> Array[OwnedItemType]:
var result: Array[OwnedItemType] = []
for owned: OwnedItemType in _items:
if owned != null:
result.append(owned.duplicate_record())
return result
func replace_all_items(items: Array[OwnedItemType]) -> bool:
var validated: Array[OwnedItemType] = []
var seen: Dictionary[StringName, bool] = {}
for owned: OwnedItemType in items:
if owned == null or not owned.is_valid() or seen.has(owned.item_id):
return false
var item: ItemDataType = _resolve_valid_item(owned.item_id)
if item == null:
return false
var maximum: int = item.max_stack if item.stackable else 1
if owned.quantity > maximum:
return false
seen[owned.item_id] = true
validated.append(owned.duplicate_record())
_items = validated
contents_changed.emit()
return true
func _resolve_valid_item(item_id: StringName) -> ItemDataType:
if _catalog == null:
return null
var item: ItemDataType = _catalog.get_item_by_id(item_id)
return item if item != null and item.is_valid() else null

View file

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

171
inventory/player_hotbar.gd Normal file
View file

@ -0,0 +1,171 @@
class_name PlayerHotbar
extends Node
const SLOT_COUNT: int = 9
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
signal slots_changed
signal selected_slot_changed(slot_index: int, item_id: StringName)
var _bag: PlayerBagType
var _catalog: ItemCatalogType
var _slots: Array[StringName] = []
var _selected_slot: int = 0
func _init() -> void:
_slots.resize(SLOT_COUNT)
_slots.fill(StringName())
func setup(bag: PlayerBagType, catalog: ItemCatalogType) -> void:
_bag = bag
_catalog = catalog
if _bag != null and not _bag.contents_changed.is_connected(
_on_bag_contents_changed
):
_bag.contents_changed.connect(_on_bag_contents_changed)
_validate_assignments()
func assign_item(slot_index: int, item_id: StringName) -> bool:
if not _is_slot_valid(slot_index) or not _can_assign(item_id):
return false
if _slots[slot_index] == item_id:
return true
for index: int in range(SLOT_COUNT):
if index != slot_index and _slots[index] == item_id:
_slots[index] = StringName()
_slots[slot_index] = item_id
slots_changed.emit()
if slot_index == _selected_slot:
selected_slot_changed.emit(_selected_slot, item_id)
return true
func clear_slot(slot_index: int) -> bool:
if not _is_slot_valid(slot_index) or _slots[slot_index].is_empty():
return false
_slots[slot_index] = StringName()
slots_changed.emit()
if slot_index == _selected_slot:
selected_slot_changed.emit(_selected_slot, StringName())
return true
func swap_slots(from_index: int, to_index: int) -> bool:
if (
not _is_slot_valid(from_index)
or not _is_slot_valid(to_index)
or from_index == to_index
):
return false
var temporary: StringName = _slots[from_index]
_slots[from_index] = _slots[to_index]
_slots[to_index] = temporary
slots_changed.emit()
if from_index == _selected_slot or to_index == _selected_slot:
selected_slot_changed.emit(
_selected_slot,
_slots[_selected_slot]
)
return true
func select_slot(slot_index: int) -> bool:
if not _is_slot_valid(slot_index):
return false
if _selected_slot == slot_index:
return true
_selected_slot = slot_index
selected_slot_changed.emit(_selected_slot, _slots[_selected_slot])
return true
func cycle_selection(direction: int) -> bool:
if direction == 0:
return false
return select_slot(
posmod(_selected_slot + signi(direction), SLOT_COUNT)
)
func get_selected_slot() -> int:
return _selected_slot
func get_selected_item_id() -> StringName:
return _slots[_selected_slot]
func get_item_id(slot_index: int) -> StringName:
return _slots[slot_index] if _is_slot_valid(slot_index) else StringName()
func get_slots() -> Array[StringName]:
return _slots.duplicate()
func replace_state(
slots: Array[StringName],
selected_slot: int,
) -> bool:
var normalized: Array[StringName] = []
normalized.resize(SLOT_COUNT)
normalized.fill(StringName())
var seen_assignments: Dictionary[StringName, bool] = {}
for index: int in range(mini(slots.size(), SLOT_COUNT)):
var item_id: StringName = slots[index]
if item_id.is_empty():
continue
if _can_assign(item_id) and not seen_assignments.has(item_id):
normalized[index] = item_id
seen_assignments[item_id] = true
_slots = normalized
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
slots_changed.emit()
selected_slot_changed.emit(
_selected_slot,
_slots[_selected_slot]
)
return true
func has_any_assignment() -> bool:
for item_id: StringName in _slots:
if not item_id.is_empty():
return true
return false
func _can_assign(item_id: StringName) -> bool:
if item_id.is_empty() or _bag == null or not _bag.owns_item(item_id):
return false
if _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 _validate_assignments() -> void:
var changed: bool = false
for index: int in range(SLOT_COUNT):
if not _slots[index].is_empty() and not _can_assign(_slots[index]):
_slots[index] = StringName()
changed = true
if changed:
slots_changed.emit()
selected_slot_changed.emit(
_selected_slot,
_slots[_selected_slot]
)
func _on_bag_contents_changed() -> void:
_validate_assignments()
func _is_slot_valid(slot_index: int) -> bool:
return slot_index >= 0 and slot_index < SLOT_COUNT

View file

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

View file

@ -0,0 +1,26 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=4 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_script"]
[sub_resource type="Gradient" id="Gradient_rod"]
colors = PackedColorArray(0.302, 0.184, 0.11, 1, 0.957, 0.827, 0.369, 1, 0.439, 0.839, 0.82, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture_rod"]
gradient = SubResource("Gradient_rod")
width = 128
height = 128
fill_from = Vector2(0.15, 0.85)
fill_to = Vector2(0.85, 0.15)
[resource]
script = ExtResource("1_script")
item_id = &"basic_fishing_rod"
display_name = "Basic Fishing Rod"
description = "A dependable starter rod. Select it to cast."
category = 0
icon = SubResource("GradientTexture_rod")
stackable = false
max_stack = 1
usable = true
equippable = true
hotbar_allowed = true

View file

@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
[resource]
script = ExtResource("1_script")
items = [ExtResource("2_rod")]

30
items/item_catalog.gd Normal file
View file

@ -0,0 +1,30 @@
class_name ItemCatalog
extends Resource
const ItemDataType = preload("res://items/item_data.gd")
@export var items: Array[ItemDataType] = []
func get_item_by_id(item_id: StringName) -> ItemDataType:
if item_id.is_empty():
return null
for item: ItemDataType in items:
if item != null and item.item_id == item_id:
return item
return null
func get_valid_items() -> Array[ItemDataType]:
var result: Array[ItemDataType] = []
var seen: Dictionary[StringName, bool] = {}
for item: ItemDataType in items:
if (
item == null
or not item.is_valid()
or seen.has(item.item_id)
):
continue
seen[item.item_id] = true
result.append(item)
return result

View file

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

36
items/item_data.gd Normal file
View file

@ -0,0 +1,36 @@
class_name ItemData
extends Resource
enum Category {
ROD,
TOOL,
BAIT,
CONSUMABLE,
UTILITY,
QUEST,
COSMETIC,
}
@export var item_id: StringName
@export var display_name: String
@export_multiline var description: String
@export var category: Category = Category.UTILITY
@export var icon: Texture2D
@export var stackable: bool = false
@export_range(1, 999, 1) var max_stack: int = 1
@export var usable: bool = false
@export var equippable: bool = false
@export var hotbar_allowed: bool = false
func is_valid() -> bool:
return (
not item_id.is_empty()
and not display_name.strip_edges().is_empty()
and max_stack >= 1
and (stackable or max_stack == 1)
)
func get_category_name() -> String:
return Category.keys()[category].capitalize()

1
items/item_data.gd.uid Normal file
View file

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

20
items/owned_item.gd Normal file
View file

@ -0,0 +1,20 @@
class_name OwnedItem
extends Resource
@export var item_id: StringName
@export var quantity: int = 1
func is_valid() -> bool:
return not item_id.is_empty() and quantity > 0
func duplicate_record():
return duplicate(true)
func to_save_dict() -> Dictionary:
return {
"item_id": String(item_id),
"quantity": quantity,
}

1
items/owned_item.gd.uid Normal file
View file

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

View file

@ -18,9 +18,12 @@ const PlayerSettingsManagerType = preload(
const PlayerSettingsType = preload("res://settings/player_settings.gd")
const TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
@export var fish_catalog: FishPoolType
@export var pelican_buyer_profile: FishBuyerProfileType
@export var item_catalog: ItemCatalogType
@onready var _test_world: TestWorldType = $TestWorld
@onready var _player: PlayerType = %Player
@ -38,17 +41,25 @@ func _ready() -> void:
_player.inventory,
_player.wallet
)
_player.bag.setup(item_catalog)
_player.hotbar.setup(_player.bag, item_catalog)
_save_manager.setup(
_player.inventory,
_player.collection_log,
_player.wallet,
fish_catalog
fish_catalog,
_player.bag,
_player.hotbar,
item_catalog
)
_save_manager.set_autosave_enabled(false)
_fishing_spot.setup(
_player,
_player.inventory,
_player.collection_log
_player.collection_log,
_player.bag,
_player.hotbar,
item_catalog
)
_game_ui.setup(
_player,
@ -58,7 +69,10 @@ func _ready() -> void:
_player.fish_sale_service,
pelican_buyer_profile,
fish_catalog,
_fishing_spot
_fishing_spot,
_player.bag,
_player.hotbar,
item_catalog
)
_water_recovery.setup(
_player,
@ -93,9 +107,22 @@ func _ready() -> void:
_on_reset_progress_requested
)
pause_menu.quit_requested.connect(_on_quit_requested)
pause_menu.menu_visibility_changed.connect(
_game_ui.set_system_menu_open
)
_player.hotbar.selected_slot_changed.connect(
_on_active_hotbar_item_changed
)
_fishing_spot.ready_for_equipment_refresh.connect(
_refresh_active_hotbar_item
)
_water_recovery.recovery_starting.connect(
_on_water_recovery_starting
)
_on_active_hotbar_item_changed(
_player.hotbar.get_selected_slot(),
_player.hotbar.get_selected_item_id()
)
func _input(event: InputEvent) -> void:
@ -182,6 +209,29 @@ func _on_water_recovery_starting() -> void:
_game_ui.get_pause_menu().close_for_water_recovery()
func _on_active_hotbar_item_changed(
_slot_index: int,
item_id: StringName,
) -> void:
if _fishing_spot.state != FishingSpotType.FishingState.READY:
return
var item = item_catalog.get_item_by_id(item_id)
var active_is_rod: bool = (
item != null
and item.category == ItemDataType.Category.ROD
and _player.bag.owns_item(item_id)
)
_player.set_active_item_is_rod(active_is_rod)
_fishing_spot.refresh_active_item_status()
func _refresh_active_hotbar_item() -> void:
_on_active_hotbar_item_changed(
_player.hotbar.get_selected_slot(),
_player.hotbar.get_selected_item_id()
)
func _on_quit_requested() -> void:
_settings_manager.save_if_dirty()
if _gameplay_started:

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=11 format=3]
[gd_scene load_steps=12 format=3]
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
@ -10,11 +10,13 @@
[ext_resource type="Script" path="res://world/water_recovery_controller.gd" id="8_recovery"]
[ext_resource type="Script" path="res://save/player_save_manager.gd" id="9_save"]
[ext_resource type="Script" path="res://settings/player_settings_manager.gd" id="10_settings"]
[ext_resource type="Resource" path="res://items/catalog/item_catalog.tres" id="11_items"]
[node name="Main" type="Node3D"]
script = ExtResource("3_main")
fish_catalog = ExtResource("6_pool")
pelican_buyer_profile = ExtResource("7_pelicans")
item_catalog = ExtResource("11_items")
[node name="TestWorld" parent="." instance=ExtResource("1_world")]

View file

@ -6,6 +6,8 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
class ShowcaseCameraSnapshot:
extends RefCounted
@ -67,6 +69,8 @@ class ShowcaseCameraSnapshot:
@onready var collection_log: CollectionLogType = %CollectionLog
@onready var wallet: PlayerWalletType = %Wallet
@onready var fish_sale_service: FishSaleServiceType = %FishSaleService
@onready var bag: PlayerBagType = %Bag
@onready var hotbar: PlayerHotbarType = %Hotbar
@onready var _cast_origin: Marker3D = %CastOrigin
@onready var _fishing_rod: Node3D = %FishingRod
@onready var _fishing_rod_tip: Marker3D = %FishingRodTip
@ -198,10 +202,18 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("camera_zoom_in"):
if (
event is InputEventMouseButton
and event.shift_pressed
and event.is_action_pressed("camera_zoom_in")
):
_set_target_zoom(_target_zoom - zoom_step)
get_viewport().set_input_as_handled()
elif event.is_action_pressed("camera_zoom_out"):
elif (
event is InputEventMouseButton
and event.shift_pressed
and event.is_action_pressed("camera_zoom_out")
):
_set_target_zoom(_target_zoom + zoom_step)
get_viewport().set_input_as_handled()
@ -298,6 +310,13 @@ func get_fishing_rod() -> Node3D:
return _fishing_rod
func set_active_item_is_rod(active_is_rod: bool) -> void:
if _showcase_rod_state_stored:
_showcase_rod_visibility = active_is_rod
else:
_fishing_rod.visible = active_is_rod
func get_cast_origin_position() -> Vector3:
return _cast_origin.global_position

View file

@ -1,10 +1,12 @@
[gd_scene load_steps=13 format=3]
[gd_scene load_steps=15 format=3]
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
[ext_resource type="Script" path="res://collection/collection_log.gd" id="3_collection"]
[ext_resource type="Script" path="res://economy/player_wallet.gd" id="4_wallet"]
[ext_resource type="Script" path="res://economy/fish_sale_service.gd" id="5_sale_service"]
[ext_resource type="Script" path="res://inventory/player_bag.gd" id="6_bag"]
[ext_resource type="Script" path="res://inventory/player_hotbar.gd" id="7_hotbar"]
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
radius = 0.45
@ -88,6 +90,14 @@ script = ExtResource("4_wallet")
unique_name_in_owner = true
script = ExtResource("5_sale_service")
[node name="Bag" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("6_bag")
[node name="Hotbar" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("7_hotbar")
[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
unique_name_in_owner = true
position = Vector3(0, 1.45, -1.15)

View file

@ -29,9 +29,11 @@ Sprint Shift
Sneak Ctrl
Slow walk Alt
Rotate camera Hold right mouse, or use right stick
Zoom camera Mouse wheel
Cycle active hotbar slot Mouse wheel
Select hotbar slot 1 through 9
Zoom camera Shift + mouse wheel
Cast / withdraw / reel Left mouse
Inventory and Logbook Tab
Cooler, Bag, and Logbook Tab
Game Menu / back Escape
FISHING
@ -54,7 +56,8 @@ FEATURES TO TRY
- Barrier-and-chase catching
- Accessibility auto-click settings
- Catch showcase and fish size variation
- Inventory, Logbook, favorites, and sorting
- Cooler, Bag, Logbook, favorites, and sorting
- Basic Fishing Rod equipment and the 19 hotbar
- Pelican selling and wallet updates
- Save, Continue, New Game, and Delete Save
- Game Menu and persistent camera settings

View file

@ -92,6 +92,51 @@ camera_zoom_out={
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
hotbar_1={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_2={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_3={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_4={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":52,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_5={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":53,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_6={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":54,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_7={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":55,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_8={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":56,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
hotbar_9={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":57,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
open_backpack={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)

View file

@ -7,8 +7,13 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const SaveInspectionType = preload("res://save/player_save_inspection.gd")
const ItemCatalogType = preload("res://items/item_catalog.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 SAVE_VERSION: int = 1
const SAVE_VERSION: int = 2
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
const SAVE_PATH: String = "user://player_save.json"
const TEMP_PATH: String = "user://player_save.json.tmp"
const BACKUP_PATH: String = "user://player_save.json.backup"
@ -21,6 +26,9 @@ class LoadSnapshot:
var discovered_ids: Array[StringName] = []
var wallet_balance: int = 0
var next_catch_sequence: int = 1
var bag_items: Array[OwnedItemType] = []
var hotbar_slots: Array[StringName] = []
var selected_hotbar_slot: int = 0
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
@ -29,6 +37,9 @@ var _inventory: FishInventoryType
var _collection_log: CollectionLogType
var _wallet: PlayerWalletType
var _catalog: FishPoolType
var _bag: PlayerBagType
var _hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _autosave_timer: Timer
var _is_configured: bool = false
var _is_restoring: bool = false
@ -49,16 +60,25 @@ func setup(
collection_log: CollectionLogType,
wallet: PlayerWalletType,
catalog: FishPoolType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void:
_inventory = inventory
_collection_log = collection_log
_wallet = wallet
_catalog = catalog
_bag = bag
_hotbar = hotbar
_item_catalog = item_catalog
_is_configured = (
_inventory != null
and _collection_log != null
and _wallet != null
and _catalog != null
and _bag != null
and _hotbar != null
and _item_catalog != null
)
if not _is_configured:
push_error("PlayerSaveManager setup is missing required references.")
@ -71,6 +91,16 @@ func setup(
_collection_log.fish_discovered.connect(_on_fish_discovered)
if not _wallet.balance_changed.is_connected(_on_balance_changed):
_wallet.balance_changed.connect(_on_balance_changed)
if not _bag.contents_changed.is_connected(_mark_dirty):
_bag.contents_changed.connect(_mark_dirty)
if not _hotbar.slots_changed.is_connected(_mark_dirty):
_hotbar.slots_changed.connect(_mark_dirty)
if not _hotbar.selected_slot_changed.is_connected(
_on_selected_hotbar_slot_changed
):
_hotbar.selected_slot_changed.connect(
_on_selected_hotbar_slot_changed
)
func load_player_data() -> bool:
@ -131,11 +161,18 @@ func load_player_data() -> bool:
var wallet_restored: bool = _wallet.restore_balance(
snapshot.wallet_balance
)
var bag_restored: bool = _bag.replace_all_items(snapshot.bag_items)
var hotbar_restored: bool = _hotbar.replace_state(
snapshot.hotbar_slots,
snapshot.selected_hotbar_slot
)
_is_restoring = false
if (
not inventory_restored
or not collection_restored
or not wallet_restored
or not bag_restored
or not hotbar_restored
):
push_error("Validated player save could not be restored.")
return false
@ -174,10 +211,16 @@ func inspect_save() -> SaveInspectionType:
result.status = SaveInspectionType.Status.UNSUPPORTED_VERSION
result.message = "This save belongs to a newer game version."
return result
if result.detected_version != SAVE_VERSION:
if result.detected_version < 1:
result.status = SaveInspectionType.Status.MALFORMED
result.message = "The save version is unsupported."
return result
if result.detected_version != SAVE_VERSION:
save_data = _migrate_save(save_data, result.detected_version)
if save_data.is_empty():
result.status = SaveInspectionType.Status.MALFORMED
result.message = "The save version is unsupported."
return result
var snapshot: LoadSnapshot = _build_load_snapshot(save_data)
if snapshot == null:
result.status = SaveInspectionType.Status.MALFORMED
@ -300,6 +343,14 @@ func _build_save_dictionary() -> Dictionary:
if fish_id.is_empty():
return {}
discovered_strings.append(String(fish_id))
var serialized_items: Array[Dictionary] = []
for owned: OwnedItemType in _bag.get_all_items():
if owned == null or not owned.is_valid():
return {}
serialized_items.append(owned.to_save_dict())
var serialized_slots: Array[String] = []
for item_id: StringName in _hotbar.get_slots():
serialized_slots.append(String(item_id))
if (
_wallet.get_balance() < 0
or _wallet.get_balance() > MAX_SAFE_BALANCE
@ -323,6 +374,13 @@ func _build_save_dictionary() -> Dictionary:
"next_catch_sequence": _inventory.get_next_catch_sequence(),
"catches": serialized_catches,
},
"bag": {
"items": serialized_items,
},
"hotbar": {
"selected_slot": _hotbar.get_selected_slot(),
"slots": serialized_slots,
},
}
@ -331,16 +389,23 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
typeof(save_data.get("wallet")) != TYPE_DICTIONARY
or typeof(save_data.get("collection")) != TYPE_DICTIONARY
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
or typeof(save_data.get("bag")) != TYPE_DICTIONARY
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
):
return null
var wallet_data: Dictionary = save_data["wallet"]
var collection_data: Dictionary = save_data["collection"]
var inventory_data: Dictionary = save_data["inventory"]
var bag_data: Dictionary = save_data["bag"]
var hotbar_data: Dictionary = save_data["hotbar"]
if (
not wallet_data.has("balance")
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
or not inventory_data.has("next_catch_sequence")
or typeof(bag_data.get("items")) != TYPE_ARRAY
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
or not hotbar_data.has("selected_slot")
):
return null
@ -424,6 +489,81 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
requested_next_sequence,
maximum_sequence + 1
)
var seen_items: Dictionary[StringName, bool] = {}
var item_values: Array = bag_data["items"]
for item_index: int in range(item_values.size()):
var value: Variant = item_values[item_index]
if typeof(value) != TYPE_DICTIONARY:
push_warning("Skipped invalid saved Bag item.")
continue
var item_record: Dictionary = value
var item_id: StringName = StringName(
str(item_record.get("item_id", ""))
)
var item_data = _item_catalog.get_item_by_id(item_id)
var quantity: int = _read_integer(
item_record.get("quantity"),
-1,
999
)
if (
item_id.is_empty()
or item_data == null
or not item_data.is_valid()
or quantity <= 0
or seen_items.has(item_id)
):
push_warning(
"Skipped invalid or unknown saved Bag item '%s'."
% String(item_id)
)
continue
var maximum: int = (
item_data.max_stack if item_data.stackable else 1
)
if quantity > maximum:
push_warning(
"Skipped saved Bag item with invalid quantity '%s'."
% String(item_id)
)
continue
var owned := OwnedItemType.new()
owned.item_id = item_id
owned.quantity = quantity
seen_items[item_id] = true
snapshot.bag_items.append(owned)
var slot_values: Array = hotbar_data["slots"]
snapshot.hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
snapshot.hotbar_slots.fill(StringName())
for slot_index: int in range(
mini(slot_values.size(), PlayerHotbarType.SLOT_COUNT)
):
var slot_value: Variant = slot_values[slot_index]
if typeof(slot_value) not in [TYPE_STRING, TYPE_STRING_NAME]:
continue
var slot_item_id: StringName = StringName(str(slot_value))
if slot_item_id.is_empty():
continue
var slot_item = _item_catalog.get_item_by_id(slot_item_id)
if (
slot_item != null
and slot_item.is_valid()
and slot_item.hotbar_allowed
and seen_items.has(slot_item_id)
):
snapshot.hotbar_slots[slot_index] = slot_item_id
var selected_slot: int = _read_integer(
hotbar_data["selected_slot"],
0,
PlayerHotbarType.SLOT_COUNT - 1
)
snapshot.selected_hotbar_slot = clampi(
selected_slot,
0,
PlayerHotbarType.SLOT_COUNT - 1
)
return snapshot
@ -433,6 +573,26 @@ func _migrate_save(
) -> Dictionary:
if from_version == SAVE_VERSION:
return data
if from_version == 1:
var migrated: Dictionary = data.duplicate(true)
migrated["save_version"] = SAVE_VERSION
migrated["bag"] = {
"items": [
{
"item_id": String(BASIC_ROD_ID),
"quantity": 1,
},
],
}
var slots: Array[String] = []
slots.resize(PlayerHotbarType.SLOT_COUNT)
slots.fill("")
slots[0] = String(BASIC_ROD_ID)
migrated["hotbar"] = {
"selected_slot": 0,
"slots": slots,
}
return migrated
return {}
@ -456,6 +616,13 @@ func _on_balance_changed(_new_balance: int, _delta: int) -> void:
_mark_dirty()
func _on_selected_hotbar_slot_changed(
_slot_index: int,
_item_id: StringName,
) -> void:
_mark_dirty()
func _on_autosave_timeout() -> void:
if _is_dirty:
save_now()
@ -496,9 +663,20 @@ func _restore_defaults() -> void:
_is_restoring = true
var empty_catches: Array[FishCatchType] = []
var empty_discoveries: Array[StringName] = []
var default_items: Array[OwnedItemType] = []
var basic_rod := OwnedItemType.new()
basic_rod.item_id = BASIC_ROD_ID
basic_rod.quantity = 1
default_items.append(basic_rod)
var default_slots: Array[StringName] = []
default_slots.resize(PlayerHotbarType.SLOT_COUNT)
default_slots.fill(StringName())
default_slots[0] = BASIC_ROD_ID
_inventory.replace_all_catches(empty_catches, 1)
_collection_log.replace_discovered_ids(empty_discoveries)
_wallet.restore_balance(0)
_bag.replace_all_items(default_items)
_hotbar.replace_state(default_slots, 0)
_is_restoring = false
_is_dirty = false

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