Add multiplayer item use and session chat
This commit is contained in:
parent
5e04b70609
commit
5dda74c9e4
27 changed files with 1361 additions and 31 deletions
241
ui/chat_ui.gd
Normal file
241
ui/chat_ui.gd
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
class_name ChatUI
|
||||
extends Control
|
||||
|
||||
const INPUT_OWNER: StringName = &"chat"
|
||||
const COMPACT_SECONDS: float = 8.0
|
||||
const SPEECH_SECONDS: float = 6.0
|
||||
|
||||
var _service: NetworkChatService
|
||||
var _session: NetworkSession
|
||||
var _spawn: PlayerSpawnService
|
||||
var _player: Player
|
||||
var _fishing_spot: FishingSpot
|
||||
var _history: Label
|
||||
var _entry: LineEdit
|
||||
var _chat_button: Button
|
||||
var _panel: PanelContainer
|
||||
var _speech_layer: Control
|
||||
var _speech: Dictionary[int, Dictionary] = {}
|
||||
var _opened: bool = false
|
||||
var _available: bool = false
|
||||
var _last_message_time: float = -INF
|
||||
var _prior_movement: bool = true
|
||||
var _prior_camera: bool = true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_build_ui()
|
||||
|
||||
|
||||
func setup(
|
||||
service: NetworkChatService,
|
||||
session: NetworkSession,
|
||||
spawn: PlayerSpawnService,
|
||||
player: Player,
|
||||
fishing_spot: FishingSpot,
|
||||
) -> void:
|
||||
_service = service
|
||||
_session = session
|
||||
_spawn = spawn
|
||||
_player = player
|
||||
_fishing_spot = fishing_spot
|
||||
_service.message_received.connect(_on_message)
|
||||
_service.history_replaced.connect(_on_history)
|
||||
_service.send_rejected.connect(_on_rejected)
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
_refresh_history()
|
||||
|
||||
|
||||
func open_chat() -> void:
|
||||
if (
|
||||
_opened or not _available or _service == null
|
||||
or not _session.is_gameplay_session_active()
|
||||
):
|
||||
return
|
||||
_opened = true
|
||||
_prior_movement = _player.is_movement_enabled()
|
||||
_prior_camera = _player.is_camera_input_enabled()
|
||||
_player.set_movement_enabled(false)
|
||||
_player.set_camera_input_enabled(false)
|
||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||
_panel.show()
|
||||
_entry.show()
|
||||
_entry.grab_focus()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
|
||||
func set_available(value: bool) -> void:
|
||||
_available = value
|
||||
_chat_button.visible = value
|
||||
if not value:
|
||||
close_chat()
|
||||
|
||||
|
||||
func close_chat() -> void:
|
||||
if not _opened:
|
||||
return
|
||||
_opened = false
|
||||
_entry.release_focus()
|
||||
_entry.hide()
|
||||
_player.set_movement_enabled(_prior_movement)
|
||||
_player.set_camera_input_enabled(_prior_camera)
|
||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||
_refresh_visibility()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
if event.keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||
if _opened:
|
||||
_send()
|
||||
elif (
|
||||
_available
|
||||
and get_viewport().gui_get_focus_owner() is not LineEdit
|
||||
):
|
||||
open_chat()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.keycode == KEY_ESCAPE and _opened:
|
||||
close_chat()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_refresh_visibility()
|
||||
var now := Time.get_ticks_msec() / 1000.0
|
||||
for peer_id: int in _speech.keys():
|
||||
var state: Dictionary = _speech[peer_id]
|
||||
var label := state.get("label") as Label
|
||||
if label == null or now >= float(state.get("expires", 0.0)):
|
||||
if label != null:
|
||||
label.queue_free()
|
||||
_speech.erase(peer_id)
|
||||
continue
|
||||
var avatar := _spawn.get_avatar(peer_id)
|
||||
var camera := _player.get_gameplay_camera()
|
||||
if avatar == null or camera == null:
|
||||
label.hide()
|
||||
continue
|
||||
var world_position := avatar.get_body_center_position() + Vector3.UP * 1.2
|
||||
if camera.is_position_behind(world_position):
|
||||
label.hide()
|
||||
continue
|
||||
var screen_position := camera.unproject_position(world_position)
|
||||
label.position = screen_position - Vector2(label.size.x * 0.5, 42.0)
|
||||
label.show()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
_panel = PanelContainer.new()
|
||||
_panel.position = Vector2(18, 390)
|
||||
_panel.size = Vector2(390, 240)
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(_panel)
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 6)
|
||||
_panel.add_child(box)
|
||||
_history = Label.new()
|
||||
_history.custom_minimum_size = Vector2(360, 150)
|
||||
_history.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_history.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
box.add_child(_history)
|
||||
_entry = LineEdit.new()
|
||||
_entry.placeholder_text = "type a message…"
|
||||
_entry.max_length = NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
|
||||
_entry.text_submitted.connect(func(_value: String) -> void: _send())
|
||||
_entry.hide()
|
||||
box.add_child(_entry)
|
||||
_chat_button = Button.new()
|
||||
_chat_button.text = "chat"
|
||||
_chat_button.position = Vector2(18, 570)
|
||||
_chat_button.size = Vector2(76, 54)
|
||||
_chat_button.pressed.connect(open_chat)
|
||||
add_child(_chat_button)
|
||||
_speech_layer = Control.new()
|
||||
_speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_speech_layer)
|
||||
_panel.hide()
|
||||
|
||||
|
||||
func _send() -> void:
|
||||
var body := _entry.text
|
||||
if _service.send_local_message(body):
|
||||
_entry.clear()
|
||||
close_chat()
|
||||
|
||||
|
||||
func _on_message(message: Dictionary) -> void:
|
||||
_last_message_time = Time.get_ticks_msec() / 1000.0
|
||||
_refresh_history()
|
||||
if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER:
|
||||
return
|
||||
var peer_id: int = message["sender_peer_id"]
|
||||
_on_peer_removed(peer_id)
|
||||
var label := Label.new()
|
||||
label.text = "%s: %s" % [
|
||||
str(message["sender_display_name"]), str(message["body"]).left(120),
|
||||
]
|
||||
label.custom_minimum_size = Vector2(80, 34)
|
||||
label.size = Vector2(280, 70)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_speech_layer.add_child(label)
|
||||
_speech[peer_id] = {
|
||||
"label": label,
|
||||
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
func _on_history(_messages: Array[Dictionary]) -> void:
|
||||
if _messages.is_empty():
|
||||
for peer_id: int in _speech.keys():
|
||||
_on_peer_removed(peer_id)
|
||||
_refresh_history()
|
||||
|
||||
|
||||
func _on_rejected(message: String) -> void:
|
||||
_entry.placeholder_text = message
|
||||
if not _opened:
|
||||
open_chat()
|
||||
|
||||
|
||||
func _refresh_history() -> void:
|
||||
if _service == null:
|
||||
return
|
||||
var lines: Array[String] = []
|
||||
var messages := _service.get_history()
|
||||
var start := maxi(0, messages.size() - 8)
|
||||
for message: Dictionary in messages.slice(start):
|
||||
if int(message["kind"]) == NetworkChatProtocol.Kind.SYSTEM:
|
||||
lines.append("• %s" % str(message["body"]))
|
||||
else:
|
||||
lines.append("%s: %s" % [
|
||||
str(message["sender_display_name"]), str(message["body"]),
|
||||
])
|
||||
_history.text = "\n".join(lines)
|
||||
|
||||
|
||||
func _refresh_visibility() -> void:
|
||||
if not _available:
|
||||
_panel.hide()
|
||||
return
|
||||
if _opened:
|
||||
_panel.show()
|
||||
return
|
||||
var recent := (
|
||||
Time.get_ticks_msec() / 1000.0 - _last_message_time
|
||||
< COMPACT_SECONDS
|
||||
)
|
||||
_panel.visible = recent and _service != null and not _service.get_history().is_empty()
|
||||
|
||||
|
||||
func _on_peer_removed(peer_id: int) -> void:
|
||||
var state: Dictionary = _speech.get(peer_id, {})
|
||||
var label := state.get("label") as Label
|
||||
if label != null:
|
||||
label.queue_free()
|
||||
_speech.erase(peer_id)
|
||||
Loading…
Add table
Add a link
Reference in a new issue