Integrate per-save identity and interaction UI
This commit is contained in:
parent
ac82a28f3b
commit
4fda6e97f6
58 changed files with 3471 additions and 461 deletions
324
ui/chat_ui.gd
324
ui/chat_ui.gd
|
|
@ -25,9 +25,21 @@ const SPEECH_BUBBLE_WIDTH: float = 320.0
|
|||
const SPEECH_POINTER_HALF_WIDTH: float = 12.0
|
||||
const SPEECH_POINTER_HEIGHT: float = 14.0
|
||||
const SPEECH_POINTER_OVERLAP: float = 3.0
|
||||
const NAMEPLATE_WIDTH: float = 300.0
|
||||
const NAMEPLATE_NAME_HEIGHT: float = 24.0
|
||||
const NAMEPLATE_TITLE_TOP: float = 21.0
|
||||
const NAMEPLATE_TITLE_HEIGHT: float = 18.0
|
||||
const NAMEPLATE_HEIGHT: float = (
|
||||
NAMEPLATE_TITLE_TOP + NAMEPLATE_TITLE_HEIGHT
|
||||
)
|
||||
const NAMEPLATE_NAME_FONT_SIZE: int = 20
|
||||
const NAMEPLATE_TITLE_FONT_SIZE: int = 14
|
||||
const NAMEPLATE_HEAD_GAP: float = 3.0
|
||||
const NAMEPLATE_SPEECH_GAP: float = 5.0
|
||||
const ANIMALESE_FULL_VOLUME_DISTANCE: float = 4.0
|
||||
const ANIMALESE_SILENT_DISTANCE: float = 24.0
|
||||
const ANIMALESE_SILENT_VOLUME_DB: float = -80.0
|
||||
const ROLEPLAY_FONT_SLANT: float = -0.18
|
||||
const MAX_EDITOR_GIVE_BALANCE: int = 1_000_000_000_000
|
||||
const MOBILE_COMPACT_WIDTH: float = 620.0
|
||||
const MOBILE_EXPANDED_WIDTH: float = 820.0
|
||||
|
|
@ -107,6 +119,7 @@ var _player: Player
|
|||
var _fishing_spot: FishingSpot
|
||||
var _settings: PlayerSettingsManager
|
||||
var _history: RichTextLabel
|
||||
var _roleplay_font: FontVariation
|
||||
var _entry: LineEdit
|
||||
var _status: Label
|
||||
var _panel: PanelContainer
|
||||
|
|
@ -114,6 +127,7 @@ var _collapse_button: Button
|
|||
var _height_button: Button
|
||||
var _unread_indicator: Label
|
||||
var _hint: Label
|
||||
var _nameplate_layer: Control
|
||||
var _speech_layer: Control
|
||||
var _animalese_voice: AnimaleseVoiceType
|
||||
var _clock_panel: PanelContainer
|
||||
|
|
@ -125,6 +139,7 @@ var _calendar_balance: CurrencyAmount
|
|||
var _weather_icon: WeatherIconType
|
||||
var _status_effect_column: VBoxContainer
|
||||
var _status_effect_icons: Dictionary[StringName, TextureRect] = {}
|
||||
var _nameplates: Dictionary[int, Dictionary] = {}
|
||||
var _speech: Dictionary[int, Dictionary] = {}
|
||||
var _draft_save_timer: Timer
|
||||
var _opacity_tween: Tween
|
||||
|
|
@ -132,6 +147,7 @@ var _height_tween: Tween
|
|||
var _opened: bool = false
|
||||
var _available: bool = false
|
||||
var _hud_hidden: bool = false
|
||||
var _nameplates_hud_hidden: bool = false
|
||||
var _world_speech_visible: bool = true
|
||||
var _presentation_state := PresentationState.COMPACT
|
||||
var _visible_state_before_collapse := PresentationState.COMPACT
|
||||
|
|
@ -247,7 +263,12 @@ func setup(
|
|||
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||
_service.history_replaced.connect(_on_history)
|
||||
_service.send_rejected.connect(_on_rejected)
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||
_session.peer_display_name_changed.connect(_on_peer_display_name_changed)
|
||||
_session.peer_title_changed.connect(_on_peer_title_changed)
|
||||
_session.peer_removed.connect(_on_session_peer_removed)
|
||||
_spawn.avatar_spawned.connect(_on_avatar_spawned)
|
||||
_spawn.avatar_removed.connect(_on_avatar_removed)
|
||||
_entry.text = _settings.current_settings.chat_draft
|
||||
_entry.caret_column = _entry.text.length()
|
||||
_set_presentation_state(
|
||||
|
|
@ -258,6 +279,7 @@ func setup(
|
|||
false,
|
||||
)
|
||||
_refresh_history()
|
||||
_reconcile_nameplates()
|
||||
|
||||
|
||||
func _rebuild_status_effect_icons() -> void:
|
||||
|
|
@ -370,6 +392,7 @@ func open_command_chat() -> void:
|
|||
|
||||
func set_available(value: bool) -> void:
|
||||
_available = value
|
||||
_refresh_nameplate_layer_visibility()
|
||||
if not value:
|
||||
_clear_suspended_chat_state()
|
||||
_send_pending = false
|
||||
|
|
@ -601,6 +624,7 @@ func _process(_delta: float) -> void:
|
|||
_refresh_visibility()
|
||||
_update_status_effect_icons()
|
||||
_update_panel_opacity()
|
||||
_update_nameplates()
|
||||
_update_speech()
|
||||
|
||||
|
||||
|
|
@ -612,6 +636,11 @@ func _exit_tree() -> void:
|
|||
|
||||
|
||||
func _build_ui() -> void:
|
||||
_roleplay_font = FontVariation.new()
|
||||
_roleplay_font.base_font = UtilityPageStyle.TuffyFont
|
||||
var roleplay_transform := Transform2D.IDENTITY
|
||||
roleplay_transform.y.x = ROLEPLAY_FONT_SLANT
|
||||
_roleplay_font.variation_transform = roleplay_transform
|
||||
_clock_panel = PanelContainer.new()
|
||||
_clock_panel.name = "WorldClockPanel"
|
||||
_clock_panel.size = CLOCK_SIZE
|
||||
|
|
@ -720,7 +749,9 @@ func _build_ui() -> void:
|
|||
_history.scroll_following = true
|
||||
_history.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_history.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_history.bbcode_enabled = false
|
||||
_history.add_theme_font_override("normal_font", UtilityPageStyle.TuffyFont)
|
||||
_history.add_theme_font_override("italics_font", _roleplay_font)
|
||||
_history.add_theme_font_size_override("normal_font_size", HISTORY_FONT_SIZE)
|
||||
_history.add_theme_color_override("default_color", UtilityPageStyle.LIGHT_TEXT)
|
||||
_history.add_theme_stylebox_override("normal", _borderless_style(Color.TRANSPARENT))
|
||||
|
|
@ -846,7 +877,16 @@ func _build_ui() -> void:
|
|||
_hint.add_theme_constant_override("shadow_offset_x", 1)
|
||||
_hint.add_theme_constant_override("shadow_offset_y", 2)
|
||||
add_child(_hint)
|
||||
_nameplate_layer = Control.new()
|
||||
_nameplate_layer.name = "PlayerNameplateLayer"
|
||||
_nameplate_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_nameplate_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nameplate_layer.visible = (
|
||||
_available and _world_speech_visible and not _nameplates_hud_hidden
|
||||
)
|
||||
add_child(_nameplate_layer)
|
||||
_speech_layer = Control.new()
|
||||
_speech_layer.name = "PlayerSpeechLayer"
|
||||
_speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_speech_layer)
|
||||
|
|
@ -1193,10 +1233,15 @@ func _on_message(message: Dictionary) -> void:
|
|||
_update_panel_opacity(true)
|
||||
if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER:
|
||||
return
|
||||
var body: String = str(message["body"])
|
||||
# Roleplay actions belong only in chat history. They deliberately do not
|
||||
# create world speech, animalese, or a talking animation.
|
||||
if NetworkChatProtocol.is_roleplay_body(body):
|
||||
return
|
||||
var peer_id: int = message["sender_peer_id"]
|
||||
_show_speech_bubble(
|
||||
peer_id,
|
||||
str(message["body"]),
|
||||
body,
|
||||
str(message.get("sender_fingerprint", "")),
|
||||
VoiceProfilesType.sanitized_id(str(message.get(
|
||||
"voice_id", VoiceProfilesType.DEFAULT_ID
|
||||
|
|
@ -1221,6 +1266,8 @@ func _show_speech_bubble(
|
|||
requested_sample_set_id: String = "",
|
||||
) -> void:
|
||||
_on_peer_removed(peer_id)
|
||||
if not _speaker_shares_local_space(peer_id):
|
||||
return
|
||||
var bubble := PanelContainer.new()
|
||||
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
bubble.custom_minimum_size = Vector2(SPEECH_BUBBLE_WIDTH, 0.0)
|
||||
|
|
@ -1342,16 +1389,27 @@ func _on_rejected(message: String) -> void:
|
|||
func _refresh_history() -> void:
|
||||
if _service == null:
|
||||
return
|
||||
var lines: Array[String] = []
|
||||
var messages := _service.get_history()
|
||||
for message: Dictionary in messages:
|
||||
_history.clear()
|
||||
for message_index: int in messages.size():
|
||||
var message: Dictionary = messages[message_index]
|
||||
if message_index > 0:
|
||||
_history.add_text("\n")
|
||||
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.add_text("• %s" % str(message["body"]))
|
||||
continue
|
||||
var body: String = str(message["body"])
|
||||
if NetworkChatProtocol.is_roleplay_body(body):
|
||||
_history.push_italics()
|
||||
_history.add_text("%s %s" % [
|
||||
str(message["sender_display_name"]),
|
||||
NetworkChatProtocol.roleplay_action(body),
|
||||
])
|
||||
_history.text = "\n".join(lines)
|
||||
_history.pop()
|
||||
continue
|
||||
_history.add_text("%s: %s" % [
|
||||
str(message["sender_display_name"]), body,
|
||||
])
|
||||
_scroll_history_to_bottom.call_deferred()
|
||||
|
||||
|
||||
|
|
@ -1465,6 +1523,8 @@ func is_mobile_mode() -> bool:
|
|||
|
||||
func set_hud_hidden(is_hidden: bool) -> void:
|
||||
_hud_hidden = is_hidden
|
||||
_nameplates_hud_hidden = is_hidden
|
||||
_refresh_nameplate_layer_visibility()
|
||||
_refresh_visibility()
|
||||
|
||||
|
||||
|
|
@ -1472,8 +1532,14 @@ func is_hud_hidden() -> bool:
|
|||
return _hud_hidden
|
||||
|
||||
|
||||
func set_world_nameplates_hud_hidden(is_hidden: bool) -> void:
|
||||
_nameplates_hud_hidden = is_hidden
|
||||
_refresh_nameplate_layer_visibility()
|
||||
|
||||
|
||||
func set_world_speech_visible(should_be_visible: bool) -> void:
|
||||
_world_speech_visible = should_be_visible
|
||||
_refresh_nameplate_layer_visibility()
|
||||
if _speech_layer != null:
|
||||
_speech_layer.visible = should_be_visible
|
||||
|
||||
|
|
@ -1482,6 +1548,14 @@ func is_world_speech_visible() -> bool:
|
|||
return _world_speech_visible
|
||||
|
||||
|
||||
func _refresh_nameplate_layer_visibility() -> void:
|
||||
if _nameplate_layer == null:
|
||||
return
|
||||
_nameplate_layer.visible = (
|
||||
_available and _world_speech_visible and not _nameplates_hud_hidden
|
||||
)
|
||||
|
||||
|
||||
func _refresh_handle_labels(state: PresentationState) -> void:
|
||||
var collapsed := state == PresentationState.COLLAPSED
|
||||
var height_state := _visible_state_before_collapse if collapsed else state
|
||||
|
|
@ -1538,11 +1612,217 @@ func _update_panel_opacity(immediate_recheck: bool = false) -> void:
|
|||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
func _update_nameplates() -> void:
|
||||
_reconcile_nameplates()
|
||||
if _spawn == null or _player == null:
|
||||
_hide_all_nameplates()
|
||||
return
|
||||
var camera := _player.get_active_gameplay_camera()
|
||||
if camera == null:
|
||||
_hide_all_nameplates()
|
||||
return
|
||||
var viewport_size := get_viewport_rect().size
|
||||
for peer_id: int in _nameplates.keys():
|
||||
var state: Dictionary = _nameplates[peer_id]
|
||||
var plate := state.get("control") as Control
|
||||
var avatar := _spawn.get_avatar(peer_id)
|
||||
if plate == null:
|
||||
continue
|
||||
if (
|
||||
avatar == null
|
||||
or not _available
|
||||
or _nameplates_hud_hidden
|
||||
or not _world_speech_visible
|
||||
or not _speaker_shares_local_space(peer_id)
|
||||
or not _avatar_presentation_visible(peer_id, avatar)
|
||||
):
|
||||
plate.hide()
|
||||
continue
|
||||
var world_position := avatar.get_nameplate_anchor_position()
|
||||
if (
|
||||
camera.is_position_behind(world_position)
|
||||
or _is_speech_world_occluded(camera, avatar, world_position)
|
||||
):
|
||||
plate.hide()
|
||||
continue
|
||||
var screen_position := (
|
||||
camera.unproject_position(world_position) / _output_scale
|
||||
)
|
||||
if (
|
||||
screen_position.x < -NAMEPLATE_WIDTH * 0.5
|
||||
or screen_position.x > viewport_size.x + NAMEPLATE_WIDTH * 0.5
|
||||
or screen_position.y < -NAMEPLATE_HEIGHT
|
||||
or screen_position.y > viewport_size.y + NAMEPLATE_HEIGHT
|
||||
):
|
||||
plate.hide()
|
||||
continue
|
||||
plate.position = Vector2(
|
||||
screen_position.x - NAMEPLATE_WIDTH * 0.5,
|
||||
screen_position.y - NAMEPLATE_HEIGHT - NAMEPLATE_HEAD_GAP,
|
||||
)
|
||||
plate.show()
|
||||
|
||||
|
||||
func _reconcile_nameplates() -> void:
|
||||
if _spawn == null or _session == null or _nameplate_layer == null:
|
||||
return
|
||||
var live_peer_ids: Dictionary[int, bool] = {}
|
||||
for peer_id: int in _spawn.get_peer_ids():
|
||||
if (
|
||||
_spawn.get_avatar(peer_id) == null
|
||||
or _session.get_peer_record(peer_id) == null
|
||||
):
|
||||
continue
|
||||
live_peer_ids[peer_id] = true
|
||||
_ensure_nameplate(peer_id)
|
||||
for peer_id: int in _nameplates.keys():
|
||||
if not live_peer_ids.has(peer_id):
|
||||
_remove_nameplate(peer_id)
|
||||
|
||||
|
||||
func _ensure_nameplate(peer_id: int) -> void:
|
||||
if _nameplates.has(peer_id):
|
||||
_refresh_nameplate_text(peer_id)
|
||||
return
|
||||
var plate := Control.new()
|
||||
plate.name = "PlayerNameplate_%d" % peer_id
|
||||
plate.size = Vector2(NAMEPLATE_WIDTH, NAMEPLATE_HEIGHT)
|
||||
plate.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
plate.hide()
|
||||
var name_label := _create_nameplate_label(
|
||||
"PlayerName",
|
||||
NAMEPLATE_NAME_FONT_SIZE,
|
||||
0.0,
|
||||
NAMEPLATE_NAME_HEIGHT,
|
||||
)
|
||||
var title_label := _create_nameplate_label(
|
||||
"PlayerTitle",
|
||||
NAMEPLATE_TITLE_FONT_SIZE,
|
||||
NAMEPLATE_TITLE_TOP,
|
||||
NAMEPLATE_TITLE_HEIGHT,
|
||||
)
|
||||
plate.add_child(name_label)
|
||||
plate.add_child(title_label)
|
||||
_nameplate_layer.add_child(plate)
|
||||
_nameplates[peer_id] = {
|
||||
"control": plate,
|
||||
"name": name_label,
|
||||
"title": title_label,
|
||||
}
|
||||
_refresh_nameplate_text(peer_id)
|
||||
|
||||
|
||||
func _create_nameplate_label(
|
||||
node_name: String,
|
||||
font_size: int,
|
||||
vertical_position: float,
|
||||
height: float,
|
||||
) -> Label:
|
||||
var label := Label.new()
|
||||
label.name = node_name
|
||||
label.position = Vector2(0.0, vertical_position)
|
||||
label.size = Vector2(NAMEPLATE_WIDTH, height)
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.clip_text = true
|
||||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
label.add_theme_color_override("font_color", Color.WHITE)
|
||||
label.add_theme_color_override("font_outline_color", Color.TRANSPARENT)
|
||||
label.add_theme_color_override("font_shadow_color", Color.TRANSPARENT)
|
||||
label.add_theme_constant_override("outline_size", 0)
|
||||
label.add_theme_constant_override("shadow_offset_x", 0)
|
||||
label.add_theme_constant_override("shadow_offset_y", 0)
|
||||
return label
|
||||
|
||||
|
||||
func _refresh_nameplate_text(peer_id: int) -> void:
|
||||
if _session == null or not _nameplates.has(peer_id):
|
||||
return
|
||||
var record := _session.get_peer_record(peer_id)
|
||||
if record == null:
|
||||
return
|
||||
var state: Dictionary = _nameplates[peer_id]
|
||||
var name_label := state.get("name") as Label
|
||||
var title_label := state.get("title") as Label
|
||||
if name_label != null:
|
||||
name_label.text = (
|
||||
record.display_name
|
||||
if not record.display_name.is_empty()
|
||||
else NetworkProfilePreferences.DEFAULT_DISPLAY_NAME
|
||||
)
|
||||
if title_label != null:
|
||||
title_label.text = (
|
||||
record.title
|
||||
if not record.title.is_empty()
|
||||
else NetworkProfilePreferences.DEFAULT_TITLE
|
||||
)
|
||||
|
||||
|
||||
func _remove_nameplate(peer_id: int) -> void:
|
||||
var state: Dictionary = _nameplates.get(peer_id, {})
|
||||
var plate := state.get("control") as Control
|
||||
if plate != null:
|
||||
plate.queue_free()
|
||||
_nameplates.erase(peer_id)
|
||||
|
||||
|
||||
func _hide_all_nameplates() -> void:
|
||||
for state: Dictionary in _nameplates.values():
|
||||
var plate := state.get("control") as Control
|
||||
if plate != null:
|
||||
plate.hide()
|
||||
|
||||
|
||||
func _avatar_presentation_visible(peer_id: int, avatar: Player) -> bool:
|
||||
if _session == null or peer_id == _session.get_local_peer_id():
|
||||
return true
|
||||
return avatar.is_remote_presentation_visible()
|
||||
|
||||
|
||||
func _nameplate_speech_clearance(peer_id: int) -> float:
|
||||
var state: Dictionary = _nameplates.get(peer_id, {})
|
||||
var plate := state.get("control") as Control
|
||||
if plate == null or not plate.is_visible_in_tree():
|
||||
return 0.0
|
||||
return NAMEPLATE_HEIGHT + NAMEPLATE_HEAD_GAP + NAMEPLATE_SPEECH_GAP
|
||||
|
||||
|
||||
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||
_ensure_nameplate(peer_id)
|
||||
|
||||
|
||||
func _on_peer_display_name_changed(peer_id: int, _display_name: String) -> void:
|
||||
_ensure_nameplate(peer_id)
|
||||
|
||||
|
||||
func _on_peer_title_changed(peer_id: int, _title: String) -> void:
|
||||
_ensure_nameplate(peer_id)
|
||||
|
||||
|
||||
func _on_avatar_spawned(peer_id: int, _avatar: Player) -> void:
|
||||
_ensure_nameplate(peer_id)
|
||||
|
||||
|
||||
func _on_avatar_removed(peer_id: int) -> void:
|
||||
_remove_nameplate(peer_id)
|
||||
|
||||
|
||||
func _on_session_peer_removed(peer_id: int) -> void:
|
||||
_on_peer_removed(peer_id)
|
||||
_remove_nameplate(peer_id)
|
||||
|
||||
|
||||
func _update_speech() -> void:
|
||||
var now := Time.get_ticks_msec() / 1000.0
|
||||
for peer_id: int in _speech.keys():
|
||||
var state: Dictionary = _speech[peer_id]
|
||||
var bubble := state.get("bubble") as PanelContainer
|
||||
if not _speaker_shares_local_space(peer_id):
|
||||
_on_peer_removed(peer_id)
|
||||
continue
|
||||
if bubble == null or now >= float(state.get("expires", 0.0)):
|
||||
if bubble != null:
|
||||
bubble.queue_free()
|
||||
|
|
@ -1553,10 +1833,14 @@ func _update_speech() -> void:
|
|||
continue
|
||||
var avatar := _spawn.get_avatar(peer_id)
|
||||
var camera := _player.get_active_gameplay_camera()
|
||||
if avatar == null or camera == null:
|
||||
if (
|
||||
avatar == null
|
||||
or camera == null
|
||||
or not _avatar_presentation_visible(peer_id, avatar)
|
||||
):
|
||||
bubble.hide()
|
||||
continue
|
||||
var world_position := avatar.get_chat_anchor_position()
|
||||
var world_position := avatar.get_nameplate_anchor_position()
|
||||
if camera.is_position_behind(world_position):
|
||||
bubble.hide()
|
||||
continue
|
||||
|
|
@ -1566,6 +1850,7 @@ func _update_speech() -> void:
|
|||
var screen_position := (
|
||||
camera.unproject_position(world_position) / _output_scale
|
||||
)
|
||||
screen_position.y -= _nameplate_speech_clearance(peer_id)
|
||||
var viewport_size := get_viewport_rect().size
|
||||
if (
|
||||
screen_position.x < -80.0
|
||||
|
|
@ -1631,10 +1916,27 @@ func _is_speech_world_occluded(
|
|||
if collider is Player:
|
||||
excluded.append(collider.get_rid())
|
||||
continue
|
||||
if (
|
||||
collider != null
|
||||
and bool(collider.get_meta(
|
||||
HomeWorldService.INTERIOR_SHELL_COLLISION_META, false
|
||||
))
|
||||
):
|
||||
excluded.append(collider.get_rid())
|
||||
continue
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _speaker_shares_local_space(peer_id: int) -> bool:
|
||||
if _session == null:
|
||||
return true
|
||||
var local_peer_id: int = _session.get_local_peer_id()
|
||||
if local_peer_id <= 0:
|
||||
return true
|
||||
return _session.peers_share_space(local_peer_id, peer_id)
|
||||
|
||||
|
||||
func _on_draft_changed(_value: String) -> void:
|
||||
if _settings == null:
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue