Add multiplayer fish showcases
This commit is contained in:
parent
3f4392fdd3
commit
904a9c2d9e
23 changed files with 966 additions and 46 deletions
|
|
@ -55,6 +55,7 @@ signal showcase_changed(
|
||||||
)
|
)
|
||||||
signal bite_activated
|
signal bite_activated
|
||||||
signal ready_for_equipment_refresh
|
signal ready_for_equipment_refresh
|
||||||
|
signal fish_showcase_toggle_requested
|
||||||
|
|
||||||
enum FishingState {
|
enum FishingState {
|
||||||
READY,
|
READY,
|
||||||
|
|
@ -449,6 +450,13 @@ 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 (
|
||||||
|
_local_hotbar != null
|
||||||
|
and not _local_hotbar.get_selected_fish_catch_id().is_empty()
|
||||||
|
):
|
||||||
|
fish_showcase_toggle_requested.emit()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
var active_item: ItemDataType = _get_active_item()
|
var active_item: ItemDataType = _get_active_item()
|
||||||
if (
|
if (
|
||||||
active_item != null
|
active_item != null
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,56 @@ const SLOT_COUNT: int = 9
|
||||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const ItemDataType = preload("res://items/item_data.gd")
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
|
||||||
|
enum AssignmentKind {
|
||||||
|
EMPTY,
|
||||||
|
ITEM,
|
||||||
|
FISH,
|
||||||
|
}
|
||||||
|
|
||||||
signal slots_changed
|
signal slots_changed
|
||||||
signal selected_slot_changed(slot_index: int, item_id: StringName)
|
signal selected_slot_changed(slot_index: int, item_id: StringName)
|
||||||
|
signal selected_assignment_changed(
|
||||||
|
slot_index: int,
|
||||||
|
kind: AssignmentKind,
|
||||||
|
identity: StringName,
|
||||||
|
)
|
||||||
|
|
||||||
var _bag: PlayerBagType
|
var _bag: PlayerBagType
|
||||||
var _catalog: ItemCatalogType
|
var _catalog: ItemCatalogType
|
||||||
|
var _fish_inventory: FishInventoryType
|
||||||
var _slots: Array[StringName] = []
|
var _slots: Array[StringName] = []
|
||||||
|
var _fish_slots: Array[StringName] = []
|
||||||
var _selected_slot: int = 0
|
var _selected_slot: int = 0
|
||||||
|
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
_slots.resize(SLOT_COUNT)
|
_slots.resize(SLOT_COUNT)
|
||||||
_slots.fill(StringName())
|
_slots.fill(StringName())
|
||||||
|
_fish_slots.resize(SLOT_COUNT)
|
||||||
|
_fish_slots.fill(StringName())
|
||||||
|
|
||||||
|
|
||||||
func setup(bag: PlayerBagType, catalog: ItemCatalogType) -> void:
|
func setup(
|
||||||
|
bag: PlayerBagType,
|
||||||
|
catalog: ItemCatalogType,
|
||||||
|
fish_inventory: FishInventoryType = null,
|
||||||
|
) -> void:
|
||||||
_bag = bag
|
_bag = bag
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
if _bag != null and not _bag.contents_changed.is_connected(
|
if _bag != null and not _bag.contents_changed.is_connected(
|
||||||
_on_bag_contents_changed
|
_on_bag_contents_changed
|
||||||
):
|
):
|
||||||
_bag.contents_changed.connect(_on_bag_contents_changed)
|
_bag.contents_changed.connect(_on_bag_contents_changed)
|
||||||
|
if (
|
||||||
|
_fish_inventory != null
|
||||||
|
and not _fish_inventory.catches_changed.is_connected(
|
||||||
|
_on_fish_inventory_changed
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_fish_inventory.catches_changed.connect(_on_fish_inventory_changed)
|
||||||
_validate_assignments()
|
_validate_assignments()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,23 +63,55 @@ func assign_item(slot_index: int, item_id: StringName) -> bool:
|
||||||
return false
|
return false
|
||||||
if _slots[slot_index] == item_id:
|
if _slots[slot_index] == item_id:
|
||||||
return true
|
return true
|
||||||
|
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
||||||
for index: int in range(SLOT_COUNT):
|
for index: int in range(SLOT_COUNT):
|
||||||
if index != slot_index and _slots[index] == item_id:
|
if index != slot_index and _slots[index] == item_id:
|
||||||
_slots[index] = StringName()
|
_slots[index] = StringName()
|
||||||
|
selected_slot_was_affected = (
|
||||||
|
selected_slot_was_affected or index == _selected_slot
|
||||||
|
)
|
||||||
_slots[slot_index] = item_id
|
_slots[slot_index] = item_id
|
||||||
|
_fish_slots[slot_index] = StringName()
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
if slot_index == _selected_slot:
|
if selected_slot_was_affected:
|
||||||
selected_slot_changed.emit(_selected_slot, item_id)
|
_emit_selected_assignment()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func assign_fish(slot_index: int, catch_id: StringName) -> bool:
|
||||||
|
if not _is_slot_valid(slot_index) or not _can_assign_fish(catch_id):
|
||||||
|
return false
|
||||||
|
if _fish_slots[slot_index] == catch_id:
|
||||||
|
return true
|
||||||
|
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
||||||
|
for index: int in range(SLOT_COUNT):
|
||||||
|
if index != slot_index and _fish_slots[index] == catch_id:
|
||||||
|
_fish_slots[index] = StringName()
|
||||||
|
selected_slot_was_affected = (
|
||||||
|
selected_slot_was_affected or index == _selected_slot
|
||||||
|
)
|
||||||
|
_slots[slot_index] = StringName()
|
||||||
|
_fish_slots[slot_index] = catch_id
|
||||||
|
slots_changed.emit()
|
||||||
|
if selected_slot_was_affected:
|
||||||
|
_emit_selected_assignment()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
func clear_slot(slot_index: int) -> bool:
|
func clear_slot(slot_index: int) -> bool:
|
||||||
if not _is_slot_valid(slot_index) or _slots[slot_index].is_empty():
|
if (
|
||||||
|
not _is_slot_valid(slot_index)
|
||||||
|
or (
|
||||||
|
_slots[slot_index].is_empty()
|
||||||
|
and _fish_slots[slot_index].is_empty()
|
||||||
|
)
|
||||||
|
):
|
||||||
return false
|
return false
|
||||||
_slots[slot_index] = StringName()
|
_slots[slot_index] = StringName()
|
||||||
|
_fish_slots[slot_index] = StringName()
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
if slot_index == _selected_slot:
|
if slot_index == _selected_slot:
|
||||||
selected_slot_changed.emit(_selected_slot, StringName())
|
_emit_selected_assignment()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,12 +125,12 @@ func swap_slots(from_index: int, to_index: int) -> bool:
|
||||||
var temporary: StringName = _slots[from_index]
|
var temporary: StringName = _slots[from_index]
|
||||||
_slots[from_index] = _slots[to_index]
|
_slots[from_index] = _slots[to_index]
|
||||||
_slots[to_index] = temporary
|
_slots[to_index] = temporary
|
||||||
|
temporary = _fish_slots[from_index]
|
||||||
|
_fish_slots[from_index] = _fish_slots[to_index]
|
||||||
|
_fish_slots[to_index] = temporary
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
if from_index == _selected_slot or to_index == _selected_slot:
|
if from_index == _selected_slot or to_index == _selected_slot:
|
||||||
selected_slot_changed.emit(
|
_emit_selected_assignment()
|
||||||
_selected_slot,
|
|
||||||
_slots[_selected_slot]
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -80,7 +140,7 @@ func select_slot(slot_index: int) -> bool:
|
||||||
if _selected_slot == slot_index:
|
if _selected_slot == slot_index:
|
||||||
return true
|
return true
|
||||||
_selected_slot = slot_index
|
_selected_slot = slot_index
|
||||||
selected_slot_changed.emit(_selected_slot, _slots[_selected_slot])
|
_emit_selected_assignment()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,21 +160,55 @@ func get_selected_item_id() -> StringName:
|
||||||
return _slots[_selected_slot]
|
return _slots[_selected_slot]
|
||||||
|
|
||||||
|
|
||||||
|
func get_selected_fish_catch_id() -> StringName:
|
||||||
|
return _fish_slots[_selected_slot]
|
||||||
|
|
||||||
|
|
||||||
|
func get_selected_assignment_kind() -> AssignmentKind:
|
||||||
|
return get_assignment_kind(_selected_slot)
|
||||||
|
|
||||||
|
|
||||||
func get_item_id(slot_index: int) -> StringName:
|
func get_item_id(slot_index: int) -> StringName:
|
||||||
return _slots[slot_index] if _is_slot_valid(slot_index) else StringName()
|
return _slots[slot_index] if _is_slot_valid(slot_index) else StringName()
|
||||||
|
|
||||||
|
|
||||||
|
func get_fish_catch_id(slot_index: int) -> StringName:
|
||||||
|
return (
|
||||||
|
_fish_slots[slot_index]
|
||||||
|
if _is_slot_valid(slot_index)
|
||||||
|
else StringName()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_assignment_kind(slot_index: int) -> AssignmentKind:
|
||||||
|
if not _is_slot_valid(slot_index):
|
||||||
|
return AssignmentKind.EMPTY
|
||||||
|
if not _fish_slots[slot_index].is_empty():
|
||||||
|
return AssignmentKind.FISH
|
||||||
|
if not _slots[slot_index].is_empty():
|
||||||
|
return AssignmentKind.ITEM
|
||||||
|
return AssignmentKind.EMPTY
|
||||||
|
|
||||||
|
|
||||||
func get_slots() -> Array[StringName]:
|
func get_slots() -> Array[StringName]:
|
||||||
return _slots.duplicate()
|
return _slots.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func get_fish_slots() -> Array[StringName]:
|
||||||
|
return _fish_slots.duplicate()
|
||||||
|
|
||||||
|
|
||||||
func replace_state(
|
func replace_state(
|
||||||
slots: Array[StringName],
|
slots: Array[StringName],
|
||||||
selected_slot: int,
|
selected_slot: int,
|
||||||
|
fish_slots: Array[StringName] = [],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
var normalized: Array[StringName] = []
|
var normalized: Array[StringName] = []
|
||||||
normalized.resize(SLOT_COUNT)
|
normalized.resize(SLOT_COUNT)
|
||||||
normalized.fill(StringName())
|
normalized.fill(StringName())
|
||||||
|
var normalized_fish: Array[StringName] = []
|
||||||
|
normalized_fish.resize(SLOT_COUNT)
|
||||||
|
normalized_fish.fill(StringName())
|
||||||
var seen_assignments: Dictionary[StringName, bool] = {}
|
var seen_assignments: Dictionary[StringName, bool] = {}
|
||||||
for index: int in range(mini(slots.size(), SLOT_COUNT)):
|
for index: int in range(mini(slots.size(), SLOT_COUNT)):
|
||||||
var item_id: StringName = slots[index]
|
var item_id: StringName = slots[index]
|
||||||
|
|
@ -123,19 +217,28 @@ func replace_state(
|
||||||
if _can_assign(item_id) and not seen_assignments.has(item_id):
|
if _can_assign(item_id) and not seen_assignments.has(item_id):
|
||||||
normalized[index] = item_id
|
normalized[index] = item_id
|
||||||
seen_assignments[item_id] = true
|
seen_assignments[item_id] = true
|
||||||
|
var seen_fish: Dictionary[StringName, bool] = {}
|
||||||
|
for index: int in range(mini(fish_slots.size(), SLOT_COUNT)):
|
||||||
|
if not normalized[index].is_empty():
|
||||||
|
continue
|
||||||
|
var catch_id: StringName = fish_slots[index]
|
||||||
|
if _can_assign_fish(catch_id) and not seen_fish.has(catch_id):
|
||||||
|
normalized_fish[index] = catch_id
|
||||||
|
seen_fish[catch_id] = true
|
||||||
_slots = normalized
|
_slots = normalized
|
||||||
|
_fish_slots = normalized_fish
|
||||||
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
|
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
selected_slot_changed.emit(
|
_emit_selected_assignment()
|
||||||
_selected_slot,
|
|
||||||
_slots[_selected_slot]
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
func has_any_assignment() -> bool:
|
func has_any_assignment() -> bool:
|
||||||
for item_id: StringName in _slots:
|
for index: int in range(SLOT_COUNT):
|
||||||
if not item_id.is_empty():
|
if (
|
||||||
|
not _slots[index].is_empty()
|
||||||
|
or not _fish_slots[index].is_empty()
|
||||||
|
):
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
@ -149,23 +252,49 @@ func _can_assign(item_id: StringName) -> bool:
|
||||||
return item != null and item.is_valid() and item.hotbar_allowed
|
return item != null and item.is_valid() and item.hotbar_allowed
|
||||||
|
|
||||||
|
|
||||||
|
func _can_assign_fish(catch_id: StringName) -> bool:
|
||||||
|
return (
|
||||||
|
not catch_id.is_empty()
|
||||||
|
and _fish_inventory != null
|
||||||
|
and _fish_inventory.contains_catch_id(catch_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _validate_assignments() -> void:
|
func _validate_assignments() -> void:
|
||||||
var changed: bool = false
|
var changed: bool = false
|
||||||
for index: int in range(SLOT_COUNT):
|
for index: int in range(SLOT_COUNT):
|
||||||
if not _slots[index].is_empty() and not _can_assign(_slots[index]):
|
if not _slots[index].is_empty() and not _can_assign(_slots[index]):
|
||||||
_slots[index] = StringName()
|
_slots[index] = StringName()
|
||||||
changed = true
|
changed = true
|
||||||
|
if (
|
||||||
|
not _fish_slots[index].is_empty()
|
||||||
|
and not _can_assign_fish(_fish_slots[index])
|
||||||
|
):
|
||||||
|
_fish_slots[index] = StringName()
|
||||||
|
changed = true
|
||||||
if changed:
|
if changed:
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
selected_slot_changed.emit(
|
_emit_selected_assignment()
|
||||||
_selected_slot,
|
|
||||||
_slots[_selected_slot]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_bag_contents_changed() -> void:
|
func _on_bag_contents_changed() -> void:
|
||||||
_validate_assignments()
|
_validate_assignments()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_fish_inventory_changed() -> void:
|
||||||
|
_validate_assignments()
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_selected_assignment() -> void:
|
||||||
|
var kind: AssignmentKind = get_selected_assignment_kind()
|
||||||
|
var identity: StringName = StringName()
|
||||||
|
if kind == AssignmentKind.ITEM:
|
||||||
|
identity = _slots[_selected_slot]
|
||||||
|
elif kind == AssignmentKind.FISH:
|
||||||
|
identity = _fish_slots[_selected_slot]
|
||||||
|
selected_slot_changed.emit(_selected_slot, _slots[_selected_slot])
|
||||||
|
selected_assignment_changed.emit(_selected_slot, kind, identity)
|
||||||
|
|
||||||
|
|
||||||
func _is_slot_valid(slot_index: int) -> bool:
|
func _is_slot_valid(slot_index: int) -> bool:
|
||||||
return slot_index >= 0 and slot_index < SLOT_COUNT
|
return slot_index >= 0 and slot_index < SLOT_COUNT
|
||||||
|
|
|
||||||
18
main/main.gd
18
main/main.gd
|
|
@ -55,6 +55,9 @@ const NetworkShopServiceType = preload(
|
||||||
const NetworkItemUseServiceType = preload(
|
const NetworkItemUseServiceType = preload(
|
||||||
"res://network/network_item_use_service.gd"
|
"res://network/network_item_use_service.gd"
|
||||||
)
|
)
|
||||||
|
const NetworkFishShowcaseServiceType = preload(
|
||||||
|
"res://network/network_fish_showcase_service.gd"
|
||||||
|
)
|
||||||
const NetworkChatServiceType = preload(
|
const NetworkChatServiceType = preload(
|
||||||
"res://network/network_chat_service.gd"
|
"res://network/network_chat_service.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -124,6 +127,9 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
||||||
@onready var _network_item_use: NetworkItemUseServiceType = (
|
@onready var _network_item_use: NetworkItemUseServiceType = (
|
||||||
%NetworkItemUseService
|
%NetworkItemUseService
|
||||||
)
|
)
|
||||||
|
@onready var _network_fish_showcase: NetworkFishShowcaseServiceType = (
|
||||||
|
%NetworkFishShowcaseService
|
||||||
|
)
|
||||||
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
|
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
|
||||||
@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
|
@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
|
||||||
@onready var _network_player_list: NetworkPlayerListService = %NetworkPlayerListService
|
@onready var _network_player_list: NetworkPlayerListService = %NetworkPlayerListService
|
||||||
|
|
@ -295,7 +301,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_asset_reservations
|
_asset_reservations
|
||||||
)
|
)
|
||||||
_player.bag.setup(item_catalog)
|
_player.bag.setup(item_catalog)
|
||||||
_player.hotbar.setup(_player.bag, item_catalog)
|
_player.hotbar.setup(_player.bag, item_catalog, _player.inventory)
|
||||||
_shop_interaction = _test_world.get_fishing_shop()
|
_shop_interaction = _test_world.get_fishing_shop()
|
||||||
_shop_interaction.setup_local_player(_player)
|
_shop_interaction.setup_local_player(_player)
|
||||||
_shop_interaction.local_player_range_changed.connect(
|
_shop_interaction.local_player_range_changed.connect(
|
||||||
|
|
@ -338,6 +344,13 @@ func _initialize_after_data_root() -> void:
|
||||||
_save_manager,
|
_save_manager,
|
||||||
_asset_reservations
|
_asset_reservations
|
||||||
)
|
)
|
||||||
|
_network_fish_showcase.setup(
|
||||||
|
_network_session,
|
||||||
|
_player_spawn_service,
|
||||||
|
fish_catalog,
|
||||||
|
_player.inventory,
|
||||||
|
_player.hotbar,
|
||||||
|
)
|
||||||
_network_chat.setup(_network_session)
|
_network_chat.setup(_network_session)
|
||||||
_network_fishing.setup(
|
_network_fishing.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
|
|
@ -521,6 +534,9 @@ func _initialize_after_data_root() -> void:
|
||||||
_fishing_spot.ready_for_equipment_refresh.connect(
|
_fishing_spot.ready_for_equipment_refresh.connect(
|
||||||
_refresh_active_hotbar_item
|
_refresh_active_hotbar_item
|
||||||
)
|
)
|
||||||
|
_fishing_spot.fish_showcase_toggle_requested.connect(
|
||||||
|
_network_fish_showcase.toggle_selected_fish
|
||||||
|
)
|
||||||
_water_recovery.recovery_starting.connect(
|
_water_recovery.recovery_starting.connect(
|
||||||
_on_water_recovery_starting
|
_on_water_recovery_starting
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
[ext_resource type="Shader" uid="uid://byw3iub66qvog" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"]
|
[ext_resource type="Shader" uid="uid://byw3iub66qvog" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"]
|
||||||
[ext_resource type="Texture2D" uid="uid://b6xws1d2dnbnn" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"]
|
[ext_resource type="Texture2D" uid="uid://b6xws1d2dnbnn" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"]
|
||||||
[ext_resource type="PackedScene" uid="uid://w7n4gjqq1juc" path="res://art/exported/characters/base/netfishing_base_character.glb" id="44_4pcu1"]
|
[ext_resource type="PackedScene" uid="uid://w7n4gjqq1juc" path="res://art/exported/characters/base/netfishing_base_character.glb" id="44_4pcu1"]
|
||||||
|
[ext_resource type="Script" path="res://network/network_fish_showcase_service.gd" id="45_fish_showcase"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||||
shader = ExtResource("40_title_water")
|
shader = ExtResource("40_title_water")
|
||||||
|
|
@ -183,6 +184,10 @@ script = ExtResource("23_network_shop")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("24_network_item")
|
script = ExtResource("24_network_item")
|
||||||
|
|
||||||
|
[node name="NetworkFishShowcaseService" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("45_fish_showcase")
|
||||||
|
|
||||||
[node name="NetworkChatService" type="Node" parent="." unique_id=1956959711]
|
[node name="NetworkChatService" type="Node" parent="." unique_id=1956959711]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("25_network_chat")
|
script = ExtResource("25_network_chat")
|
||||||
|
|
|
||||||
56
network/network_fish_showcase_protocol.gd
Normal file
56
network/network_fish_showcase_protocol.gd
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
class_name NetworkFishShowcaseProtocol
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const CAPABILITY: StringName = &"fish_showcase_v1"
|
||||||
|
const RELIABLE_CHANNEL: int = NetworkProtocol.ITEM_RELIABLE_CHANNEL
|
||||||
|
const MAX_SESSION_ID_LENGTH: int = 96
|
||||||
|
const MAX_FISH_ID_LENGTH: int = 96
|
||||||
|
const MAX_WEIGHT_LB: float = 1000.0
|
||||||
|
const MAX_DISPLAY_SCALE: float = 20.0
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_state(data: Variant) -> bool:
|
||||||
|
if typeof(data) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var value: Dictionary = data
|
||||||
|
for key: String in [
|
||||||
|
"session_id",
|
||||||
|
"owner_peer_id",
|
||||||
|
"visible",
|
||||||
|
"fish_id",
|
||||||
|
"weight_lb",
|
||||||
|
"display_scale",
|
||||||
|
"revision",
|
||||||
|
]:
|
||||||
|
if not value.has(key):
|
||||||
|
return false
|
||||||
|
if (
|
||||||
|
typeof(value["session_id"]) != TYPE_STRING
|
||||||
|
or str(value["session_id"]).is_empty()
|
||||||
|
or str(value["session_id"]).length() > MAX_SESSION_ID_LENGTH
|
||||||
|
or typeof(value["owner_peer_id"]) != TYPE_INT
|
||||||
|
or int(value["owner_peer_id"]) <= 0
|
||||||
|
or typeof(value["visible"]) != TYPE_BOOL
|
||||||
|
or typeof(value["fish_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||||
|
or str(value["fish_id"]).length() > MAX_FISH_ID_LENGTH
|
||||||
|
or typeof(value["weight_lb"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or not is_finite(float(value["weight_lb"]))
|
||||||
|
or typeof(value["display_scale"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or not is_finite(float(value["display_scale"]))
|
||||||
|
or typeof(value["revision"]) != TYPE_INT
|
||||||
|
or int(value["revision"]) < 0
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
if bool(value["visible"]):
|
||||||
|
return (
|
||||||
|
not str(value["fish_id"]).is_empty()
|
||||||
|
and float(value["weight_lb"]) > 0.0
|
||||||
|
and float(value["weight_lb"]) <= MAX_WEIGHT_LB
|
||||||
|
and float(value["display_scale"]) > 0.0
|
||||||
|
and float(value["display_scale"]) <= MAX_DISPLAY_SCALE
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
str(value["fish_id"]).is_empty()
|
||||||
|
and is_zero_approx(float(value["weight_lb"]))
|
||||||
|
and is_zero_approx(float(value["display_scale"]))
|
||||||
|
)
|
||||||
1
network/network_fish_showcase_protocol.gd.uid
Normal file
1
network/network_fish_showcase_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b3h0h47ilh3nm
|
||||||
253
network/network_fish_showcase_service.gd
Normal file
253
network/network_fish_showcase_service.gd
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
class_name NetworkFishShowcaseService
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
|
||||||
|
var _session: NetworkSession
|
||||||
|
var _spawn_service: PlayerSpawnService
|
||||||
|
var _fish_catalog: FishPoolType
|
||||||
|
var _local_inventory: FishInventoryType
|
||||||
|
var _local_hotbar: PlayerHotbarType
|
||||||
|
var _states: Dictionary[int, Dictionary] = {}
|
||||||
|
var _local_revision: int = 0
|
||||||
|
var _local_visible: bool = false
|
||||||
|
var _local_catch_id: StringName
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
session: NetworkSession,
|
||||||
|
spawn_service: PlayerSpawnService,
|
||||||
|
fish_catalog: FishPoolType,
|
||||||
|
local_inventory: FishInventoryType,
|
||||||
|
local_hotbar: PlayerHotbarType,
|
||||||
|
) -> void:
|
||||||
|
_session = session
|
||||||
|
_spawn_service = spawn_service
|
||||||
|
_fish_catalog = fish_catalog
|
||||||
|
_local_inventory = local_inventory
|
||||||
|
_local_hotbar = local_hotbar
|
||||||
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||||
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
|
_spawn_service.avatar_spawned.connect(_on_avatar_spawned)
|
||||||
|
_local_inventory.catches_changed.connect(_on_local_inventory_changed)
|
||||||
|
_local_hotbar.selected_assignment_changed.connect(
|
||||||
|
_on_selected_assignment_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func toggle_selected_fish() -> bool:
|
||||||
|
if _local_hotbar == null or _local_inventory == null:
|
||||||
|
return false
|
||||||
|
var catch_id: StringName = _local_hotbar.get_selected_fish_catch_id()
|
||||||
|
var fish_catch: FishCatchType = _local_inventory.get_catch_by_id(catch_id)
|
||||||
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
|
return false
|
||||||
|
if _local_visible and _local_catch_id == catch_id:
|
||||||
|
_submit_local_state(null, false)
|
||||||
|
else:
|
||||||
|
_submit_local_state(fish_catch, true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func is_local_showcase_visible() -> bool:
|
||||||
|
return _local_visible
|
||||||
|
|
||||||
|
|
||||||
|
func get_local_showcase_catch_id() -> StringName:
|
||||||
|
return _local_catch_id
|
||||||
|
|
||||||
|
|
||||||
|
func _submit_local_state(fish_catch: FishCatchType, should_show: bool) -> void:
|
||||||
|
_local_revision += 1
|
||||||
|
var local_peer_id: int = (
|
||||||
|
_session.get_local_peer_id() if _session != null else 1
|
||||||
|
)
|
||||||
|
var data: Dictionary = {
|
||||||
|
"session_id": (
|
||||||
|
_session.get_session_id()
|
||||||
|
if _session != null and not _session.get_session_id().is_empty()
|
||||||
|
else "local"
|
||||||
|
),
|
||||||
|
"owner_peer_id": local_peer_id,
|
||||||
|
"visible": should_show and fish_catch != null,
|
||||||
|
"fish_id": String(fish_catch.fish_id) if fish_catch != null else "",
|
||||||
|
"weight_lb": fish_catch.weight_lb if fish_catch != null else 0.0,
|
||||||
|
"display_scale": (
|
||||||
|
fish_catch.display_scale if fish_catch != null else 0.0
|
||||||
|
),
|
||||||
|
"revision": _local_revision,
|
||||||
|
}
|
||||||
|
_local_visible = bool(data["visible"])
|
||||||
|
_local_catch_id = (
|
||||||
|
fish_catch.catch_id if _local_visible else StringName()
|
||||||
|
)
|
||||||
|
_apply_state(data)
|
||||||
|
if _session == null or not _session.is_gameplay_session_active():
|
||||||
|
return
|
||||||
|
data["session_id"] = _session.get_session_id()
|
||||||
|
if _session.is_host():
|
||||||
|
_handle_showcase_state(local_peer_id, data)
|
||||||
|
elif _session.supports_server_capability(
|
||||||
|
NetworkFishShowcaseProtocol.CAPABILITY
|
||||||
|
):
|
||||||
|
submit_showcase_state.rpc_id(1, data)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc(
|
||||||
|
"any_peer",
|
||||||
|
"call_remote",
|
||||||
|
"reliable",
|
||||||
|
NetworkFishShowcaseProtocol.RELIABLE_CHANNEL,
|
||||||
|
)
|
||||||
|
func submit_showcase_state(data: Dictionary) -> void:
|
||||||
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||||
|
if _session.is_host() and _session.is_authenticated_peer(sender_id):
|
||||||
|
_handle_showcase_state(sender_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_showcase_state(peer_id: int, data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not _session.is_host()
|
||||||
|
or not NetworkFishShowcaseProtocol.validate_state(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
or int(data["owner_peer_id"]) != peer_id
|
||||||
|
or (
|
||||||
|
peer_id != _session.get_local_peer_id()
|
||||||
|
and not _session.is_authenticated_peer(peer_id)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var previous: Dictionary = _states.get(peer_id, {})
|
||||||
|
if int(data["revision"]) <= int(previous.get("revision", -1)):
|
||||||
|
return
|
||||||
|
if not _state_matches_catalog(data):
|
||||||
|
return
|
||||||
|
var sanitized: Dictionary = data.duplicate(true)
|
||||||
|
_states[peer_id] = sanitized
|
||||||
|
_apply_state(sanitized)
|
||||||
|
receive_showcase_state.rpc(sanitized)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc(
|
||||||
|
"authority",
|
||||||
|
"call_remote",
|
||||||
|
"reliable",
|
||||||
|
NetworkFishShowcaseProtocol.RELIABLE_CHANNEL,
|
||||||
|
)
|
||||||
|
func receive_showcase_state(data: Dictionary) -> void:
|
||||||
|
if (
|
||||||
|
not NetworkFishShowcaseProtocol.validate_state(data)
|
||||||
|
or str(data["session_id"]) != _session.get_session_id()
|
||||||
|
or not _state_matches_catalog(data)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var peer_id: int = int(data["owner_peer_id"])
|
||||||
|
var previous: Dictionary = _states.get(peer_id, {})
|
||||||
|
if int(data["revision"]) <= int(previous.get("revision", -1)):
|
||||||
|
return
|
||||||
|
_states[peer_id] = data.duplicate(true)
|
||||||
|
_apply_state(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _state_matches_catalog(data: Dictionary) -> bool:
|
||||||
|
if not bool(data["visible"]):
|
||||||
|
return true
|
||||||
|
var fish_id: StringName = StringName(str(data["fish_id"]))
|
||||||
|
var fish: FishDataType = _fish_catalog.get_fish_by_id(fish_id)
|
||||||
|
if fish == null or not fish.is_selectable():
|
||||||
|
return false
|
||||||
|
var weight_lb: float = float(data["weight_lb"])
|
||||||
|
var display_scale: float = float(data["display_scale"])
|
||||||
|
return (
|
||||||
|
weight_lb >= fish.get_minimum_weight()
|
||||||
|
and weight_lb <= fish.get_maximum_weight()
|
||||||
|
and is_equal_approx(
|
||||||
|
display_scale,
|
||||||
|
fish.get_display_scale_for_weight(weight_lb),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_state(data: Dictionary) -> void:
|
||||||
|
var peer_id: int = int(data["owner_peer_id"])
|
||||||
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
if avatar == null:
|
||||||
|
return
|
||||||
|
if not bool(data["visible"]):
|
||||||
|
avatar.set_held_fish(null, 1.0, false)
|
||||||
|
return
|
||||||
|
var fish: FishDataType = _fish_catalog.get_fish_by_id(
|
||||||
|
StringName(str(data["fish_id"]))
|
||||||
|
)
|
||||||
|
avatar.set_held_fish(fish, float(data["display_scale"]), true)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_selected_assignment_changed(
|
||||||
|
_slot_index: int,
|
||||||
|
kind: int,
|
||||||
|
identity: StringName,
|
||||||
|
) -> void:
|
||||||
|
if (
|
||||||
|
_local_visible
|
||||||
|
and (
|
||||||
|
kind != PlayerHotbarType.AssignmentKind.FISH
|
||||||
|
or identity != _local_catch_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_submit_local_state(null, false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_local_inventory_changed() -> void:
|
||||||
|
if (
|
||||||
|
_local_visible
|
||||||
|
and not _local_inventory.contains_catch_id(_local_catch_id)
|
||||||
|
):
|
||||||
|
_submit_local_state(null, false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||||
|
if not _session.is_host():
|
||||||
|
return
|
||||||
|
for state: Dictionary in _states.values():
|
||||||
|
if bool(state.get("visible", false)):
|
||||||
|
receive_showcase_state.rpc_id(peer_id, state)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_avatar_spawned(peer_id: int, _avatar: Player) -> void:
|
||||||
|
var state: Dictionary = _states.get(peer_id, {})
|
||||||
|
if not state.is_empty():
|
||||||
|
_apply_state(state)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_removed(peer_id: int) -> void:
|
||||||
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
if avatar != null:
|
||||||
|
avatar.set_held_fish(null, 1.0, false)
|
||||||
|
_states.erase(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
|
if state not in [
|
||||||
|
NetworkSession.State.INACTIVE,
|
||||||
|
NetworkSession.State.DISCONNECTING,
|
||||||
|
NetworkSession.State.CONNECTION_FAILED,
|
||||||
|
NetworkSession.State.SERVER_LOST,
|
||||||
|
]:
|
||||||
|
return
|
||||||
|
for peer_id: int in _states:
|
||||||
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
if avatar != null:
|
||||||
|
avatar.set_held_fish(null, 1.0, false)
|
||||||
|
_states.clear()
|
||||||
|
_local_visible = false
|
||||||
|
_local_catch_id = StringName()
|
||||||
|
var local_avatar: Player = _spawn_service.get_avatar(
|
||||||
|
_session.get_local_peer_id()
|
||||||
|
)
|
||||||
|
if local_avatar != null:
|
||||||
|
local_avatar.set_held_fish(null, 1.0, false)
|
||||||
1
network/network_fish_showcase_service.gd.uid
Normal file
1
network/network_fish_showcase_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://m4n0bn38f34i
|
||||||
|
|
@ -10,7 +10,7 @@ const MAX_PUBLIC_KEY_LENGTH: int = 8192
|
||||||
const MAX_SIGNATURE_LENGTH: int = 2048
|
const MAX_SIGNATURE_LENGTH: int = 2048
|
||||||
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
|
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
|
||||||
# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
|
# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
|
||||||
# 6 reliable shop transactions, 7 reliable item/equipment lifecycle,
|
# 6 reliable shop transactions, 7 reliable item/equipment/showcase lifecycle,
|
||||||
# 8 reliable ordered session chat, 9 reliable private session mail.
|
# 8 reliable ordered session chat, 9 reliable private session mail.
|
||||||
const SALE_RELIABLE_CHANNEL: int = 5
|
const SALE_RELIABLE_CHANNEL: int = 5
|
||||||
const SHOP_RELIABLE_CHANNEL: int = 6
|
const SHOP_RELIABLE_CHANNEL: int = 6
|
||||||
|
|
@ -187,6 +187,7 @@ static func make_server_hello(
|
||||||
"shop_v1",
|
"shop_v1",
|
||||||
"item_use_v1",
|
"item_use_v1",
|
||||||
"equipment_v1",
|
"equipment_v1",
|
||||||
|
"fish_showcase_v1",
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,8 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
if is_host():
|
if is_host():
|
||||||
return str(capability) in PackedStringArray([
|
return str(capability) in PackedStringArray([
|
||||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||||
"item_use_v1", "equipment_v1", "chat_v1",
|
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||||
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
"identity_v1",
|
"identity_v1",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extends CharacterBody3D
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
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 FishDataType = preload("res://fish/fish_data.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 PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
|
|
@ -106,6 +107,8 @@ class ShowcaseCameraSnapshot:
|
||||||
@onready var _fishing_rod_tip: Marker3D = %FishingRodTip
|
@onready var _fishing_rod_tip: Marker3D = %FishingRodTip
|
||||||
@onready var _catch_display: Node3D = %CatchDisplay
|
@onready var _catch_display: Node3D = %CatchDisplay
|
||||||
@onready var _catch_sprite: Sprite3D = %CatchSprite
|
@onready var _catch_sprite: Sprite3D = %CatchSprite
|
||||||
|
@onready var _held_fish_display: Node3D = %HeldFishDisplay
|
||||||
|
@onready var _held_fish_sprite: Sprite3D = %HeldFishSprite
|
||||||
|
|
||||||
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity"))
|
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity"))
|
||||||
var _camera_dragging: bool = false
|
var _camera_dragging: bool = false
|
||||||
|
|
@ -696,6 +699,25 @@ func set_active_item_is_rod(active_is_rod: bool) -> void:
|
||||||
_fishing_rod.visible = active_is_rod
|
_fishing_rod.visible = active_is_rod
|
||||||
|
|
||||||
|
|
||||||
|
func set_held_fish(
|
||||||
|
fish: FishDataType,
|
||||||
|
display_scale: float,
|
||||||
|
should_show: bool,
|
||||||
|
) -> void:
|
||||||
|
if not should_show or fish == null or fish.display_texture == null:
|
||||||
|
_held_fish_display.visible = false
|
||||||
|
_held_fish_display.scale = Vector3.ONE
|
||||||
|
_held_fish_sprite.texture = null
|
||||||
|
return
|
||||||
|
_held_fish_sprite.texture = fish.display_texture
|
||||||
|
_held_fish_display.scale = (
|
||||||
|
Vector3.ONE
|
||||||
|
* maxf(display_scale, 0.01)
|
||||||
|
* catch_presentation_base_scale
|
||||||
|
)
|
||||||
|
_held_fish_display.visible = true
|
||||||
|
|
||||||
|
|
||||||
func get_cast_origin_position() -> Vector3:
|
func get_cast_origin_position() -> Vector3:
|
||||||
return _cast_origin.global_position
|
return _cast_origin.global_position
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,21 @@ shaded = false
|
||||||
double_sided = true
|
double_sided = true
|
||||||
texture_filter = 0
|
texture_filter = 0
|
||||||
|
|
||||||
|
[node name="HeldFishAnchor" type="Marker3D" parent="Visuals"]
|
||||||
|
position = Vector3(0, 1.15, -0.9)
|
||||||
|
|
||||||
|
[node name="HeldFishDisplay" type="Node3D" parent="Visuals/HeldFishAnchor"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="HeldFishSprite" type="Sprite3D" parent="Visuals/HeldFishAnchor/HeldFishDisplay"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
billboard = 1
|
||||||
|
shaded = false
|
||||||
|
double_sided = true
|
||||||
|
texture_filter = 0
|
||||||
|
|
||||||
[node name="CameraYaw" type="Node3D" parent="."]
|
[node name="CameraYaw" type="Node3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
position = Vector3(0, 1.35, 0)
|
position = Vector3(0, 1.35, 0)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class LoadSnapshot:
|
||||||
var next_catch_sequence: int = 1
|
var next_catch_sequence: int = 1
|
||||||
var bag_items: Array[OwnedItemType] = []
|
var bag_items: Array[OwnedItemType] = []
|
||||||
var hotbar_slots: Array[StringName] = []
|
var hotbar_slots: Array[StringName] = []
|
||||||
|
var fish_hotbar_slots: Array[StringName] = []
|
||||||
var selected_hotbar_slot: int = 0
|
var selected_hotbar_slot: int = 0
|
||||||
var reel_speed_level: int = 0
|
var reel_speed_level: int = 0
|
||||||
var barrier_power_level: int = 0
|
var barrier_power_level: int = 0
|
||||||
|
|
@ -205,7 +206,8 @@ func load_player_data() -> bool:
|
||||||
var bag_restored: bool = _bag.replace_all_items(snapshot.bag_items)
|
var bag_restored: bool = _bag.replace_all_items(snapshot.bag_items)
|
||||||
var hotbar_restored: bool = _hotbar.replace_state(
|
var hotbar_restored: bool = _hotbar.replace_state(
|
||||||
snapshot.hotbar_slots,
|
snapshot.hotbar_slots,
|
||||||
snapshot.selected_hotbar_slot
|
snapshot.selected_hotbar_slot,
|
||||||
|
snapshot.fish_hotbar_slots,
|
||||||
)
|
)
|
||||||
var upgrades_restored: bool = _fishing_upgrades.restore_levels(
|
var upgrades_restored: bool = _fishing_upgrades.restore_levels(
|
||||||
snapshot.reel_speed_level,
|
snapshot.reel_speed_level,
|
||||||
|
|
@ -393,6 +395,9 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
var serialized_slots: Array[String] = []
|
var serialized_slots: Array[String] = []
|
||||||
for item_id: StringName in _hotbar.get_slots():
|
for item_id: StringName in _hotbar.get_slots():
|
||||||
serialized_slots.append(String(item_id))
|
serialized_slots.append(String(item_id))
|
||||||
|
var serialized_fish_slots: Array[String] = []
|
||||||
|
for catch_id: StringName in _hotbar.get_fish_slots():
|
||||||
|
serialized_fish_slots.append(String(catch_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
|
||||||
|
|
@ -422,6 +427,7 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
"hotbar": {
|
"hotbar": {
|
||||||
"selected_slot": _hotbar.get_selected_slot(),
|
"selected_slot": _hotbar.get_selected_slot(),
|
||||||
"slots": serialized_slots,
|
"slots": serialized_slots,
|
||||||
|
"fish_slots": serialized_fish_slots,
|
||||||
},
|
},
|
||||||
"upgrades": _fishing_upgrades.to_save_data(),
|
"upgrades": _fishing_upgrades.to_save_data(),
|
||||||
"cooler": _cooler_capacity.to_save_data(),
|
"cooler": _cooler_capacity.to_save_data(),
|
||||||
|
|
@ -610,6 +616,22 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
and seen_items.has(slot_item_id)
|
and seen_items.has(slot_item_id)
|
||||||
):
|
):
|
||||||
snapshot.hotbar_slots[slot_index] = slot_item_id
|
snapshot.hotbar_slots[slot_index] = slot_item_id
|
||||||
|
var fish_slot_values: Array = []
|
||||||
|
if typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY:
|
||||||
|
fish_slot_values = hotbar_data["fish_slots"]
|
||||||
|
snapshot.fish_hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
|
||||||
|
snapshot.fish_hotbar_slots.fill(StringName())
|
||||||
|
for slot_index: int in range(
|
||||||
|
mini(fish_slot_values.size(), PlayerHotbarType.SLOT_COUNT)
|
||||||
|
):
|
||||||
|
if not snapshot.hotbar_slots[slot_index].is_empty():
|
||||||
|
continue
|
||||||
|
var slot_value: Variant = fish_slot_values[slot_index]
|
||||||
|
if typeof(slot_value) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
continue
|
||||||
|
var catch_id: StringName = StringName(str(slot_value))
|
||||||
|
if not catch_id.is_empty() and seen_ids.has(catch_id):
|
||||||
|
snapshot.fish_hotbar_slots[slot_index] = catch_id
|
||||||
var selected_slot: int = _read_integer(
|
var selected_slot: int = _read_integer(
|
||||||
hotbar_data["selected_slot"],
|
hotbar_data["selected_slot"],
|
||||||
0,
|
0,
|
||||||
|
|
|
||||||
138
tests/fish_hotbar_showcase_validation.gd
Normal file
138
tests/fish_hotbar_showcase_validation.gd
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene = preload("res://main/main.tscn")
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main := MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(session.start_private_host(17977))
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var fish_catalog := main.get("fish_catalog") as FishPool
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkFishShowcaseService"
|
||||||
|
) as NetworkFishShowcaseService
|
||||||
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
|
assert(player != null and fish_catalog != null and service != null)
|
||||||
|
var fish: FishDataType = fish_catalog.get_fish_by_id(&"bluegill")
|
||||||
|
assert(fish != null)
|
||||||
|
var fish_catch := FishCatchType.new()
|
||||||
|
fish_catch.fish = fish
|
||||||
|
fish_catch.fish_id = fish.id
|
||||||
|
fish_catch.weight_lb = fish.get_minimum_weight()
|
||||||
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.sale_value = fish.get_sale_value_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.ensure_identity()
|
||||||
|
player.inventory.add_catch(fish_catch)
|
||||||
|
var game_ui := main.get_node("%GameUI") as GameUI
|
||||||
|
var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
|
||||||
|
assert(hotbar_ui != null)
|
||||||
|
hotbar_ui.set_drag_enabled(true)
|
||||||
|
var slots: Array = hotbar_ui.get("_slots")
|
||||||
|
var fish_payload: Dictionary = {
|
||||||
|
"kind": "cooler_fish",
|
||||||
|
"catch_id": String(fish_catch.catch_id),
|
||||||
|
}
|
||||||
|
assert(slots.size() == PlayerHotbar.SLOT_COUNT)
|
||||||
|
assert(bool(slots[1].call("_can_drop_data", Vector2.ZERO, fish_payload)))
|
||||||
|
slots[1].call("_drop_data", Vector2.ZERO, fish_payload)
|
||||||
|
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||||
|
assert(player.hotbar.select_slot(1))
|
||||||
|
assert(
|
||||||
|
player.hotbar.get_selected_assignment_kind()
|
||||||
|
== PlayerHotbar.AssignmentKind.FISH
|
||||||
|
)
|
||||||
|
assert(player.hotbar.get_selected_fish_catch_id() == fish_catch.catch_id)
|
||||||
|
assert(player.hotbar.get_selected_item_id().is_empty())
|
||||||
|
|
||||||
|
var press := InputEventMouseButton.new()
|
||||||
|
press.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
press.pressed = true
|
||||||
|
fishing_spot.call("_unhandled_input", press)
|
||||||
|
await process_frame
|
||||||
|
assert(service.is_local_showcase_visible())
|
||||||
|
assert(service.get_local_showcase_catch_id() == fish_catch.catch_id)
|
||||||
|
assert((player.get_node("%HeldFishDisplay") as Node3D).visible)
|
||||||
|
fishing_spot.call("_unhandled_input", press)
|
||||||
|
await process_frame
|
||||||
|
assert(not service.is_local_showcase_visible())
|
||||||
|
assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible)
|
||||||
|
|
||||||
|
assert(save_manager.save_now())
|
||||||
|
var save_path: String = str(save_manager.get("_save_path"))
|
||||||
|
var save_file := FileAccess.open(save_path, FileAccess.READ)
|
||||||
|
assert(save_file != null)
|
||||||
|
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
|
||||||
|
save_file.close()
|
||||||
|
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||||
|
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||||
|
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||||
|
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||||
|
assert(int((parsed as Dictionary)["save_version"]) == 4)
|
||||||
|
|
||||||
|
assert(player.hotbar.clear_slot(1))
|
||||||
|
assert(save_manager.load_player_data())
|
||||||
|
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||||
|
assert(player.hotbar.get_selected_slot() == 1)
|
||||||
|
assert(not service.is_local_showcase_visible())
|
||||||
|
assert(service.toggle_selected_fish())
|
||||||
|
assert(service.is_local_showcase_visible())
|
||||||
|
assert(player.inventory.remove_catch_by_id(fish_catch.catch_id) != null)
|
||||||
|
await process_frame
|
||||||
|
assert(player.hotbar.get_fish_catch_id(1).is_empty())
|
||||||
|
assert(not service.is_local_showcase_visible())
|
||||||
|
assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible)
|
||||||
|
|
||||||
|
var valid_state: Dictionary = {
|
||||||
|
"session_id": "session",
|
||||||
|
"owner_peer_id": 2,
|
||||||
|
"visible": true,
|
||||||
|
"fish_id": String(fish.id),
|
||||||
|
"weight_lb": fish.get_minimum_weight(),
|
||||||
|
"display_scale": fish.get_display_scale_for_weight(
|
||||||
|
fish.get_minimum_weight()
|
||||||
|
),
|
||||||
|
"revision": 1,
|
||||||
|
}
|
||||||
|
assert(NetworkFishShowcaseProtocol.validate_state(valid_state))
|
||||||
|
var invalid_state: Dictionary = valid_state.duplicate(true)
|
||||||
|
invalid_state["display_scale"] = 1000.0
|
||||||
|
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||||
|
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
||||||
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
|
assert(
|
||||||
|
NetworkFishShowcaseProtocol.CAPABILITY
|
||||||
|
== &"fish_showcase_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Fish hotbar showcase validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
await process_frame
|
||||||
|
quit()
|
||||||
1
tests/fish_hotbar_showcase_validation.gd.uid
Normal file
1
tests/fish_hotbar_showcase_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cpnfrron1g65q
|
||||||
153
tests/fish_showcase_multiplayer_validation.gd
Normal file
153
tests/fish_showcase_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene = preload("res://main/main.tscn")
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const TEST_PORT: int = 17978
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("host"):
|
||||||
|
await _run_host()
|
||||||
|
return
|
||||||
|
if arguments.has("client"):
|
||||||
|
await _run_client()
|
||||||
|
return
|
||||||
|
push_error("Fish showcase multiplayer validation needs host or client mode.")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_host() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkFishShowcaseService"
|
||||||
|
) as NetworkFishShowcaseService
|
||||||
|
var fish_catch: FishCatch = _add_bluegill(main, player)
|
||||||
|
assert(player.hotbar.assign_fish(1, fish_catch.catch_id))
|
||||||
|
assert(player.hotbar.select_slot(1))
|
||||||
|
assert(service.toggle_selected_fish())
|
||||||
|
assert(service.is_local_showcase_visible())
|
||||||
|
assert(session.set_host_open(true))
|
||||||
|
|
||||||
|
var remote_peer_id: int = 0
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
|
||||||
|
await process_frame
|
||||||
|
for peer_id: int in session.get_authenticated_peer_ids():
|
||||||
|
if peer_id != session.get_local_peer_id():
|
||||||
|
remote_peer_id = peer_id
|
||||||
|
break
|
||||||
|
assert(remote_peer_id > 1)
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
|
assert(remote_avatar != null)
|
||||||
|
var remote_display := remote_avatar.get_node("%HeldFishDisplay") as Node3D
|
||||||
|
var visible_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < visible_deadline and not remote_display.visible:
|
||||||
|
await process_frame
|
||||||
|
assert(remote_display.visible)
|
||||||
|
var hidden_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < hidden_deadline and remote_display.visible:
|
||||||
|
await process_frame
|
||||||
|
assert(not remote_display.visible)
|
||||||
|
service.toggle_selected_fish()
|
||||||
|
print("Fish showcase multiplayer host validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
await process_frame
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
main.call(
|
||||||
|
"_on_title_join_game_requested",
|
||||||
|
"127.0.0.1:%d" % TEST_PORT,
|
||||||
|
)
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline:
|
||||||
|
await process_frame
|
||||||
|
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||||
|
main.call("_confirm_server_trust")
|
||||||
|
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||||
|
break
|
||||||
|
assert(session.is_joined_client())
|
||||||
|
assert(
|
||||||
|
session.supports_server_capability(
|
||||||
|
NetworkFishShowcaseProtocol.CAPABILITY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
var host_avatar: Player = spawn_service.get_avatar(1)
|
||||||
|
assert(host_avatar != null)
|
||||||
|
var host_display := host_avatar.get_node("%HeldFishDisplay") as Node3D
|
||||||
|
var snapshot_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < snapshot_deadline and not host_display.visible:
|
||||||
|
await process_frame
|
||||||
|
assert(host_display.visible)
|
||||||
|
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkFishShowcaseService"
|
||||||
|
) as NetworkFishShowcaseService
|
||||||
|
var fish_catch: FishCatch = _add_bluegill(main, player)
|
||||||
|
assert(player.hotbar.assign_fish(1, fish_catch.catch_id))
|
||||||
|
assert(player.hotbar.select_slot(1))
|
||||||
|
assert(service.toggle_selected_fish())
|
||||||
|
assert((player.get_node("%HeldFishDisplay") as Node3D).visible)
|
||||||
|
await create_timer(2.0).timeout
|
||||||
|
assert(service.toggle_selected_fish())
|
||||||
|
assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible)
|
||||||
|
await create_timer(1.0).timeout
|
||||||
|
print("Fish showcase multiplayer client validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
await process_frame
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _add_bluegill(main: Node, player: Player) -> FishCatch:
|
||||||
|
var catalog := main.get("fish_catalog") as FishPool
|
||||||
|
var fish: FishData = catalog.get_fish_by_id(&"bluegill")
|
||||||
|
var fish_catch := FishCatchType.new()
|
||||||
|
fish_catch.fish = fish
|
||||||
|
fish_catch.fish_id = fish.id
|
||||||
|
fish_catch.weight_lb = fish.get_minimum_weight()
|
||||||
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.sale_value = fish.get_sale_value_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.ensure_identity()
|
||||||
|
player.inventory.add_catch(fish_catch)
|
||||||
|
return fish_catch
|
||||||
|
|
||||||
|
|
||||||
|
func _create_initialized_main() -> Node:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main := MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
return main
|
||||||
1
tests/fish_showcase_multiplayer_validation.gd.uid
Normal file
1
tests/fish_showcase_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ckta4mcodq0hg
|
||||||
|
|
@ -10,6 +10,8 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const ItemDataType = preload("res://items/item_data.gd")
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
|
||||||
const SELECTED_SCALE: float = 1.12
|
const SELECTED_SCALE: float = 1.12
|
||||||
const HOVER_SCALE: float = 1.025
|
const HOVER_SCALE: float = 1.025
|
||||||
|
|
@ -41,6 +43,7 @@ const DEFORMATION_Y_AMPLITUDE: float = 0.007
|
||||||
var _hotbar: PlayerHotbarType
|
var _hotbar: PlayerHotbarType
|
||||||
var _bag: PlayerBagType
|
var _bag: PlayerBagType
|
||||||
var _catalog: ItemCatalogType
|
var _catalog: ItemCatalogType
|
||||||
|
var _fish_inventory: FishInventoryType
|
||||||
var _drag_enabled: bool = false
|
var _drag_enabled: bool = false
|
||||||
var _drag_in_progress: bool = false
|
var _drag_in_progress: bool = false
|
||||||
var _selected: bool = false
|
var _selected: bool = false
|
||||||
|
|
@ -69,10 +72,12 @@ func setup(
|
||||||
hotbar: PlayerHotbarType,
|
hotbar: PlayerHotbarType,
|
||||||
bag: PlayerBagType,
|
bag: PlayerBagType,
|
||||||
catalog: ItemCatalogType,
|
catalog: ItemCatalogType,
|
||||||
|
fish_inventory: FishInventoryType,
|
||||||
) -> void:
|
) -> void:
|
||||||
_hotbar = hotbar
|
_hotbar = hotbar
|
||||||
_bag = bag
|
_bag = bag
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
refresh()
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -144,12 +149,22 @@ func refresh() -> void:
|
||||||
if _hotbar == null:
|
if _hotbar == null:
|
||||||
return
|
return
|
||||||
var item_id: StringName = _hotbar.get_item_id(slot_index)
|
var item_id: StringName = _hotbar.get_item_id(slot_index)
|
||||||
|
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||||||
var item: ItemDataType = (
|
var item: ItemDataType = (
|
||||||
_catalog.get_item_by_id(item_id)
|
_catalog.get_item_by_id(item_id)
|
||||||
if _catalog != null and not item_id.is_empty()
|
if _catalog != null and not item_id.is_empty()
|
||||||
else null
|
else null
|
||||||
)
|
)
|
||||||
_item_icon.texture = item.icon if item != null else null
|
var fish_catch: FishCatchType = (
|
||||||
|
_fish_inventory.get_catch_by_id(catch_id)
|
||||||
|
if _fish_inventory != null and not catch_id.is_empty()
|
||||||
|
else null
|
||||||
|
)
|
||||||
|
_item_icon.texture = (
|
||||||
|
fish_catch.fish.display_texture
|
||||||
|
if fish_catch != null
|
||||||
|
else item.icon if item != null else null
|
||||||
|
)
|
||||||
var quantity: int = (
|
var quantity: int = (
|
||||||
_bag.get_quantity(item_id)
|
_bag.get_quantity(item_id)
|
||||||
if _bag != null and not item_id.is_empty()
|
if _bag != null and not item_id.is_empty()
|
||||||
|
|
@ -163,12 +178,17 @@ func refresh() -> void:
|
||||||
_quantity_label.text = quantity_text
|
_quantity_label.text = quantity_text
|
||||||
_quantity_label.visible = not quantity_text.is_empty()
|
_quantity_label.visible = not quantity_text.is_empty()
|
||||||
tooltip_text = (
|
tooltip_text = (
|
||||||
item.display_name if item != null else "empty hotbar slot"
|
"%s · %.1f lb" % [
|
||||||
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.weight_lb,
|
||||||
|
]
|
||||||
|
if fish_catch != null
|
||||||
|
else item.display_name if item != null else "empty hotbar slot"
|
||||||
)
|
)
|
||||||
var was_selected: bool = _selected
|
var was_selected: bool = _selected
|
||||||
var was_empty: bool = _empty
|
var was_empty: bool = _empty
|
||||||
_selected = slot_index == _hotbar.get_selected_slot()
|
_selected = slot_index == _hotbar.get_selected_slot()
|
||||||
_empty = item == null
|
_empty = item == null and fish_catch == null
|
||||||
if was_selected != _selected or was_empty != _empty:
|
if was_selected != _selected or was_empty != _empty:
|
||||||
_apply_style()
|
_apply_style()
|
||||||
|
|
||||||
|
|
@ -283,12 +303,22 @@ func _get_drag_data(_at_position: Vector2) -> Variant:
|
||||||
if not _drag_enabled or _hotbar == null:
|
if not _drag_enabled or _hotbar == null:
|
||||||
return null
|
return null
|
||||||
var item_id: StringName = _hotbar.get_item_id(slot_index)
|
var item_id: StringName = _hotbar.get_item_id(slot_index)
|
||||||
if item_id.is_empty():
|
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||||||
|
if item_id.is_empty() and catch_id.is_empty():
|
||||||
return null
|
return null
|
||||||
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
||||||
|
var fish_catch: FishCatchType = (
|
||||||
|
_fish_inventory.get_catch_by_id(catch_id)
|
||||||
|
if _fish_inventory != null and not catch_id.is_empty()
|
||||||
|
else null
|
||||||
|
)
|
||||||
var preview := TextureRect.new()
|
var preview := TextureRect.new()
|
||||||
preview.custom_minimum_size = Vector2(44.0, 44.0)
|
preview.custom_minimum_size = Vector2(44.0, 44.0)
|
||||||
preview.texture = item.icon if item != null else null
|
preview.texture = (
|
||||||
|
fish_catch.fish.display_texture
|
||||||
|
if fish_catch != null
|
||||||
|
else item.icon if item != null else null
|
||||||
|
)
|
||||||
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
|
@ -299,7 +329,6 @@ func _get_drag_data(_at_position: Vector2) -> Variant:
|
||||||
return {
|
return {
|
||||||
"kind": "hotbar_slot",
|
"kind": "hotbar_slot",
|
||||||
"slot_index": slot_index,
|
"slot_index": slot_index,
|
||||||
"item_id": String(item_id),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -315,13 +344,19 @@ func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||||
and source_index < PlayerHotbarType.SLOT_COUNT
|
and source_index < PlayerHotbarType.SLOT_COUNT
|
||||||
and source_index != slot_index
|
and source_index != slot_index
|
||||||
)
|
)
|
||||||
if kind != "bag_item":
|
if kind == "bag_item":
|
||||||
return false
|
var item_id: StringName = StringName(str(payload.get("item_id", "")))
|
||||||
var item_id: StringName = StringName(str(payload.get("item_id", "")))
|
if _bag == null or not _bag.owns_item(item_id) or _catalog == null:
|
||||||
if _bag == null or not _bag.owns_item(item_id) or _catalog == null:
|
return false
|
||||||
return false
|
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
||||||
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
return item != null and item.is_valid() and item.hotbar_allowed
|
||||||
return item != null and item.is_valid() and item.hotbar_allowed
|
if kind == "cooler_fish":
|
||||||
|
var catch_id: StringName = StringName(str(payload.get("catch_id", "")))
|
||||||
|
return (
|
||||||
|
_fish_inventory != null
|
||||||
|
and _fish_inventory.contains_catch_id(catch_id)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
||||||
|
|
@ -330,6 +365,11 @@ func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
||||||
var payload: Dictionary = data
|
var payload: Dictionary = data
|
||||||
if str(payload.get("kind", "")) == "hotbar_slot":
|
if str(payload.get("kind", "")) == "hotbar_slot":
|
||||||
_hotbar.swap_slots(int(payload["slot_index"]), slot_index)
|
_hotbar.swap_slots(int(payload["slot_index"]), slot_index)
|
||||||
|
elif str(payload.get("kind", "")) == "cooler_fish":
|
||||||
|
_hotbar.assign_fish(
|
||||||
|
slot_index,
|
||||||
|
StringName(str(payload["catch_id"])),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_hotbar.assign_item(
|
_hotbar.assign_item(
|
||||||
slot_index,
|
slot_index,
|
||||||
|
|
@ -346,7 +386,10 @@ func _select_slot() -> void:
|
||||||
func _on_mouse_entered() -> void:
|
func _on_mouse_entered() -> void:
|
||||||
_hovered = true
|
_hovered = true
|
||||||
if _hotbar != null:
|
if _hotbar != null:
|
||||||
item_hovered.emit(slot_index, _hotbar.get_item_id(slot_index))
|
var identity: StringName = _hotbar.get_item_id(slot_index)
|
||||||
|
if identity.is_empty():
|
||||||
|
identity = _hotbar.get_fish_catch_id(slot_index)
|
||||||
|
item_hovered.emit(slot_index, identity)
|
||||||
|
|
||||||
|
|
||||||
func _on_mouse_exited() -> void:
|
func _on_mouse_exited() -> void:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ extends Button
|
||||||
@onready var _favorite_marker: Label = %FavoriteMarker
|
@onready var _favorite_marker: Label = %FavoriteMarker
|
||||||
|
|
||||||
var catch_id: StringName
|
var catch_id: StringName
|
||||||
|
var _display_name: String = ""
|
||||||
var _neutral_position := Vector2.ZERO
|
var _neutral_position := Vector2.ZERO
|
||||||
var _target_position := Vector2.ZERO
|
var _target_position := Vector2.ZERO
|
||||||
var _motion_phase: float = 0.0
|
var _motion_phase: float = 0.0
|
||||||
|
|
@ -35,18 +36,48 @@ func _ready() -> void:
|
||||||
|
|
||||||
func configure(
|
func configure(
|
||||||
identity: StringName,
|
identity: StringName,
|
||||||
|
display_name: String,
|
||||||
texture: Texture2D,
|
texture: Texture2D,
|
||||||
phase: float,
|
phase: float,
|
||||||
depth_scale: float,
|
depth_scale: float,
|
||||||
rarity_color: Color,
|
rarity_color: Color,
|
||||||
) -> void:
|
) -> void:
|
||||||
catch_id = identity
|
catch_id = identity
|
||||||
|
_display_name = display_name
|
||||||
_fish_texture.texture = texture
|
_fish_texture.texture = texture
|
||||||
_fish_shadow.texture = texture
|
_fish_shadow.texture = texture
|
||||||
_motion_phase = phase
|
_motion_phase = phase
|
||||||
_depth_scale = depth_scale
|
_depth_scale = depth_scale
|
||||||
_rarity_color = rarity_color
|
_rarity_color = rarity_color
|
||||||
_refresh_style()
|
_refresh_style()
|
||||||
|
tooltip_text = "%s · drag to a hotbar slot" % _display_name
|
||||||
|
|
||||||
|
|
||||||
|
func _get_drag_data(_at_position: Vector2) -> Variant:
|
||||||
|
if catch_id.is_empty() or disabled or _fish_texture.texture == null:
|
||||||
|
return null
|
||||||
|
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(72.0, 52.0)
|
||||||
|
texture.texture = _fish_texture.texture
|
||||||
|
texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
texture.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
texture.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
preview.add_child(texture)
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = _display_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)
|
||||||
|
set_drag_preview(preview)
|
||||||
|
return {
|
||||||
|
"kind": "cooler_fish",
|
||||||
|
"catch_id": String(catch_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func set_target_position(target: Vector2, immediate: bool = false) -> void:
|
func set_target_position(target: Vector2, immediate: bool = false) -> void:
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ func setup(
|
||||||
network_profile_service,
|
network_profile_service,
|
||||||
network_player_list,
|
network_player_list,
|
||||||
)
|
)
|
||||||
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot)
|
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory)
|
||||||
_fishing_shop.setup(
|
_fishing_shop.setup(
|
||||||
player,
|
player,
|
||||||
wallet,
|
wallet,
|
||||||
|
|
|
||||||
34
ui/hotbar.gd
34
ui/hotbar.gd
|
|
@ -6,6 +6,8 @@ signal presentation_transition_finished(is_visible: bool)
|
||||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
const BubbleHotbarSlotType = preload(
|
const BubbleHotbarSlotType = preload(
|
||||||
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -28,6 +30,7 @@ const HOTBAR_MENU_Z_INDEX: int = 90
|
||||||
var _hotbar: PlayerHotbarType
|
var _hotbar: PlayerHotbarType
|
||||||
var _bag: PlayerBagType
|
var _bag: PlayerBagType
|
||||||
var _catalog: ItemCatalogType
|
var _catalog: ItemCatalogType
|
||||||
|
var _fish_inventory: FishInventoryType
|
||||||
var _fishing_spot: FishingSpotType
|
var _fishing_spot: FishingSpotType
|
||||||
var _slots: Array[BubbleHotbarSlotType] = []
|
var _slots: Array[BubbleHotbarSlotType] = []
|
||||||
var _gameplay_input_enabled: bool = false
|
var _gameplay_input_enabled: bool = false
|
||||||
|
|
@ -59,11 +62,13 @@ func setup(
|
||||||
bag: PlayerBagType,
|
bag: PlayerBagType,
|
||||||
catalog: ItemCatalogType,
|
catalog: ItemCatalogType,
|
||||||
fishing_spot: FishingSpotType,
|
fishing_spot: FishingSpotType,
|
||||||
|
fish_inventory: FishInventoryType,
|
||||||
) -> void:
|
) -> void:
|
||||||
_hotbar = hotbar
|
_hotbar = hotbar
|
||||||
_bag = bag
|
_bag = bag
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
if not _hotbar.slots_changed.is_connected(_refresh):
|
if not _hotbar.slots_changed.is_connected(_refresh):
|
||||||
_hotbar.slots_changed.connect(_refresh)
|
_hotbar.slots_changed.connect(_refresh)
|
||||||
if not _hotbar.selected_slot_changed.is_connected(
|
if not _hotbar.selected_slot_changed.is_connected(
|
||||||
|
|
@ -72,8 +77,10 @@ func setup(
|
||||||
_hotbar.selected_slot_changed.connect(_on_selected_slot_changed)
|
_hotbar.selected_slot_changed.connect(_on_selected_slot_changed)
|
||||||
if not _bag.contents_changed.is_connected(_refresh):
|
if not _bag.contents_changed.is_connected(_refresh):
|
||||||
_bag.contents_changed.connect(_refresh)
|
_bag.contents_changed.connect(_refresh)
|
||||||
|
if not _fish_inventory.catches_changed.is_connected(_refresh):
|
||||||
|
_fish_inventory.catches_changed.connect(_refresh)
|
||||||
for slot: BubbleHotbarSlotType in _slots:
|
for slot: BubbleHotbarSlotType in _slots:
|
||||||
slot.setup(_hotbar, _bag, _catalog)
|
slot.setup(_hotbar, _bag, _catalog, _fish_inventory)
|
||||||
slot.set_drag_enabled(_drag_enabled)
|
slot.set_drag_enabled(_drag_enabled)
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
@ -274,7 +281,7 @@ func _on_slot_item_hovered(
|
||||||
return
|
return
|
||||||
_hovered_slot_index = slot_index
|
_hovered_slot_index = slot_index
|
||||||
_item_name_timer.stop()
|
_item_name_timer.stop()
|
||||||
_show_item_name(item_id)
|
_show_assignment_name(slot_index, item_id)
|
||||||
|
|
||||||
|
|
||||||
func _on_slot_item_hover_ended(slot_index: int) -> void:
|
func _on_slot_item_hover_ended(slot_index: int) -> void:
|
||||||
|
|
@ -297,16 +304,31 @@ func _show_selected_item_briefly() -> void:
|
||||||
if _item_name_suppressed or _hotbar == null:
|
if _item_name_suppressed or _hotbar == null:
|
||||||
_hide_item_name()
|
_hide_item_name()
|
||||||
return
|
return
|
||||||
_show_item_name(_hotbar.get_selected_item_id())
|
var selected_slot: int = _hotbar.get_selected_slot()
|
||||||
|
var identity: StringName = _hotbar.get_selected_item_id()
|
||||||
|
if identity.is_empty():
|
||||||
|
identity = _hotbar.get_selected_fish_catch_id()
|
||||||
|
_show_assignment_name(selected_slot, identity)
|
||||||
if _selected_item_label.visible:
|
if _selected_item_label.visible:
|
||||||
_item_name_timer.start()
|
_item_name_timer.start()
|
||||||
|
|
||||||
|
|
||||||
func _show_item_name(item_id: StringName) -> void:
|
func _show_assignment_name(slot_index: int, identity: StringName) -> void:
|
||||||
if item_id.is_empty() or _catalog == null:
|
if identity.is_empty() or _hotbar == null:
|
||||||
_hide_item_name()
|
_hide_item_name()
|
||||||
return
|
return
|
||||||
var item := _catalog.get_item_by_id(item_id)
|
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||||||
|
if not catch_id.is_empty() and _fish_inventory != null:
|
||||||
|
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(catch_id)
|
||||||
|
if fish_catch != null:
|
||||||
|
_selected_item_label.text = fish_catch.fish.display_name
|
||||||
|
_selected_item_label.visible = true
|
||||||
|
return
|
||||||
|
var item = (
|
||||||
|
_catalog.get_item_by_id(identity)
|
||||||
|
if _catalog != null
|
||||||
|
else null
|
||||||
|
)
|
||||||
if item == null:
|
if item == null:
|
||||||
_hide_item_name()
|
_hide_item_name()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -2609,6 +2609,7 @@ func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
|
||||||
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
|
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
|
||||||
fish_node.configure(
|
fish_node.configure(
|
||||||
fish_catch.catch_id,
|
fish_catch.catch_id,
|
||||||
|
fish_catch.fish.display_name,
|
||||||
fish_catch.fish.display_texture,
|
fish_catch.fish.display_texture,
|
||||||
float(identity_hash % 628) / 100.0,
|
float(identity_hash % 628) / 100.0,
|
||||||
0.96 + float(identity_hash % 9) * 0.01,
|
0.96 + float(identity_hash % 9) * 0.01,
|
||||||
|
|
|
||||||
|
|
@ -1215,7 +1215,7 @@ theme_override_constants/separation = 7
|
||||||
|
|
||||||
[node name="BagHint" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection"]
|
[node name="BagHint" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/BagSection"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "drag hotbar-compatible items onto a slot below. right-click a slot to clear it."
|
text = "drag fish or hotbar-compatible items onto a slot below. right-click a slot to clear it."
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue