Fix multiplayer gameplay and UI interaction edge cases
This commit is contained in:
parent
e589ca9133
commit
d690d74268
7 changed files with 137 additions and 19 deletions
|
|
@ -67,7 +67,6 @@ signal showcase_changed(
|
||||||
signal bite_activated
|
signal bite_activated
|
||||||
signal bite_prompt_changed(is_visible: bool)
|
signal bite_prompt_changed(is_visible: bool)
|
||||||
signal ready_for_equipment_refresh
|
signal ready_for_equipment_refresh
|
||||||
signal art_ui_toggle_requested
|
|
||||||
|
|
||||||
enum FishingState {
|
enum FishingState {
|
||||||
READY,
|
READY,
|
||||||
|
|
@ -510,13 +509,6 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
if not _new_cast_press_armed:
|
if not _new_cast_press_armed:
|
||||||
return
|
return
|
||||||
var active_item: ItemDataType = _get_active_item()
|
var active_item: ItemDataType = _get_active_item()
|
||||||
if (
|
|
||||||
active_item != null
|
|
||||||
and active_item.item_id == ArtShopStockType.ART_KIT_ITEM_ID
|
|
||||||
):
|
|
||||||
art_ui_toggle_requested.emit()
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if (
|
if (
|
||||||
active_item != null
|
active_item != null
|
||||||
and (
|
and (
|
||||||
|
|
|
||||||
11
main/main.gd
11
main/main.gd
|
|
@ -557,6 +557,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
_fishing_spot,
|
_fishing_spot,
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
|
_player.bag,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_player.experience,
|
_player.experience,
|
||||||
|
|
@ -1747,13 +1748,17 @@ func _on_active_hotbar_item_changed(
|
||||||
and item.category == ItemDataType.Category.ROD
|
and item.category == ItemDataType.Category.ROD
|
||||||
and _player.bag.owns_item(item_id)
|
and _player.bag.owns_item(item_id)
|
||||||
)
|
)
|
||||||
|
var active_is_art_kit: bool = (
|
||||||
|
item_id == ArtShopStockType.ART_KIT_ITEM_ID
|
||||||
|
and item != null
|
||||||
|
and _player.bag.owns_item(item_id)
|
||||||
|
)
|
||||||
_player.set_active_item_is_rod(active_is_rod, true)
|
_player.set_active_item_is_rod(active_is_rod, true)
|
||||||
_player.set_active_art_kit(
|
_player.set_active_art_kit(
|
||||||
item.icon if item != null else null,
|
item.icon if item != null else null,
|
||||||
item_id == ArtShopStockType.ART_KIT_ITEM_ID
|
active_is_art_kit,
|
||||||
and item != null
|
|
||||||
and _player.bag.owns_item(item_id),
|
|
||||||
)
|
)
|
||||||
|
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
|
||||||
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
||||||
item != null and _player.bag.owns_item(item_id)
|
item != null and _player.bag.owns_item(item_id)
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ var _session: NetworkSession
|
||||||
var _spawn_service: PlayerSpawnService
|
var _spawn_service: PlayerSpawnService
|
||||||
var _fishing_spot: FishingSpot
|
var _fishing_spot: FishingSpot
|
||||||
var _local_inventory: FishInventory
|
var _local_inventory: FishInventory
|
||||||
|
var _local_bag: PlayerBag
|
||||||
var _local_collection: CollectionLog
|
var _local_collection: CollectionLog
|
||||||
var _local_capacity: PlayerCoolerCapacity
|
var _local_capacity: PlayerCoolerCapacity
|
||||||
var _local_experience: PlayerExperienceType
|
var _local_experience: PlayerExperienceType
|
||||||
|
|
@ -47,6 +48,7 @@ var _result_acknowledgements: Dictionary[String, String] = {}
|
||||||
var _last_cast_time: Dictionary[int, float] = {}
|
var _last_cast_time: Dictionary[int, float] = {}
|
||||||
var _last_input_time: Dictionary[int, float] = {}
|
var _last_input_time: Dictionary[int, float] = {}
|
||||||
var _remote_presentations: Dictionary[int, RemoteFishingPresentation] = {}
|
var _remote_presentations: Dictionary[int, RemoteFishingPresentation] = {}
|
||||||
|
var _pending_local_bait_by_request: Dictionary[String, StringName] = {}
|
||||||
var _snapshot_accumulator: float = 0.0
|
var _snapshot_accumulator: float = 0.0
|
||||||
var _local_input_sequence: int = 0
|
var _local_input_sequence: int = 0
|
||||||
|
|
||||||
|
|
@ -56,6 +58,7 @@ func setup(
|
||||||
spawn_service: PlayerSpawnService,
|
spawn_service: PlayerSpawnService,
|
||||||
fishing_spot: FishingSpot,
|
fishing_spot: FishingSpot,
|
||||||
local_inventory: FishInventory,
|
local_inventory: FishInventory,
|
||||||
|
local_bag: PlayerBag,
|
||||||
local_collection: CollectionLog,
|
local_collection: CollectionLog,
|
||||||
local_capacity: PlayerCoolerCapacity,
|
local_capacity: PlayerCoolerCapacity,
|
||||||
local_experience: PlayerExperienceType,
|
local_experience: PlayerExperienceType,
|
||||||
|
|
@ -68,6 +71,7 @@ func setup(
|
||||||
_spawn_service = spawn_service
|
_spawn_service = spawn_service
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
_local_inventory = local_inventory
|
_local_inventory = local_inventory
|
||||||
|
_local_bag = local_bag
|
||||||
_local_collection = local_collection
|
_local_collection = local_collection
|
||||||
_local_capacity = local_capacity
|
_local_capacity = local_capacity
|
||||||
_local_experience = local_experience
|
_local_experience = local_experience
|
||||||
|
|
@ -97,6 +101,18 @@ func request_local_cast(
|
||||||
):
|
):
|
||||||
local_cast_rejected.emit("Fishing attempt ended.")
|
local_cast_rejected.emit("Fishing attempt ended.")
|
||||||
return ""
|
return ""
|
||||||
|
var bait_id := StringName(str(evidence.get("bait_id", "")))
|
||||||
|
if not bait_id.is_empty() and (
|
||||||
|
_local_bag == null or not _local_bag.owns_item(bait_id)
|
||||||
|
):
|
||||||
|
local_cast_rejected.emit("No bait available.")
|
||||||
|
return ""
|
||||||
|
var lure_id := StringName(str(evidence.get("lure_id", "")))
|
||||||
|
if not lure_id.is_empty() and (
|
||||||
|
_local_bag == null or not _local_bag.owns_item(lure_id)
|
||||||
|
):
|
||||||
|
local_cast_rejected.emit("Lure is unavailable.")
|
||||||
|
return ""
|
||||||
var request_id: String = _new_id("cast")
|
var request_id: String = _new_id("cast")
|
||||||
var data: Dictionary = {
|
var data: Dictionary = {
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
|
|
@ -111,12 +127,14 @@ func request_local_cast(
|
||||||
"rarity_multipliers": evidence.get("rarity_multipliers", []),
|
"rarity_multipliers": evidence.get("rarity_multipliers", []),
|
||||||
"discovered_fish_ids": evidence.get("discovered_fish_ids", []),
|
"discovered_fish_ids": evidence.get("discovered_fish_ids", []),
|
||||||
"capacity_available": bool(evidence.get("capacity_available", false)),
|
"capacity_available": bool(evidence.get("capacity_available", false)),
|
||||||
"bait_id": str(evidence.get("bait_id", "")),
|
"bait_id": str(bait_id),
|
||||||
"lure_id": str(evidence.get("lure_id", "")),
|
"lure_id": str(lure_id),
|
||||||
}
|
}
|
||||||
if _session.is_host():
|
if _session.is_host():
|
||||||
_handle_cast_request(_session.get_local_peer_id(), data)
|
_handle_cast_request(_session.get_local_peer_id(), data)
|
||||||
else:
|
else:
|
||||||
|
if not bait_id.is_empty():
|
||||||
|
_pending_local_bait_by_request[request_id] = bait_id
|
||||||
submit_cast_request.rpc_id(1, data)
|
submit_cast_request.rpc_id(1, data)
|
||||||
return request_id
|
return request_id
|
||||||
|
|
||||||
|
|
@ -303,6 +321,14 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
return
|
return
|
||||||
var bait_id: StringName = StringName(str(data.get("bait_id", "")))
|
var bait_id: StringName = StringName(str(data.get("bait_id", "")))
|
||||||
var avatar_bag: PlayerBag = avatar.bag
|
var avatar_bag: PlayerBag = avatar.bag
|
||||||
|
var owns_authoritative_bag: bool = (
|
||||||
|
peer_id == _session.get_local_peer_id()
|
||||||
|
)
|
||||||
|
var bait: ItemDataType = (
|
||||||
|
_item_catalog.get_item_by_id(bait_id)
|
||||||
|
if not bait_id.is_empty() and _item_catalog != null
|
||||||
|
else null
|
||||||
|
)
|
||||||
var lure_id: StringName = StringName(str(data.get("lure_id", "")))
|
var lure_id: StringName = StringName(str(data.get("lure_id", "")))
|
||||||
var lure: ItemDataType = (
|
var lure: ItemDataType = (
|
||||||
_item_catalog.get_item_by_id(lure_id)
|
_item_catalog.get_item_by_id(lure_id)
|
||||||
|
|
@ -314,13 +340,28 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
and (
|
and (
|
||||||
lure == null
|
lure == null
|
||||||
or not lure.is_lure()
|
or not lure.is_lure()
|
||||||
or avatar_bag == null
|
or (
|
||||||
|
owns_authoritative_bag
|
||||||
|
and (
|
||||||
|
avatar_bag == null
|
||||||
or not avatar_bag.owns_item(lure_id)
|
or not avatar_bag.owns_item(lure_id)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
_record_and_reject(peer_id, request_id, "Lure is unavailable.")
|
_record_and_reject(peer_id, request_id, "Lure is unavailable.")
|
||||||
return
|
return
|
||||||
if not bait_id.is_empty() and (avatar_bag == null or not avatar_bag.remove_item(bait_id, 1)):
|
if not bait_id.is_empty() and (
|
||||||
|
bait == null
|
||||||
|
or not bait.is_bait()
|
||||||
|
or (
|
||||||
|
owns_authoritative_bag
|
||||||
|
and (
|
||||||
|
avatar_bag == null
|
||||||
|
or not avatar_bag.remove_item(bait_id, 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
_record_and_reject(peer_id, request_id, "No bait available.")
|
_record_and_reject(peer_id, request_id, "No bait available.")
|
||||||
return
|
return
|
||||||
var attempt := NetworkFishingAttempt.new()
|
var attempt := NetworkFishingAttempt.new()
|
||||||
|
|
@ -1025,6 +1066,20 @@ func _apply_cast_accepted(data: Dictionary) -> void:
|
||||||
# On the host this replaces the same authoritative value with itself.
|
# On the host this replaces the same authoritative value with itself.
|
||||||
if not _session.is_host():
|
if not _session.is_host():
|
||||||
_attempts[peer_id] = attempt
|
_attempts[peer_id] = attempt
|
||||||
|
var pending_bait_id: StringName = (
|
||||||
|
_pending_local_bait_by_request.get(
|
||||||
|
attempt.request_id,
|
||||||
|
StringName(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_pending_local_bait_by_request.erase(attempt.request_id)
|
||||||
|
if not pending_bait_id.is_empty() and (
|
||||||
|
_local_bag == null
|
||||||
|
or not _local_bag.remove_item(pending_bait_id, 1)
|
||||||
|
):
|
||||||
|
cancel_local_attempt("No bait available.")
|
||||||
|
local_cast_rejected.emit("No bait available.")
|
||||||
|
return
|
||||||
local_cast_accepted.emit(attempt.attempt_id, target)
|
local_cast_accepted.emit(attempt.attempt_id, target)
|
||||||
else:
|
else:
|
||||||
var presentation := _get_remote_presentation(peer_id)
|
var presentation := _get_remote_presentation(peer_id)
|
||||||
|
|
@ -1093,7 +1148,8 @@ func _send_cast_rejected(
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable", 0)
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
func receive_cast_rejected(_request_id: String, message: String) -> void:
|
func receive_cast_rejected(request_id: String, message: String) -> void:
|
||||||
|
_pending_local_bait_by_request.erase(request_id)
|
||||||
local_cast_rejected.emit(message)
|
local_cast_rejected.emit(message)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -296,11 +296,20 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(not service.is_active() and not toolbar.visible)
|
assert(not service.is_active() and not toolbar.visible)
|
||||||
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
||||||
|
player.hotbar.select_slot(0)
|
||||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
||||||
fishing_spot.art_ui_toggle_requested.emit()
|
|
||||||
await process_frame
|
await process_frame
|
||||||
|
assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
|
||||||
assert(service.is_active() and toolbar.visible)
|
assert(service.is_active() and toolbar.visible)
|
||||||
assert(service.get_grid_size() == 128)
|
assert(service.get_grid_size() == 128)
|
||||||
|
assert(service.handle_input(world_click, true))
|
||||||
|
assert(service.is_active() and toolbar.visible)
|
||||||
|
player.hotbar.select_slot(1)
|
||||||
|
await process_frame
|
||||||
|
assert(not service.is_active() and not toolbar.visible)
|
||||||
|
assert(service.handle_input(paint_key, true))
|
||||||
|
await process_frame
|
||||||
|
assert(service.is_active() and toolbar.visible)
|
||||||
|
|
||||||
print("Art tools validation: PASS")
|
print("Art tools validation: PASS")
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ func _run_host() -> void:
|
||||||
) as PlayerSpawnService
|
) as PlayerSpawnService
|
||||||
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
assert(remote_avatar != null)
|
assert(remote_avatar != null)
|
||||||
|
assert(remote_avatar.bag.get_quantity(&"worms") == 0)
|
||||||
remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1)
|
remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1)
|
||||||
var remote_visuals := remote_avatar.get_node("Visuals") as Node3D
|
var remote_visuals := remote_avatar.get_node("Visuals") as Node3D
|
||||||
remote_visuals.rotation.y = PI * 0.5
|
remote_visuals.rotation.y = PI * 0.5
|
||||||
|
|
@ -178,6 +179,12 @@ func _run_client() -> void:
|
||||||
)
|
)
|
||||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
|
var item_catalog := main.get("item_catalog") as ItemCatalog
|
||||||
|
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
|
||||||
|
if player.bag.get_quantity(&"worms") == 0:
|
||||||
|
assert(player.bag.add_item(&"worms", 1))
|
||||||
|
assert(player.equip_bait(worms))
|
||||||
|
var worms_before_cast: int = player.bag.get_quantity(&"worms")
|
||||||
var character_animation_player := player.get_node(
|
var character_animation_player := player.get_node(
|
||||||
"Visuals/CharacterRig/AnimationPlayer"
|
"Visuals/CharacterRig/AnimationPlayer"
|
||||||
) as AnimationPlayer
|
) as AnimationPlayer
|
||||||
|
|
@ -265,6 +272,7 @@ func _run_client() -> void:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert(service.has_local_attempt())
|
assert(service.has_local_attempt())
|
||||||
|
assert(player.bag.get_quantity(&"worms") == worms_before_cast - 1)
|
||||||
var fishing_deadline: int = Time.get_ticks_msec() + 5000
|
var fishing_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < fishing_deadline
|
Time.get_ticks_msec() < fishing_deadline
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ func set_available(value: bool) -> void:
|
||||||
func close_chat() -> void:
|
func close_chat() -> void:
|
||||||
var ownership_was_active: bool = _opened or _input_lock_applied
|
var ownership_was_active: bool = _opened or _input_lock_applied
|
||||||
_opened = false
|
_opened = false
|
||||||
|
_set_status("")
|
||||||
_entry.virtual_keyboard_enabled = false
|
_entry.virtual_keyboard_enabled = false
|
||||||
_entry.release_focus()
|
_entry.release_focus()
|
||||||
_entry.hide()
|
_entry.hide()
|
||||||
|
|
@ -743,6 +744,9 @@ func _send() -> void:
|
||||||
if _send_pending:
|
if _send_pending:
|
||||||
return
|
return
|
||||||
var body := _entry.text
|
var body := _entry.text
|
||||||
|
if body.strip_edges().is_empty():
|
||||||
|
close_chat()
|
||||||
|
return
|
||||||
if _handle_chat_command(body):
|
if _handle_chat_command(body):
|
||||||
return
|
return
|
||||||
_send_pending = true
|
_send_pending = true
|
||||||
|
|
@ -1114,6 +1118,9 @@ func _update_speech() -> void:
|
||||||
if camera.is_position_behind(world_position):
|
if camera.is_position_behind(world_position):
|
||||||
bubble.hide()
|
bubble.hide()
|
||||||
continue
|
continue
|
||||||
|
if _is_speech_world_occluded(camera, avatar, world_position):
|
||||||
|
bubble.hide()
|
||||||
|
continue
|
||||||
var screen_position := (
|
var screen_position := (
|
||||||
camera.unproject_position(world_position) / _output_scale
|
camera.unproject_position(world_position) / _output_scale
|
||||||
)
|
)
|
||||||
|
|
@ -1154,6 +1161,38 @@ func _update_speech() -> void:
|
||||||
bubble.show()
|
bubble.show()
|
||||||
|
|
||||||
|
|
||||||
|
func _is_speech_world_occluded(
|
||||||
|
camera: Camera3D,
|
||||||
|
speaker: Player,
|
||||||
|
world_position: Vector3,
|
||||||
|
) -> bool:
|
||||||
|
var world := camera.get_world_3d()
|
||||||
|
if world == null:
|
||||||
|
return false
|
||||||
|
var excluded: Array[RID] = []
|
||||||
|
if _player != null:
|
||||||
|
excluded.append(_player.get_rid())
|
||||||
|
if speaker != _player:
|
||||||
|
excluded.append(speaker.get_rid())
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
|
camera.global_position,
|
||||||
|
world_position,
|
||||||
|
)
|
||||||
|
query.collide_with_areas = false
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
for _attempt: int in range(8):
|
||||||
|
query.exclude = excluded
|
||||||
|
var hit: Dictionary = world.direct_space_state.intersect_ray(query)
|
||||||
|
if hit.is_empty():
|
||||||
|
return false
|
||||||
|
var collider := hit.get("collider") as CollisionObject3D
|
||||||
|
if collider is Player:
|
||||||
|
excluded.append(collider.get_rid())
|
||||||
|
continue
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _on_draft_changed(_value: String) -> void:
|
func _on_draft_changed(_value: String) -> void:
|
||||||
if _settings == null:
|
if _settings == null:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,6 @@ func setup(
|
||||||
fishing_spot.bite_prompt_changed.connect(_on_bite_prompt_changed)
|
fishing_spot.bite_prompt_changed.connect(_on_bite_prompt_changed)
|
||||||
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
|
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
|
||||||
fishing_spot.showcase_changed.connect(_on_showcase_changed)
|
fishing_spot.showcase_changed.connect(_on_showcase_changed)
|
||||||
fishing_spot.art_ui_toggle_requested.connect(_toggle_surface_drawing)
|
|
||||||
_player_menu.menu_visibility_changed.connect(
|
_player_menu.menu_visibility_changed.connect(
|
||||||
_on_player_menu_visibility_changed
|
_on_player_menu_visibility_changed
|
||||||
)
|
)
|
||||||
|
|
@ -1113,6 +1112,16 @@ func _toggle_surface_drawing() -> void:
|
||||||
_surface_drawing.activate(_drawing_pointer_window_position())
|
_surface_drawing.activate(_drawing_pointer_window_position())
|
||||||
|
|
||||||
|
|
||||||
|
func set_surface_drawing_hotbar_selected(is_selected: bool) -> void:
|
||||||
|
if _surface_drawing == null:
|
||||||
|
return
|
||||||
|
if is_selected:
|
||||||
|
if not _surface_drawing.is_active() and _surface_drawing.can_activate():
|
||||||
|
_surface_drawing.activate(_drawing_pointer_window_position())
|
||||||
|
elif _surface_drawing.is_active():
|
||||||
|
_surface_drawing.deactivate()
|
||||||
|
|
||||||
|
|
||||||
func _drawing_pointer_window_position() -> Vector2:
|
func _drawing_pointer_window_position() -> Vector2:
|
||||||
if _virtual_mouse_active:
|
if _virtual_mouse_active:
|
||||||
return _virtual_mouse_window_position
|
return _virtual_mouse_window_position
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue