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 ready_for_equipment_refresh
|
||||
signal fish_showcase_toggle_requested
|
||||
|
||||
enum FishingState {
|
||||
READY,
|
||||
|
|
@ -449,6 +450,13 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
FishingState.READY:
|
||||
if not _new_cast_press_armed:
|
||||
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()
|
||||
if (
|
||||
active_item != null
|
||||
|
|
|
|||
|
|
@ -5,28 +5,56 @@ 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")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
|
||||
enum AssignmentKind {
|
||||
EMPTY,
|
||||
ITEM,
|
||||
FISH,
|
||||
}
|
||||
|
||||
signal slots_changed
|
||||
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 _catalog: ItemCatalogType
|
||||
var _fish_inventory: FishInventoryType
|
||||
var _slots: Array[StringName] = []
|
||||
var _fish_slots: Array[StringName] = []
|
||||
var _selected_slot: int = 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_slots.resize(SLOT_COUNT)
|
||||
_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
|
||||
_catalog = catalog
|
||||
_fish_inventory = fish_inventory
|
||||
if _bag != null and not _bag.contents_changed.is_connected(
|
||||
_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()
|
||||
|
||||
|
||||
|
|
@ -35,23 +63,55 @@ func assign_item(slot_index: int, item_id: StringName) -> bool:
|
|||
return false
|
||||
if _slots[slot_index] == item_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 _slots[index] == item_id:
|
||||
_slots[index] = StringName()
|
||||
selected_slot_was_affected = (
|
||||
selected_slot_was_affected or index == _selected_slot
|
||||
)
|
||||
_slots[slot_index] = item_id
|
||||
_fish_slots[slot_index] = StringName()
|
||||
slots_changed.emit()
|
||||
if slot_index == _selected_slot:
|
||||
selected_slot_changed.emit(_selected_slot, item_id)
|
||||
if selected_slot_was_affected:
|
||||
_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
|
||||
|
||||
|
||||
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
|
||||
_slots[slot_index] = StringName()
|
||||
_fish_slots[slot_index] = StringName()
|
||||
slots_changed.emit()
|
||||
if slot_index == _selected_slot:
|
||||
selected_slot_changed.emit(_selected_slot, StringName())
|
||||
_emit_selected_assignment()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -65,12 +125,12 @@ func swap_slots(from_index: int, to_index: int) -> bool:
|
|||
var temporary: StringName = _slots[from_index]
|
||||
_slots[from_index] = _slots[to_index]
|
||||
_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()
|
||||
if from_index == _selected_slot or to_index == _selected_slot:
|
||||
selected_slot_changed.emit(
|
||||
_selected_slot,
|
||||
_slots[_selected_slot]
|
||||
)
|
||||
_emit_selected_assignment()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -80,7 +140,7 @@ func select_slot(slot_index: int) -> bool:
|
|||
if _selected_slot == slot_index:
|
||||
return true
|
||||
_selected_slot = slot_index
|
||||
selected_slot_changed.emit(_selected_slot, _slots[_selected_slot])
|
||||
_emit_selected_assignment()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -100,21 +160,55 @@ func get_selected_item_id() -> StringName:
|
|||
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:
|
||||
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]:
|
||||
return _slots.duplicate()
|
||||
|
||||
|
||||
func get_fish_slots() -> Array[StringName]:
|
||||
return _fish_slots.duplicate()
|
||||
|
||||
|
||||
func replace_state(
|
||||
slots: Array[StringName],
|
||||
selected_slot: int,
|
||||
fish_slots: Array[StringName] = [],
|
||||
) -> bool:
|
||||
var normalized: Array[StringName] = []
|
||||
normalized.resize(SLOT_COUNT)
|
||||
normalized.fill(StringName())
|
||||
var normalized_fish: Array[StringName] = []
|
||||
normalized_fish.resize(SLOT_COUNT)
|
||||
normalized_fish.fill(StringName())
|
||||
var seen_assignments: Dictionary[StringName, bool] = {}
|
||||
for index: int in range(mini(slots.size(), SLOT_COUNT)):
|
||||
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):
|
||||
normalized[index] = item_id
|
||||
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
|
||||
_fish_slots = normalized_fish
|
||||
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
|
||||
slots_changed.emit()
|
||||
selected_slot_changed.emit(
|
||||
_selected_slot,
|
||||
_slots[_selected_slot]
|
||||
)
|
||||
_emit_selected_assignment()
|
||||
return true
|
||||
|
||||
|
||||
func has_any_assignment() -> bool:
|
||||
for item_id: StringName in _slots:
|
||||
if not item_id.is_empty():
|
||||
for index: int in range(SLOT_COUNT):
|
||||
if (
|
||||
not _slots[index].is_empty()
|
||||
or not _fish_slots[index].is_empty()
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
|
@ -149,23 +252,49 @@ func _can_assign(item_id: StringName) -> bool:
|
|||
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:
|
||||
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 (
|
||||
not _fish_slots[index].is_empty()
|
||||
and not _can_assign_fish(_fish_slots[index])
|
||||
):
|
||||
_fish_slots[index] = StringName()
|
||||
changed = true
|
||||
if changed:
|
||||
slots_changed.emit()
|
||||
selected_slot_changed.emit(
|
||||
_selected_slot,
|
||||
_slots[_selected_slot]
|
||||
)
|
||||
_emit_selected_assignment()
|
||||
|
||||
|
||||
func _on_bag_contents_changed() -> void:
|
||||
_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:
|
||||
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(
|
||||
"res://network/network_item_use_service.gd"
|
||||
)
|
||||
const NetworkFishShowcaseServiceType = preload(
|
||||
"res://network/network_fish_showcase_service.gd"
|
||||
)
|
||||
const NetworkChatServiceType = preload(
|
||||
"res://network/network_chat_service.gd"
|
||||
)
|
||||
|
|
@ -124,6 +127,9 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
|||
@onready var _network_item_use: NetworkItemUseServiceType = (
|
||||
%NetworkItemUseService
|
||||
)
|
||||
@onready var _network_fish_showcase: NetworkFishShowcaseServiceType = (
|
||||
%NetworkFishShowcaseService
|
||||
)
|
||||
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
|
||||
@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
|
||||
@onready var _network_player_list: NetworkPlayerListService = %NetworkPlayerListService
|
||||
|
|
@ -295,7 +301,7 @@ func _initialize_after_data_root() -> void:
|
|||
_asset_reservations
|
||||
)
|
||||
_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.setup_local_player(_player)
|
||||
_shop_interaction.local_player_range_changed.connect(
|
||||
|
|
@ -338,6 +344,13 @@ func _initialize_after_data_root() -> void:
|
|||
_save_manager,
|
||||
_asset_reservations
|
||||
)
|
||||
_network_fish_showcase.setup(
|
||||
_network_session,
|
||||
_player_spawn_service,
|
||||
fish_catalog,
|
||||
_player.inventory,
|
||||
_player.hotbar,
|
||||
)
|
||||
_network_chat.setup(_network_session)
|
||||
_network_fishing.setup(
|
||||
_network_session,
|
||||
|
|
@ -521,6 +534,9 @@ func _initialize_after_data_root() -> void:
|
|||
_fishing_spot.ready_for_equipment_refresh.connect(
|
||||
_refresh_active_hotbar_item
|
||||
)
|
||||
_fishing_spot.fish_showcase_toggle_requested.connect(
|
||||
_network_fish_showcase.toggle_selected_fish
|
||||
)
|
||||
_water_recovery.recovery_starting.connect(
|
||||
_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="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="Script" path="res://network/network_fish_showcase_service.gd" id="45_fish_showcase"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||
shader = ExtResource("40_title_water")
|
||||
|
|
@ -183,6 +184,10 @@ script = ExtResource("23_network_shop")
|
|||
unique_name_in_owner = true
|
||||
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]
|
||||
unique_name_in_owner = true
|
||||
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
|
||||
# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
|
||||
# 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.
|
||||
const SALE_RELIABLE_CHANNEL: int = 5
|
||||
const SHOP_RELIABLE_CHANNEL: int = 6
|
||||
|
|
@ -187,6 +187,7 @@ static func make_server_hello(
|
|||
"shop_v1",
|
||||
"item_use_v1",
|
||||
"equipment_v1",
|
||||
"fish_showcase_v1",
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
|
|||
|
|
@ -360,7 +360,8 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
if is_host():
|
||||
return str(capability) in PackedStringArray([
|
||||
"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",
|
||||
"profile_v1",
|
||||
"identity_v1",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends CharacterBody3D
|
|||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const CollectionLogType = preload("res://collection/collection_log.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 PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||
|
|
@ -106,6 +107,8 @@ class ShowcaseCameraSnapshot:
|
|||
@onready var _fishing_rod_tip: Marker3D = %FishingRodTip
|
||||
@onready var _catch_display: Node3D = %CatchDisplay
|
||||
@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 _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
|
||||
|
||||
|
||||
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:
|
||||
return _cast_origin.global_position
|
||||
|
||||
|
|
|
|||
|
|
@ -140,6 +140,21 @@ shaded = false
|
|||
double_sided = true
|
||||
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="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 1.35, 0)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class LoadSnapshot:
|
|||
var next_catch_sequence: int = 1
|
||||
var bag_items: Array[OwnedItemType] = []
|
||||
var hotbar_slots: Array[StringName] = []
|
||||
var fish_hotbar_slots: Array[StringName] = []
|
||||
var selected_hotbar_slot: int = 0
|
||||
var reel_speed_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 hotbar_restored: bool = _hotbar.replace_state(
|
||||
snapshot.hotbar_slots,
|
||||
snapshot.selected_hotbar_slot
|
||||
snapshot.selected_hotbar_slot,
|
||||
snapshot.fish_hotbar_slots,
|
||||
)
|
||||
var upgrades_restored: bool = _fishing_upgrades.restore_levels(
|
||||
snapshot.reel_speed_level,
|
||||
|
|
@ -393,6 +395,9 @@ func _build_save_dictionary() -> Dictionary:
|
|||
var serialized_slots: Array[String] = []
|
||||
for item_id: StringName in _hotbar.get_slots():
|
||||
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 (
|
||||
_wallet.get_balance() < 0
|
||||
or _wallet.get_balance() > MAX_SAFE_BALANCE
|
||||
|
|
@ -422,6 +427,7 @@ func _build_save_dictionary() -> Dictionary:
|
|||
"hotbar": {
|
||||
"selected_slot": _hotbar.get_selected_slot(),
|
||||
"slots": serialized_slots,
|
||||
"fish_slots": serialized_fish_slots,
|
||||
},
|
||||
"upgrades": _fishing_upgrades.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)
|
||||
):
|
||||
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(
|
||||
hotbar_data["selected_slot"],
|
||||
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 PlayerBagType = preload("res://inventory/player_bag.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 HOVER_SCALE: float = 1.025
|
||||
|
|
@ -41,6 +43,7 @@ const DEFORMATION_Y_AMPLITUDE: float = 0.007
|
|||
var _hotbar: PlayerHotbarType
|
||||
var _bag: PlayerBagType
|
||||
var _catalog: ItemCatalogType
|
||||
var _fish_inventory: FishInventoryType
|
||||
var _drag_enabled: bool = false
|
||||
var _drag_in_progress: bool = false
|
||||
var _selected: bool = false
|
||||
|
|
@ -69,10 +72,12 @@ func setup(
|
|||
hotbar: PlayerHotbarType,
|
||||
bag: PlayerBagType,
|
||||
catalog: ItemCatalogType,
|
||||
fish_inventory: FishInventoryType,
|
||||
) -> void:
|
||||
_hotbar = hotbar
|
||||
_bag = bag
|
||||
_catalog = catalog
|
||||
_fish_inventory = fish_inventory
|
||||
refresh()
|
||||
|
||||
|
||||
|
|
@ -144,12 +149,22 @@ func refresh() -> void:
|
|||
if _hotbar == null:
|
||||
return
|
||||
var item_id: StringName = _hotbar.get_item_id(slot_index)
|
||||
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||||
var item: ItemDataType = (
|
||||
_catalog.get_item_by_id(item_id)
|
||||
if _catalog != null and not item_id.is_empty()
|
||||
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 = (
|
||||
_bag.get_quantity(item_id)
|
||||
if _bag != null and not item_id.is_empty()
|
||||
|
|
@ -163,12 +178,17 @@ func refresh() -> void:
|
|||
_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"
|
||||
"%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_empty: bool = _empty
|
||||
_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:
|
||||
_apply_style()
|
||||
|
||||
|
|
@ -283,12 +303,22 @@ 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():
|
||||
var catch_id: StringName = _hotbar.get_fish_catch_id(slot_index)
|
||||
if item_id.is_empty() and catch_id.is_empty():
|
||||
return null
|
||||
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()
|
||||
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.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
|
|
@ -299,7 +329,6 @@ func _get_drag_data(_at_position: Vector2) -> Variant:
|
|||
return {
|
||||
"kind": "hotbar_slot",
|
||||
"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 != 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
|
||||
if kind == "bag_item":
|
||||
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
|
||||
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:
|
||||
|
|
@ -330,6 +365,11 @@ func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
|||
var payload: Dictionary = data
|
||||
if str(payload.get("kind", "")) == "hotbar_slot":
|
||||
_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:
|
||||
_hotbar.assign_item(
|
||||
slot_index,
|
||||
|
|
@ -346,7 +386,10 @@ func _select_slot() -> void:
|
|||
func _on_mouse_entered() -> void:
|
||||
_hovered = true
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ extends Button
|
|||
@onready var _favorite_marker: Label = %FavoriteMarker
|
||||
|
||||
var catch_id: StringName
|
||||
var _display_name: String = ""
|
||||
var _neutral_position := Vector2.ZERO
|
||||
var _target_position := Vector2.ZERO
|
||||
var _motion_phase: float = 0.0
|
||||
|
|
@ -35,18 +36,48 @@ func _ready() -> void:
|
|||
|
||||
func configure(
|
||||
identity: StringName,
|
||||
display_name: String,
|
||||
texture: Texture2D,
|
||||
phase: float,
|
||||
depth_scale: float,
|
||||
rarity_color: Color,
|
||||
) -> void:
|
||||
catch_id = identity
|
||||
_display_name = display_name
|
||||
_fish_texture.texture = texture
|
||||
_fish_shadow.texture = texture
|
||||
_motion_phase = phase
|
||||
_depth_scale = depth_scale
|
||||
_rarity_color = rarity_color
|
||||
_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:
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ func setup(
|
|||
network_profile_service,
|
||||
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(
|
||||
player,
|
||||
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 PlayerBagType = preload("res://inventory/player_bag.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(
|
||||
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
||||
)
|
||||
|
|
@ -28,6 +30,7 @@ const HOTBAR_MENU_Z_INDEX: int = 90
|
|||
var _hotbar: PlayerHotbarType
|
||||
var _bag: PlayerBagType
|
||||
var _catalog: ItemCatalogType
|
||||
var _fish_inventory: FishInventoryType
|
||||
var _fishing_spot: FishingSpotType
|
||||
var _slots: Array[BubbleHotbarSlotType] = []
|
||||
var _gameplay_input_enabled: bool = false
|
||||
|
|
@ -59,11 +62,13 @@ func setup(
|
|||
bag: PlayerBagType,
|
||||
catalog: ItemCatalogType,
|
||||
fishing_spot: FishingSpotType,
|
||||
fish_inventory: FishInventoryType,
|
||||
) -> void:
|
||||
_hotbar = hotbar
|
||||
_bag = bag
|
||||
_catalog = catalog
|
||||
_fishing_spot = fishing_spot
|
||||
_fish_inventory = fish_inventory
|
||||
if not _hotbar.slots_changed.is_connected(_refresh):
|
||||
_hotbar.slots_changed.connect(_refresh)
|
||||
if not _hotbar.selected_slot_changed.is_connected(
|
||||
|
|
@ -72,8 +77,10 @@ func setup(
|
|||
_hotbar.selected_slot_changed.connect(_on_selected_slot_changed)
|
||||
if not _bag.contents_changed.is_connected(_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:
|
||||
slot.setup(_hotbar, _bag, _catalog)
|
||||
slot.setup(_hotbar, _bag, _catalog, _fish_inventory)
|
||||
slot.set_drag_enabled(_drag_enabled)
|
||||
_refresh()
|
||||
|
||||
|
|
@ -274,7 +281,7 @@ func _on_slot_item_hovered(
|
|||
return
|
||||
_hovered_slot_index = slot_index
|
||||
_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:
|
||||
|
|
@ -297,16 +304,31 @@ 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())
|
||||
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:
|
||||
_item_name_timer.start()
|
||||
|
||||
|
||||
func _show_item_name(item_id: StringName) -> void:
|
||||
if item_id.is_empty() or _catalog == null:
|
||||
func _show_assignment_name(slot_index: int, identity: StringName) -> void:
|
||||
if identity.is_empty() or _hotbar == null:
|
||||
_hide_item_name()
|
||||
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:
|
||||
_hide_item_name()
|
||||
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())
|
||||
fish_node.configure(
|
||||
fish_catch.catch_id,
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.fish.display_texture,
|
||||
float(identity_hash % 628) / 100.0,
|
||||
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"]
|
||||
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
|
||||
autowrap_mode = 2
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue