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 FishingContextType = preload("res://fishing/fishing_context.gd")
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd") const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.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 PlayerType = preload("res://player/player.gd")
const FishableWaterRegionType = preload( const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd" "res://world/fishable_water_region.gd"
@ -31,6 +35,7 @@ signal showcase_changed(
visible: bool, visible: bool,
) )
signal bite_activated signal bite_activated
signal ready_for_equipment_refresh
enum FishingState { enum FishingState {
READY, READY,
@ -84,6 +89,9 @@ var _local_menu_input_owners: Dictionary[StringName, bool] = {}
var _local_player: PlayerType var _local_player: PlayerType
var _local_inventory: FishInventoryType var _local_inventory: FishInventoryType
var _local_collection_log: CollectionLogType var _local_collection_log: CollectionLogType
var _local_bag: PlayerBagType
var _local_hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _active_player: PlayerType var _active_player: PlayerType
var _state_time_remaining: float = 0.0 var _state_time_remaining: float = 0.0
var _cast_charge: float = 0.0 var _cast_charge: float = 0.0
@ -121,10 +129,16 @@ func setup(
local_player: PlayerType, local_player: PlayerType,
local_inventory: FishInventoryType, local_inventory: FishInventoryType,
local_collection_log: CollectionLogType, local_collection_log: CollectionLogType,
local_bag: PlayerBagType,
local_hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void: ) -> void:
_local_player = local_player _local_player = local_player
_local_inventory = local_inventory _local_inventory = local_inventory
_local_collection_log = local_collection_log _local_collection_log = local_collection_log
_local_bag = local_bag
_local_hotbar = local_hotbar
_item_catalog = item_catalog
func can_open_player_menu() -> bool: 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: func handle_escape_action() -> bool:
if not _gameplay_input_enabled or _external_input_blocked: if not _gameplay_input_enabled or _external_input_blocked:
return false return false
@ -301,6 +324,10 @@ func _unhandled_input(event: InputEvent) -> void:
FishingState.READY: FishingState.READY:
if not _new_cast_press_armed: if not _new_cast_press_armed:
return 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) _begin_aiming(_local_player)
FishingState.WAITING_FOR_BITE: FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = true _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: func _update_cast_charge(delta: float) -> void:
if state != FishingState.AIMING_CAST: if state != FishingState.AIMING_CAST:
return return
@ -581,20 +637,6 @@ func _on_catch_encounter_updated(
_bobber_water_position, _bobber_water_position,
Input.is_action_pressed("fish_primary") 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: func _on_catch_completed() -> void:
@ -731,6 +773,7 @@ func _cleanup_attempt(
state = FishingState.READY state = FishingState.READY
_cooldown_status = "" _cooldown_status = ""
status_changed.emit("") status_changed.emit("")
ready_for_equipment_refresh.emit()
else: else:
state = FishingState.COOLDOWN state = FishingState.COOLDOWN
_state_time_remaining = cooldown_duration _state_time_remaining = cooldown_duration
@ -743,6 +786,7 @@ func _return_to_ready() -> void:
_state_time_remaining = 0.0 _state_time_remaining = 0.0
_cooldown_status = "" _cooldown_status = ""
status_changed.emit("") status_changed.emit("")
ready_for_equipment_refresh.emit()
func _cancel_attempt() -> void: 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 PlayerSettingsType = preload("res://settings/player_settings.gd")
const TitleScreenType = preload("res://ui/title_screen.gd") const TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.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 fish_catalog: FishPoolType
@export var pelican_buyer_profile: FishBuyerProfileType @export var pelican_buyer_profile: FishBuyerProfileType
@export var item_catalog: ItemCatalogType
@onready var _test_world: TestWorldType = $TestWorld @onready var _test_world: TestWorldType = $TestWorld
@onready var _player: PlayerType = %Player @onready var _player: PlayerType = %Player
@ -38,17 +41,25 @@ func _ready() -> void:
_player.inventory, _player.inventory,
_player.wallet _player.wallet
) )
_player.bag.setup(item_catalog)
_player.hotbar.setup(_player.bag, item_catalog)
_save_manager.setup( _save_manager.setup(
_player.inventory, _player.inventory,
_player.collection_log, _player.collection_log,
_player.wallet, _player.wallet,
fish_catalog fish_catalog,
_player.bag,
_player.hotbar,
item_catalog
) )
_save_manager.set_autosave_enabled(false) _save_manager.set_autosave_enabled(false)
_fishing_spot.setup( _fishing_spot.setup(
_player, _player,
_player.inventory, _player.inventory,
_player.collection_log _player.collection_log,
_player.bag,
_player.hotbar,
item_catalog
) )
_game_ui.setup( _game_ui.setup(
_player, _player,
@ -58,7 +69,10 @@ func _ready() -> void:
_player.fish_sale_service, _player.fish_sale_service,
pelican_buyer_profile, pelican_buyer_profile,
fish_catalog, fish_catalog,
_fishing_spot _fishing_spot,
_player.bag,
_player.hotbar,
item_catalog
) )
_water_recovery.setup( _water_recovery.setup(
_player, _player,
@ -93,9 +107,22 @@ func _ready() -> void:
_on_reset_progress_requested _on_reset_progress_requested
) )
pause_menu.quit_requested.connect(_on_quit_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( _water_recovery.recovery_starting.connect(
_on_water_recovery_starting _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: func _input(event: InputEvent) -> void:
@ -182,6 +209,29 @@ func _on_water_recovery_starting() -> void:
_game_ui.get_pause_menu().close_for_water_recovery() _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: func _on_quit_requested() -> void:
_settings_manager.save_if_dirty() _settings_manager.save_if_dirty()
if _gameplay_started: 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://world/test_world.tscn" id="1_world"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] [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://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://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="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"] [node name="Main" type="Node3D"]
script = ExtResource("3_main") script = ExtResource("3_main")
fish_catalog = ExtResource("6_pool") fish_catalog = ExtResource("6_pool")
pelican_buyer_profile = ExtResource("7_pelicans") pelican_buyer_profile = ExtResource("7_pelicans")
item_catalog = ExtResource("11_items")
[node name="TestWorld" parent="." instance=ExtResource("1_world")] [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 FishCatchType = preload("res://fish/fish_catch.gd")
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd") const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
const PlayerWalletType = preload("res://economy/player_wallet.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: class ShowcaseCameraSnapshot:
extends RefCounted extends RefCounted
@ -67,6 +69,8 @@ class ShowcaseCameraSnapshot:
@onready var collection_log: CollectionLogType = %CollectionLog @onready var collection_log: CollectionLogType = %CollectionLog
@onready var wallet: PlayerWalletType = %Wallet @onready var wallet: PlayerWalletType = %Wallet
@onready var fish_sale_service: FishSaleServiceType = %FishSaleService @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 _cast_origin: Marker3D = %CastOrigin
@onready var _fishing_rod: Node3D = %FishingRod @onready var _fishing_rod: Node3D = %FishingRod
@onready var _fishing_rod_tip: Marker3D = %FishingRodTip @onready var _fishing_rod_tip: Marker3D = %FishingRodTip
@ -198,10 +202,18 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return 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) _set_target_zoom(_target_zoom - zoom_step)
get_viewport().set_input_as_handled() 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) _set_target_zoom(_target_zoom + zoom_step)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
@ -298,6 +310,13 @@ func get_fishing_rod() -> Node3D:
return _fishing_rod 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: func get_cast_origin_position() -> Vector3:
return _cast_origin.global_position 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://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://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://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/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://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"] [sub_resource type="CapsuleShape3D" id="PlayerShape"]
radius = 0.45 radius = 0.45
@ -88,6 +90,14 @@ script = ExtResource("4_wallet")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("5_sale_service") 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"] [node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
unique_name_in_owner = true unique_name_in_owner = true
position = Vector3(0, 1.45, -1.15) position = Vector3(0, 1.45, -1.15)

View file

@ -29,9 +29,11 @@ Sprint Shift
Sneak Ctrl Sneak Ctrl
Slow walk Alt Slow walk Alt
Rotate camera Hold right mouse, or use right stick 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 Cast / withdraw / reel Left mouse
Inventory and Logbook Tab Cooler, Bag, and Logbook Tab
Game Menu / back Escape Game Menu / back Escape
FISHING FISHING
@ -54,7 +56,8 @@ FEATURES TO TRY
- Barrier-and-chase catching - Barrier-and-chase catching
- Accessibility auto-click settings - Accessibility auto-click settings
- Catch showcase and fish size variation - 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 - Pelican selling and wallet updates
- Save, Continue, New Game, and Delete Save - Save, Continue, New Game, and Delete Save
- Game Menu and persistent camera settings - 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) "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={ open_backpack={
"deadzone": 0.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":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "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 PlayerWalletType = preload("res://economy/player_wallet.gd")
const FishPoolType = preload("res://fish/fish_pool.gd") const FishPoolType = preload("res://fish/fish_pool.gd")
const SaveInspectionType = preload("res://save/player_save_inspection.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 SAVE_PATH: String = "user://player_save.json"
const TEMP_PATH: String = "user://player_save.json.tmp" const TEMP_PATH: String = "user://player_save.json.tmp"
const BACKUP_PATH: String = "user://player_save.json.backup" const BACKUP_PATH: String = "user://player_save.json.backup"
@ -21,6 +26,9 @@ class LoadSnapshot:
var discovered_ids: Array[StringName] = [] var discovered_ids: Array[StringName] = []
var wallet_balance: int = 0 var wallet_balance: int = 0
var next_catch_sequence: int = 1 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 @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 _collection_log: CollectionLogType
var _wallet: PlayerWalletType var _wallet: PlayerWalletType
var _catalog: FishPoolType var _catalog: FishPoolType
var _bag: PlayerBagType
var _hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _autosave_timer: Timer var _autosave_timer: Timer
var _is_configured: bool = false var _is_configured: bool = false
var _is_restoring: bool = false var _is_restoring: bool = false
@ -49,16 +60,25 @@ func setup(
collection_log: CollectionLogType, collection_log: CollectionLogType,
wallet: PlayerWalletType, wallet: PlayerWalletType,
catalog: FishPoolType, catalog: FishPoolType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void: ) -> void:
_inventory = inventory _inventory = inventory
_collection_log = collection_log _collection_log = collection_log
_wallet = wallet _wallet = wallet
_catalog = catalog _catalog = catalog
_bag = bag
_hotbar = hotbar
_item_catalog = item_catalog
_is_configured = ( _is_configured = (
_inventory != null _inventory != null
and _collection_log != null and _collection_log != null
and _wallet != null and _wallet != null
and _catalog != null and _catalog != null
and _bag != null
and _hotbar != null
and _item_catalog != null
) )
if not _is_configured: if not _is_configured:
push_error("PlayerSaveManager setup is missing required references.") push_error("PlayerSaveManager setup is missing required references.")
@ -71,6 +91,16 @@ func setup(
_collection_log.fish_discovered.connect(_on_fish_discovered) _collection_log.fish_discovered.connect(_on_fish_discovered)
if not _wallet.balance_changed.is_connected(_on_balance_changed): if not _wallet.balance_changed.is_connected(_on_balance_changed):
_wallet.balance_changed.connect(_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: func load_player_data() -> bool:
@ -131,11 +161,18 @@ func load_player_data() -> bool:
var wallet_restored: bool = _wallet.restore_balance( var wallet_restored: bool = _wallet.restore_balance(
snapshot.wallet_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 _is_restoring = false
if ( if (
not inventory_restored not inventory_restored
or not collection_restored or not collection_restored
or not wallet_restored or not wallet_restored
or not bag_restored
or not hotbar_restored
): ):
push_error("Validated player save could not be restored.") push_error("Validated player save could not be restored.")
return false return false
@ -174,7 +211,13 @@ func inspect_save() -> SaveInspectionType:
result.status = SaveInspectionType.Status.UNSUPPORTED_VERSION result.status = SaveInspectionType.Status.UNSUPPORTED_VERSION
result.message = "This save belongs to a newer game version." result.message = "This save belongs to a newer game version."
return result return result
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: 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.status = SaveInspectionType.Status.MALFORMED
result.message = "The save version is unsupported." result.message = "The save version is unsupported."
return result return result
@ -300,6 +343,14 @@ func _build_save_dictionary() -> Dictionary:
if fish_id.is_empty(): if fish_id.is_empty():
return {} return {}
discovered_strings.append(String(fish_id)) 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 ( if (
_wallet.get_balance() < 0 _wallet.get_balance() < 0
or _wallet.get_balance() > MAX_SAFE_BALANCE or _wallet.get_balance() > MAX_SAFE_BALANCE
@ -323,6 +374,13 @@ func _build_save_dictionary() -> Dictionary:
"next_catch_sequence": _inventory.get_next_catch_sequence(), "next_catch_sequence": _inventory.get_next_catch_sequence(),
"catches": serialized_catches, "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 typeof(save_data.get("wallet")) != TYPE_DICTIONARY
or typeof(save_data.get("collection")) != TYPE_DICTIONARY or typeof(save_data.get("collection")) != TYPE_DICTIONARY
or typeof(save_data.get("inventory")) != 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 return null
var wallet_data: Dictionary = save_data["wallet"] var wallet_data: Dictionary = save_data["wallet"]
var collection_data: Dictionary = save_data["collection"] var collection_data: Dictionary = save_data["collection"]
var inventory_data: Dictionary = save_data["inventory"] var inventory_data: Dictionary = save_data["inventory"]
var bag_data: Dictionary = save_data["bag"]
var hotbar_data: Dictionary = save_data["hotbar"]
if ( if (
not wallet_data.has("balance") not wallet_data.has("balance")
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
or typeof(inventory_data.get("catches")) != TYPE_ARRAY or typeof(inventory_data.get("catches")) != TYPE_ARRAY
or not inventory_data.has("next_catch_sequence") 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 return null
@ -424,6 +489,81 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
requested_next_sequence, requested_next_sequence,
maximum_sequence + 1 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 return snapshot
@ -433,6 +573,26 @@ func _migrate_save(
) -> Dictionary: ) -> Dictionary:
if from_version == SAVE_VERSION: if from_version == SAVE_VERSION:
return data 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 {} return {}
@ -456,6 +616,13 @@ func _on_balance_changed(_new_balance: int, _delta: int) -> void:
_mark_dirty() _mark_dirty()
func _on_selected_hotbar_slot_changed(
_slot_index: int,
_item_id: StringName,
) -> void:
_mark_dirty()
func _on_autosave_timeout() -> void: func _on_autosave_timeout() -> void:
if _is_dirty: if _is_dirty:
save_now() save_now()
@ -496,9 +663,20 @@ func _restore_defaults() -> void:
_is_restoring = true _is_restoring = true
var empty_catches: Array[FishCatchType] = [] var empty_catches: Array[FishCatchType] = []
var empty_discoveries: Array[StringName] = [] 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) _inventory.replace_all_catches(empty_catches, 1)
_collection_log.replace_discovered_ids(empty_discoveries) _collection_log.replace_discovered_ids(empty_discoveries)
_wallet.restore_balance(0) _wallet.restore_balance(0)
_bag.replace_all_items(default_items)
_hotbar.replace_state(default_slots, 0)
_is_restoring = false _is_restoring = false
_is_dirty = 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 PlayerMenuType = preload("res://ui/player_menu.gd")
const PlayerType = preload("res://player/player.gd") const PlayerType = preload("res://player/player.gd")
const PlayerWalletType = preload("res://economy/player_wallet.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 TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.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 _screen_fade: ScreenFade = %ScreenFade
@onready var _title_screen: TitleScreenType = %TitleScreen @onready var _title_screen: TitleScreenType = %TitleScreen
@onready var _pause_menu: PauseMenuType = %PauseMenu @onready var _pause_menu: PauseMenuType = %PauseMenu
@onready var _hotbar_ui: HotbarUIType = %Hotbar
var _showcase_active: bool = false var _showcase_active: bool = false
var _player_menu_open: bool = false var _player_menu_open: bool = false
var _gameplay_ui_enabled: bool = false var _gameplay_ui_enabled: bool = false
var _fishing_spot: FishingSpotType
func setup( func setup(
@ -41,7 +47,11 @@ func setup(
default_buyer: FishBuyerProfileType, default_buyer: FishBuyerProfileType,
catalog: FishPoolType, catalog: FishPoolType,
fishing_spot: FishingSpotType, fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void: ) -> void:
_fishing_spot = fishing_spot
fishing_spot.status_changed.connect(_on_fishing_status_changed) fishing_spot.status_changed.connect(_on_fishing_status_changed)
fishing_spot.catch_display_changed.connect(_on_catch_display_changed) fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
fishing_spot.showcase_changed.connect(_on_showcase_changed) fishing_spot.showcase_changed.connect(_on_showcase_changed)
@ -56,8 +66,12 @@ func setup(
sale_service, sale_service,
default_buyer, default_buyer,
catalog, catalog,
fishing_spot fishing_spot,
bag,
hotbar,
item_catalog
) )
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot)
func close_player_menu() -> void: func close_player_menu() -> void:
@ -89,10 +103,22 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
if not enabled: if not enabled:
close_player_menu_for_session_end() close_player_menu_for_session_end()
_fishing_panel.visible = false _fishing_panel.visible = false
_hotbar_ui.hide()
_hotbar_ui.set_gameplay_input_enabled(false)
else: else:
_hotbar_ui.show()
_hotbar_ui.set_gameplay_input_enabled(true)
_refresh_fishing_panel_visibility() _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: func get_screen_fade() -> ScreenFade:
return _screen_fade return _screen_fade
@ -104,6 +130,8 @@ func get_title_screen() -> TitleScreenType:
func _on_fishing_status_changed(status: String) -> void: func _on_fishing_status_changed(status: String) -> void:
if _showcase_active: if _showcase_active:
return return
if _fishing_spot != null and _fishing_spot.is_fighting():
return
_set_fishing_status(status) _set_fishing_status(status)
@ -125,7 +153,6 @@ func _refresh_fishing_panel_visibility() -> void:
_fishing_panel.visible = ( _fishing_panel.visible = (
_gameplay_ui_enabled _gameplay_ui_enabled
and has_content and has_content
and not _player_menu_open
) )
@ -138,17 +165,26 @@ func _on_catch_display_changed(
active_barrier_index: int, active_barrier_index: int,
visible: bool, visible: bool,
) -> void: ) -> 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 _green_catch_progress.value = progress * 100.0
_red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0 _red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0
_catch_track.visible = visible _catch_track.visible = encounter_visible
_barrier_summary.visible = visible _barrier_summary.visible = encounter_visible
if not visible: _barrier_health.visible = encounter_visible
if not encounter_visible:
_barrier_health.visible = false _barrier_health.visible = false
_clear_barrier_markers() _clear_barrier_markers()
_barrier_summary.text = "" _barrier_summary.text = ""
_barrier_health.text = "" _barrier_health.text = ""
_refresh_fishing_panel_visibility() _refresh_fishing_panel_visibility()
return return
_status_label.text = ""
_status_label.visible = false
_update_barrier_markers( _update_barrier_markers(
barrier_positions, barrier_positions,
@ -183,7 +219,7 @@ func _on_catch_display_changed(
] ]
) )
else: else:
_barrier_health.text = "" _barrier_health.text = "Hold left click to reel"
var chase_gap: float = progress - chase_progress var chase_gap: float = progress - chase_progress
if chase_gap <= 0.05: if chase_gap <= 0.05:
@ -194,10 +230,6 @@ func _on_catch_display_changed(
"\nRed is closing in!", "\nRed is closing in!",
] ]
).strip_edges() ).strip_edges()
_barrier_health.visible = (
visible
and (active_barrier_index >= 0 or chase_gap <= 0.05)
)
_refresh_fishing_panel_visibility() _refresh_fishing_panel_visibility()
@ -262,4 +294,8 @@ func _on_showcase_changed(
func _on_player_menu_visibility_changed(is_open: bool) -> void: func _on_player_menu_visibility_changed(is_open: bool) -> void:
_player_menu_open = is_open _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() _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="Script" path="res://ui/game_ui.gd" id="1_ui"]
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"] [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="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/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/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"] [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
bg_color = Color(0.055, 0.105, 0.125, 1) bg_color = Color(0.055, 0.105, 0.125, 1)
@ -37,35 +38,36 @@ script = ExtResource("1_ui")
[node name="FishingPanel" type="PanelContainer" parent="."] [node name="FishingPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
z_index = 60
anchors_preset = 7 anchors_preset = 7
anchor_left = 0.5 anchor_left = 0.5
anchor_top = 1.0 anchor_top = 1.0
anchor_right = 0.5 anchor_right = 0.5
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = -210.0 offset_left = -205.0
offset_top = -130.0 offset_top = -216.0
offset_right = 210.0 offset_right = 205.0
offset_bottom = -24.0 offset_bottom = -128.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
mouse_filter = 2 mouse_filter = 2
theme = ExtResource("3_theme") theme = ExtResource("3_theme")
[node name="MarginContainer" type="MarginContainer" parent="FishingPanel"] [node name="MarginContainer" type="MarginContainer" parent="FishingPanel"]
theme_override_constants/margin_left = 18 theme_override_constants/margin_left = 14
theme_override_constants/margin_top = 12 theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 18 theme_override_constants/margin_right = 14
theme_override_constants/margin_bottom = 12 theme_override_constants/margin_bottom = 8
[node name="VBoxContainer" type="VBoxContainer" parent="FishingPanel/MarginContainer"] [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"] [node name="StatusLabel" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
text = "" text = ""
horizontal_alignment = 1 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"] [node name="ShowcaseDetails" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
@ -75,7 +77,7 @@ horizontal_alignment = 1
[node name="CatchTrack" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer"] [node name="CatchTrack" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
custom_minimum_size = Vector2(0, 22) custom_minimum_size = Vector2(0, 20)
[node name="GreenCatchProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] [node name="GreenCatchProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"]
unique_name_in_owner = true unique_name_in_owner = true
@ -128,6 +130,9 @@ horizontal_alignment = 1
[node name="PlayerMenu" parent="." instance=ExtResource("2_menu")] [node name="PlayerMenu" parent="." instance=ExtResource("2_menu")]
unique_name_in_owner = true unique_name_in_owner = true
[node name="Hotbar" parent="." instance=ExtResource("7_hotbar")]
unique_name_in_owner = true
[node name="ScreenFade" type="ColorRect" parent="."] [node name="ScreenFade" type="ColorRect" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
visible = false 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 return_to_title_requested
signal reset_progress_requested signal reset_progress_requested
signal quit_requested signal quit_requested
signal menu_visibility_changed(is_open: bool)
enum ConfirmationAction { enum ConfirmationAction {
NONE, NONE,
@ -100,6 +101,7 @@ func open_menu() -> void:
_confirmation_action = ConfirmationAction.NONE _confirmation_action = ConfirmationAction.NONE
show() show()
%ResumeButton.grab_focus() %ResumeButton.grab_focus()
menu_visibility_changed.emit(true)
func resume() -> void: func resume() -> void:
@ -138,6 +140,7 @@ func close_menu(
else: else:
_control_snapshot_stored = false _control_snapshot_stored = false
_apply_mouse_close_policy(reason) _apply_mouse_close_policy(reason)
menu_visibility_changed.emit(false)
func handle_escape() -> bool: 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 FishingSpotType = preload("res://fishing/fishing_spot.gd")
const PlayerType = preload("res://player/player.gd") const PlayerType = preload("res://player/player.gd")
const PlayerWalletType = preload("res://economy/player_wallet.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) signal menu_visibility_changed(is_open: bool)
enum Section { enum Section {
INVENTORY, COOLER,
BAG,
LOGBOOK, LOGBOOK,
} }
@ -37,11 +44,18 @@ enum CloseReason {
} }
@onready var _inventory_tab: Button = %InventoryTab @onready var _inventory_tab: Button = %InventoryTab
@onready var _bag_tab: Button = %BagTab
@onready var _logbook_tab: Button = %LogbookTab @onready var _logbook_tab: Button = %LogbookTab
@onready var _close_button: Button = %CloseButton @onready var _close_button: Button = %CloseButton
@onready var _wallet_balance: Label = %WalletBalance @onready var _wallet_balance: Label = %WalletBalance
@onready var _inventory_section: Control = %InventorySection @onready var _inventory_section: Control = %InventorySection
@onready var _bag_section: Control = %BagSection
@onready var _logbook_section: Control = %LogbookSection @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_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection @onready var _sort_direction: Button = %SortDirection
@onready var _held_value: Label = %HeldValue @onready var _held_value: Label = %HeldValue
@ -69,10 +83,14 @@ var _sale_service: FishSaleServiceType
var _default_buyer: FishBuyerProfileType var _default_buyer: FishBuyerProfileType
var _catalog: FishPoolType var _catalog: FishPoolType
var _fishing_spot: FishingSpotType 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_mode: SortMode = SortMode.CATCH_ORDER
var _sort_descending: bool = true var _sort_descending: bool = true
var _selected_catch_id: StringName var _selected_catch_id: StringName
var _selected_bag_item_id: StringName
var _prior_movement_enabled: bool = true var _prior_movement_enabled: bool = true
var _prior_camera_input_enabled: bool = true var _prior_camera_input_enabled: bool = true
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
@ -87,8 +105,9 @@ var _sale_in_progress: bool = false
func _ready() -> void: func _ready() -> void:
_inventory_tab.pressed.connect( _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( _logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK) _show_section.bind(Section.LOGBOOK)
) )
@ -116,6 +135,9 @@ func setup(
default_buyer: FishBuyerProfileType, default_buyer: FishBuyerProfileType,
catalog: FishPoolType, catalog: FishPoolType,
fishing_spot: FishingSpotType, fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
) -> void: ) -> void:
_player = player _player = player
_inventory = inventory _inventory = inventory
@ -125,6 +147,9 @@ func setup(
_default_buyer = default_buyer _default_buyer = default_buyer
_catalog = catalog _catalog = catalog
_fishing_spot = fishing_spot _fishing_spot = fishing_spot
_bag = bag
_hotbar = hotbar
_item_catalog = item_catalog
if not _inventory.catches_changed.is_connected(_on_inventory_changed): if not _inventory.catches_changed.is_connected(_on_inventory_changed):
_inventory.catches_changed.connect(_on_inventory_changed) _inventory.catches_changed.connect(_on_inventory_changed)
if not _collection_log.fish_discovered.is_connected(_on_fish_discovered): 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) _wallet.balance_changed.connect(_on_wallet_balance_changed)
if not _fishing_spot.bite_activated.is_connected(_on_bite_activated): if not _fishing_spot.bite_activated.is_connected(_on_bite_activated):
_fishing_spot.bite_activated.connect(_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() _refresh_all()
@ -194,6 +221,7 @@ func close_menu(
) -> void: ) -> void:
if not visible: if not visible:
return return
get_viewport().gui_cancel_drag()
_close_sale_confirmation() _close_sale_confirmation()
var closing_generation: int = _menu_generation var closing_generation: int = _menu_generation
visible = false visible = false
@ -268,17 +296,21 @@ func _show_section(section: Section) -> void:
_current_section = section _current_section = section
if not is_node_ready(): if not is_node_ready():
return 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 _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 _logbook_tab.button_pressed = section == Section.LOGBOOK
if visible: if visible:
_focus_current_section() _focus_current_section()
func _focus_current_section() -> void: func _focus_current_section() -> void:
if _current_section == Section.INVENTORY: if _current_section == Section.COOLER:
_inventory_tab.grab_focus() _inventory_tab.grab_focus()
elif _current_section == Section.BAG:
_bag_tab.grab_focus()
else: else:
_logbook_tab.grab_focus() _logbook_tab.grab_focus()
@ -307,9 +339,99 @@ func _update_sort_direction_text() -> void:
func _refresh_all() -> void: func _refresh_all() -> void:
_refresh_economy_summary() _refresh_economy_summary()
_refresh_inventory() _refresh_inventory()
_refresh_bag()
_refresh_logbook() _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: func _on_inventory_changed() -> void:
_refresh_economy_summary() _refresh_economy_summary()
_revalidate_confirmation() _revalidate_confirmation()
@ -384,7 +506,7 @@ func _compare_catches(left: FishCatchType, right: FishCatchType) -> bool:
func _create_inventory_card(fish_catch: FishCatchType) -> Button: func _create_inventory_card(fish_catch: FishCatchType) -> Button:
var card := Button.new() var card := Button.new()
card.theme_type_variation = &"CardButton" 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.toggle_mode = true
card.button_pressed = fish_catch.catch_id == _selected_catch_id card.button_pressed = fish_catch.catch_id == _selected_catch_id
card.pressed.connect(_select_catch.bind(fish_catch.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) card.add_child(content)
content.add_child(_create_texture_frame( content.add_child(_create_texture_frame(
fish_catch.fish.display_texture, fish_catch.fish.display_texture,
Vector2(124.0, 78.0) Vector2(108.0, 62.0)
)) ))
var name_label := Label.new() var name_label := Label.new()
name_label.text = fish_catch.fish.display_name name_label.text = fish_catch.fish.display_name

View file

@ -5,17 +5,19 @@
[node name="PlayerMenu" type="Control"] [node name="PlayerMenu" type="Control"]
visible = false visible = false
z_index = 30
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
mouse_filter = 0 mouse_filter = 2
theme = ExtResource("2_theme") theme = ExtResource("2_theme")
script = ExtResource("1_menu") script = ExtResource("1_menu")
[node name="Dimmer" type="ColorRect" parent="."] [node name="Dimmer" type="ColorRect" parent="."]
z_index = -20
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@ -27,23 +29,23 @@ color = Color(0.015, 0.02, 0.03, 0.72)
[node name="MenuPanel" type="PanelContainer" parent="."] [node name="MenuPanel" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1
anchor_left = 0.055 anchor_left = 0.07
anchor_top = 0.045 anchor_top = 0.055
anchor_right = 0.945 anchor_right = 0.93
anchor_bottom = 0.955 anchor_bottom = 0.72
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
[node name="Margin" type="MarginContainer" parent="MenuPanel"] [node name="Margin" type="MarginContainer" parent="MenuPanel"]
layout_mode = 2 layout_mode = 2
theme_override_constants/margin_left = 22 theme_override_constants/margin_left = 14
theme_override_constants/margin_top = 18 theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 22 theme_override_constants/margin_right = 14
theme_override_constants/margin_bottom = 20 theme_override_constants/margin_bottom = 12
[node name="Layout" type="VBoxContainer" parent="MenuPanel/Margin"] [node name="Layout" type="VBoxContainer" parent="MenuPanel/Margin"]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 theme_override_constants/separation = 8
[node name="Header" type="HBoxContainer" parent="MenuPanel/Margin/Layout"] [node name="Header" type="HBoxContainer" parent="MenuPanel/Margin/Layout"]
layout_mode = 2 layout_mode = 2
@ -53,7 +55,7 @@ theme_override_constants/separation = 8
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) 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" text = "Player Menu"
[node name="WalletBalance" type="Label" parent="MenuPanel/Margin/Layout/Header"] [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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
text = "Inventory" text = "Cooler"
custom_minimum_size = Vector2(104, 36) 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"] [node name="LogbookTab" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
text = "Logbook" text = "Logbook"
custom_minimum_size = Vector2(104, 36) custom_minimum_size = Vector2(88, 30)
[node name="CloseButton" type="Button" parent="MenuPanel/Margin/Layout/Header"] [node name="CloseButton" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "Close" text = "Close"
custom_minimum_size = Vector2(76, 36) custom_minimum_size = Vector2(68, 30)
[node name="Separator" type="HSeparator" parent="MenuPanel/Margin/Layout"] [node name="Separator" type="HSeparator" parent="MenuPanel/Margin/Layout"]
layout_mode = 2 layout_mode = 2
@ -91,10 +100,10 @@ layout_mode = 2
text = "" text = ""
horizontal_alignment = 1 horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.82, 0.4, 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"] [node name="Content" type="Control" parent="MenuPanel/Margin/Layout"]
custom_minimum_size = Vector2(0, 390) custom_minimum_size = Vector2(0, 260)
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
@ -106,11 +115,11 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 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"] [node name="SortBar" type="HBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"]
layout_mode = 2 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"] [node name="SortLabel" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
layout_mode = 2 layout_mode = 2
@ -119,12 +128,12 @@ text = "Sort:"
[node name="SortOption" type="OptionButton" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] [node name="SortOption" type="OptionButton" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 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"] [node name="SortDirection" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
custom_minimum_size = Vector2(125, 36) custom_minimum_size = Vector2(112, 30)
text = "Newest first" text = "Newest first"
[node name="SortSpacer" type="Control" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] [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"] [node name="InventoryBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
split_offset = 560 split_offset = 510
theme_override_constants/separation = 12 theme_override_constants/separation = 8
[node name="InventoryList" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"] [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 layout_mode = 2
[node name="InventoryListMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList"] [node name="InventoryListMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList"]
layout_mode = 2 layout_mode = 2
theme_override_constants/margin_left = 12 theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 12 theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 12 theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 12 theme_override_constants/margin_bottom = 8
[node name="InventoryStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin"] [node name="InventoryStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin"]
layout_mode = 2 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"] [node name="InventoryEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "No fish in your inventory." text = "Your Cooler is empty."
horizontal_alignment = 1 horizontal_alignment = 1
[node name="InventoryScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"] [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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_constants/h_separation = 10 theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 10 theme_override_constants/v_separation = 7
columns = 3 columns = 3
[node name="DetailPanel" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"] [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 layout_mode = 2
[node name="DetailMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel"] [node name="DetailMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel"]
layout_mode = 2 layout_mode = 2
theme_override_constants/margin_left = 16 theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 14 theme_override_constants/margin_top = 9
theme_override_constants/margin_right = 16 theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 14 theme_override_constants/margin_bottom = 9
[node name="DetailStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin"] [node name="DetailStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin"]
layout_mode = 2 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"] [node name="DetailTexture" type="TextureRect" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(208, 140) custom_minimum_size = Vector2(178, 100)
layout_mode = 2 layout_mode = 2
expand_mode = 1 expand_mode = 1
stretch_mode = 5 stretch_mode = 5
@ -204,7 +213,7 @@ texture_filter = 1
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) 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 horizontal_alignment = 1
[node name="DetailData" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] [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 horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.702, 0.278, 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"] [node name="LogbookSection" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
@ -243,7 +344,7 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 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"] [node name="LogbookEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/LogbookSection"]
unique_name_in_owner = true unique_name_in_owner = true
@ -260,13 +361,14 @@ horizontal_scroll_mode = 0
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_constants/h_separation = 10 theme_override_constants/h_separation = 7
theme_override_constants/v_separation = 10 theme_override_constants/v_separation = 7
columns = 4 columns = 4
[node name="SaleConfirmation" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content"] [node name="SaleConfirmation" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
z_index = 40
layout_mode = 1 layout_mode = 1
anchors_preset = 8 anchors_preset = 8
anchor_left = 0.5 anchor_left = 0.5