diff --git a/Config/game_config.json b/Config/game_config.json index a0a2485..bb19777 100644 --- a/Config/game_config.json +++ b/Config/game_config.json @@ -6,6 +6,7 @@ }, "network": { "api_base_url": "https://whaletownend.xinghangee.icu", + "location_ws_url": "wss://whaletownend.xinghangee.icu/game", "timeout": 30, "retry_count": 3 }, diff --git a/_Core/managers/ChatManager.gd b/_Core/managers/ChatManager.gd index 384953c..bfcc66a 100644 --- a/_Core/managers/ChatManager.gd +++ b/_Core/managers/ChatManager.gd @@ -126,6 +126,10 @@ var _game_token: String = "" const SELF_ECHO_DEDUPE_WINDOW: float = 10.0 var _pending_self_messages: Array[Dictionary] = [] +# 空消息类型告警限频(避免日志刷屏) +const EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: float = 10.0 +var _last_empty_message_type_warning_at: float = -1000.0 + # ============================================================================ # 生命周期方法 # ============================================================================ @@ -489,12 +493,22 @@ func _on_data_received(message: String) -> void: push_error("ChatManager: JSON 解析失败") return - var data: Dictionary = json.data + var data_variant: Variant = json.data + if not (data_variant is Dictionary): + push_warning("ChatManager: 收到非对象消息,已忽略") + return - # 检查消息类型字段 - var message_type: String = data.get("t", "") + var data: Dictionary = data_variant + + # 兼容不同后端字段命名:t / type + var message_type: String = str(data.get("t", data.get("type", ""))).strip_edges() + if message_type.is_empty(): + _warn_empty_message_type_limited(data) + return match message_type: + "connected": + pass "login_success": _handle_login_success(data) "login_error": @@ -509,9 +523,22 @@ func _on_data_received(message: String) -> void: _handle_chat_render(data) "position_updated": _handle_position_updated(data) + "error": + _handle_error_response(data) _: push_warning("ChatManager: 未处理的消息类型 %s" % message_type) +func _warn_empty_message_type_limited(data: Dictionary) -> void: + var now: float = Time.get_unix_time_from_system() + if now - _last_empty_message_type_warning_at < EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: + return + + _last_empty_message_type_warning_at = now + var payload_preview: String = JSON.stringify(data) + if payload_preview.length() > 180: + payload_preview = payload_preview.substr(0, 180) + "..." + push_warning("ChatManager: 收到未带消息类型的消息,已忽略 payload=%s" % payload_preview) + # 处理登录成功 func _handle_login_success(data: Dictionary) -> void: _is_logged_in = true diff --git a/_Core/managers/LocationManager.gd b/_Core/managers/LocationManager.gd index 545d455..7700024 100644 --- a/_Core/managers/LocationManager.gd +++ b/_Core/managers/LocationManager.gd @@ -6,7 +6,11 @@ extends Node # 负责与后端 WebSocket 服务进行位置同步和多人会话管理 # # 协议文档: new_docs/game_architecture_design.md -# 后端地址: wss://whaletownend.xinghangee.icu/location-broadcast +# 后端地址默认值: wss://whaletownend.xinghangee.icu/game +# 可通过以下方式覆盖: +# 1) 环境变量 WHALETOWN_LOCATION_WS_URL +# 2) Config/game_config.json 或 config/game_config.json 中的 +# network.location_ws_url / network.game_ws_url # ============================================================================ signal connected_to_server() @@ -17,23 +21,32 @@ signal user_joined(data: Dictionary) signal user_left(data: Dictionary) signal position_updated(data: Dictionary) -const WS_URL = "wss://whaletownend.xinghangee.icu/location-broadcast" +const DEFAULT_WS_URL: String = "wss://whaletownend.xinghangee.icu/game" +const WS_URL_ENV_KEY: String = "WHALETOWN_LOCATION_WS_URL" const PING_INTERVAL = 25.0 # 秒 var _socket: WebSocketPeer var _connected: bool = false var _ping_timer: float = 0.0 var _auth_token: String = "" +var _connection_error_reported: bool = false +var _is_connecting: bool = false +var _ws_url: String = DEFAULT_WS_URL func _ready(): _socket = WebSocketPeer.new() process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络 + _ws_url = _resolve_ws_url() func _process(delta): _socket.poll() + var state = _socket.get_ready_state() if state == WebSocketPeer.STATE_OPEN: + _connection_error_reported = false + _is_connecting = false + if not _connected: _on_connected() @@ -51,19 +64,71 @@ func _process(delta): elif state == WebSocketPeer.STATE_CLOSED: if _connected: _on_disconnected() + elif _is_connecting and not _connection_error_reported: + var close_code := _socket.get_close_code() + var close_reason := _socket.get_close_reason() + push_warning( + "LocationManager: WebSocket 握手失败,close_code=%d, reason=%s" % [close_code, close_reason] + ) + connection_error.emit() + _connection_error_reported = true + _is_connecting = false func connect_to_server(): - if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN: + var state: WebSocketPeer.State = _socket.get_ready_state() + if state == WebSocketPeer.STATE_OPEN or state == WebSocketPeer.STATE_CONNECTING: return - - var err = _socket.connect_to_url(WS_URL) + + _connection_error_reported = false + _is_connecting = true + var err = _socket.connect_to_url(_ws_url) if err != OK: - push_error("LocationManager: WebSocket 连接请求失败,错误码: %d" % err) + push_error("LocationManager: WebSocket 连接请求失败,url=%s, 错误码: %d" % [_ws_url, err]) connection_error.emit() + _connection_error_reported = true + _is_connecting = false else: # Godot WebSocket connect is non-blocking, wait for state change in _process pass +func _resolve_ws_url() -> String: + var env_url: String = OS.get_environment(WS_URL_ENV_KEY).strip_edges() + if not env_url.is_empty(): + return env_url + + for config_path in ["res://Config/game_config.json", "res://config/game_config.json"]: + var config_url: String = _load_ws_url_from_config(config_path) + if not config_url.is_empty(): + return config_url + + return DEFAULT_WS_URL + +func _load_ws_url_from_config(config_path: String) -> String: + if not FileAccess.file_exists(config_path): + return "" + + var content: String = FileAccess.get_file_as_string(config_path) + if content.is_empty(): + return "" + + var json := JSON.new() + if json.parse(content) != OK: + push_warning("LocationManager: 读取配置失败 %s - %s" % [config_path, json.get_error_message()]) + return "" + + var data_variant: Variant = json.data + if not (data_variant is Dictionary): + return "" + + var root: Dictionary = data_variant + var network_variant: Variant = root.get("network", {}) + if not (network_variant is Dictionary): + return "" + + var network_config: Dictionary = network_variant + var ws_url: String = str(network_config.get("location_ws_url", network_config.get("game_ws_url", ""))).strip_edges() + return ws_url + func close_connection(): _socket.close() diff --git a/_Core/managers/SceneManager.gd b/_Core/managers/SceneManager.gd index bb46100..8767e4a 100644 --- a/_Core/managers/SceneManager.gd +++ b/_Core/managers/SceneManager.gd @@ -186,9 +186,9 @@ func set_next_spawn_name(spawn_name: String) -> void: # - 此方法会清除存储的名称,只能获取一次 # - 如果未设置名称,返回空字符串 func get_next_spawn_name() -> String: - var name = _next_spawn_name + var spawn_name: String = _next_spawn_name _next_spawn_name = "" - return name + return spawn_name # ============ 场景注册方法 ============ diff --git a/assets/characters/crayfish_npc_256_256.png b/assets/characters/crayfish_npc_256_256.png new file mode 100644 index 0000000..cfcfcf0 Binary files /dev/null and b/assets/characters/crayfish_npc_256_256.png differ diff --git a/assets/ui/world_text_theme.tres b/assets/ui/world_text_theme.tres new file mode 100644 index 0000000..3c07470 --- /dev/null +++ b/assets/ui/world_text_theme.tres @@ -0,0 +1,7 @@ +[gd_resource type="Theme" load_steps=2 format=3] + +[ext_resource type="FontFile" uid="uid://ce7ujbeobblyr" path="res://assets/fonts/msyh.ttc" id="1_font"] + +[resource] +resource_local_to_scene = true +default_font = ExtResource("1_font") diff --git a/scenes/Maps/datawhale_home.tscn b/scenes/Maps/datawhale_home.tscn index cb953ca..877056b 100644 --- a/scenes/Maps/datawhale_home.tscn +++ b/scenes/Maps/datawhale_home.tscn @@ -10,4 +10,4 @@ scale = Vector2(1.2916666, 1.2812501) texture = ExtResource("1_xrxds") [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="."] -polygon = PackedVector2Array(-216, -96, -216, -96, 200, -96, 200, -32, 192, -32, 192, 0, 72, 0, 72, -16, 48, -16, 48, -32, 48, -40, -40, -40, -48, -24, -64, -16, -72, -16, -80, 0, -200, 0, -200, -32, -216, -32) +polygon = PackedVector2Array(-216, -96, -216, -96, 200, -96, 200, -32, 192, -32, 192, -8, 77, -8, 72, -16, 48, -16, 48, -32, 48, -40, -40, -40, -48, -24, -64, -16, -72, -16, -80, -5, -201, -6, -200, -32, -216, -32) diff --git a/scenes/Maps/square.tscn b/scenes/Maps/square.tscn index c09c540..9a13286 100644 --- a/scenes/Maps/square.tscn +++ b/scenes/Maps/square.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=40 format=4 uid="uid://5cc0c6cpnhe8"] +[gd_scene load_steps=41 format=4 uid="uid://5cc0c6cpnhe8"] [ext_resource type="Script" uid="uid://b43tvo8cykfrq" path="res://scenes/Maps/BaseLevel.gd" id="1_m4als"] [ext_resource type="Texture2D" uid="uid://baa5wkuyqouh6" path="res://assets/sprites/environment/standard_brick_128_128.jpg" id="1_rb5kq"] @@ -19,6 +19,7 @@ [ext_resource type="PackedScene" uid="uid://bvfyllcy5fi8o" path="res://scenes/Maps/datawhale_home.tscn" id="16_m4als"] [ext_resource type="PackedScene" uid="uid://rdmrm7j4iokr" path="res://scenes/prefabs/items/notice_board.tscn" id="16_rixdf"] [ext_resource type="Script" uid="uid://d2od22agputjt" path="res://scenes/prefabs/items/WelcomeBoard.gd" id="16_u1t8b"] +[ext_resource type="PackedScene" path="res://scenes/characters/crayfish_npc.tscn" id="17_crayfish"] [ext_resource type="Script" uid="uid://rlkavptfhr4y" path="res://scenes/Maps/DoorTeleport.gd" id="18_0xqio"] [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_7nixu"] @@ -1066,7 +1067,7 @@ tile_set = SubResource("TileSet_3vk77") z_index = -1 position = Vector2(-815.00006, 480) scale = Vector2(0.4999431, 0.4999431) -tile_map_data = PackedByteArray("AAD3/wsABgAAAAsAAAD3/w0ABgAAAA0AAAD3/w4ABgAAAA4AAAD3/w8ABgAAAA8AAAD4/wsABgABAAsAAAD4/w0ABgABAA0AAAD4/w4ABgABAA4AAAD4/w8ABgABAA8AAAD5/wsABgACAAsAAAD5/wwABgACAAwAAAD5/w0ABgACAA0AAAD5/w4ABgACAA4AAAD5/w8ABgACAA8AAAD6/wsABgADAAsAAAD6/wwABgADAAwAAAD6/w0ABgADAA0AAAD6/w4ABgADAA4AAAD6/w8ABgADAA8AAAD7/wsABgAEAAsAAAD7/wwABgAEAAwAAAD7/w0ABgAEAA0AAAD7/w4ABgAEAA4AAAD7/w8ABgAEAA8AAAD8/wsABgAFAAsAAAD8/wwABgAFAAwAAAD8/w0ABgAFAA0AAAD8/w4ABgAFAA4AAAD8/w8ABgAFAA8AAAD9/wsABgAGAAsAAAD9/wwABgAGAAwAAAD9/w0ABgAGAA0AAAD9/w4ABgAGAA4AAAD9/w8ABgAGAA8AAAD+/wsABgAHAAsAAAD+/wwABgAHAAwAAAD+/w0ABgAHAA0AAAD+/w4ABgAHAA4AAAD+/w8ABgAHAA8AAAAGAAsABgAAAAsAABAFAAsABgABAAsAABAEAAsABgACAAsAABADAAsABgADAAsAABACAAsABgAEAAsAABABAAsABgAFAAsAABAAAAsABgAGAAsAABD//wsABgAHAAsAABAGAAwABgAAAAwAABAFAAwABgABAAwAABAEAAwABgACAAwAABADAAwABgADAAwAABACAAwABgAEAAwAABABAAwABgAFAAwAABAAAAwABgAGAAwAABD//wwABgAHAAwAABAGAA0ABgAAAA0AABAFAA0ABgABAA0AABAEAA0ABgACAA0AABADAA0ABgADAA0AABACAA0ABgAEAA0AABABAA0ABgAFAA0AABAAAA0ABgAGAA0AABD//w0ABgAHAA0AABAGAA4ABgAAAA4AABAFAA4ABgABAA4AABAEAA4ABgACAA4AABADAA4ABgADAA4AABACAA4ABgAEAA4AABABAA4ABgAFAA4AABAAAA4ABgAGAA4AABD//w4ABgAHAA4AABAGAA8ABgAAAA8AABAFAA8ABgABAA8AABAEAA8ABgACAA8AABADAA8ABgADAA8AABACAA8ABgAEAA8AABABAA8ABgAFAA8AABAAAA8ABgAGAA8AABD//w8ABgAHAA8AABAOAAsABgAHAAsAAAANAAsABgAGAAsAAAAMAAsABgAFAAsAAAALAAsABgAEAAsAAAAKAAsABgADAAsAAAAJAAsABgACAAsAAAAIAAsABgABAAsAAAAHAAsABgAAAAsAAAAOAAwABgAHAAwAAAANAAwABgAGAAwAAAAMAAwABgAFAAwAAAALAAwABgAEAAwAAAAKAAwABgADAAwAAAAJAAwABgACAAwAAAAIAAwABgABAAwAAAAHAAwABgAAAAwAAAAOAA0ABgAHAA0AAAANAA0ABgAGAA0AAAAMAA0ABgAFAA0AAAALAA0ABgAEAA0AAAAKAA0ABgADAA0AAAAJAA0ABgACAA0AAAAIAA0ABgABAA0AAAAHAA0ABgAAAA0AAAAOAA4ABgAHAA4AAAANAA4ABgAGAA4AAAAMAA4ABgAFAA4AAAALAA4ABgAEAA4AAAAKAA4ABgADAA4AAAAJAA4ABgACAA4AAAAIAA4ABgABAA4AAAAHAA4ABgAAAA4AAAAOAA8ABgAHAA8AAAANAA8ABgAGAA8AAAAMAA8ABgAFAA8AAAALAA8ABgAEAA8AAAAKAA8ABgADAA8AAAAJAA8ABgACAA8AAAAIAA8ABgABAA8AAAAHAA8ABgAAAA8AAAAWAAsABgAAAAsAABAVAAsABgABAAsAABAUAAsABgACAAsAABATAAsABgADAAsAABASAAsABgAEAAsAABARAAsABgAFAAsAABAQAAsABgAGAAsAABAPAAsABgAHAAsAABAWAAwABgAAAAwAABAVAAwABgABAAwAABAUAAwABgACAAwAABATAAwABgADAAwAABASAAwABgAEAAwAABARAAwABgAFAAwAABAQAAwABgAGAAwAABAPAAwABgAHAAwAABAWAA0ABgAAAA0AABAVAA0ABgABAA0AABAUAA0ABgACAA0AABATAA0ABgADAA0AABASAA0ABgAEAA0AABARAA0ABgAFAA0AABAQAA0ABgAGAA0AABAPAA0ABgAHAA0AABAWAA4ABgAAAA4AABAVAA4ABgABAA4AABAUAA4ABgACAA4AABATAA4ABgADAA4AABASAA4ABgAEAA4AABARAA4ABgAFAA4AABAQAA4ABgAGAA4AABAPAA4ABgAHAA4AABAWAA8ABgAAAA8AABAVAA8ABgABAA8AABAUAA8ABgACAA8AABATAA8ABgADAA8AABASAA8ABgAEAA8AABAPAA8ABgAHAA8AABAeAAsABgAHAAsAAAAdAAsABgAGAAsAAAAcAAsABgAFAAsAAAAbAAsABgAEAAsAAAAaAAsABgADAAsAAAAZAAsABgACAAsAAAAYAAsABgABAAsAAAAXAAsABgAAAAsAAAAeAAwABgAHAAwAAAAdAAwABgAGAAwAAAAcAAwABgAFAAwAAAAbAAwABgAEAAwAAAAaAAwABgADAAwAAAAZAAwABgACAAwAAAAYAAwABgABAAwAAAAXAAwABgAAAAwAAAAeAA0ABgAHAA0AAAAdAA0ABgAGAA0AAAAcAA0ABgAFAA0AAAAbAA0ABgAEAA0AAAAaAA0ABgADAA0AAAAZAA0ABgACAA0AAAAYAA0ABgABAA0AAAAXAA0ABgAAAA0AAAAeAA4ABgAHAA4AAAAdAA4ABgAGAA4AAAAcAA4ABgAFAA4AAAAbAA4ABgAEAA4AAAAaAA4ABgADAA4AAAAZAA4ABgACAA4AAAAYAA4ABgABAA4AAAAXAA4ABgAAAA4AAAAeAA8ABgAHAA8AAAAdAA8ABgAGAA8AAAAcAA8ABgAFAA8AAAAbAA8ABgAEAA8AAAAaAA8ABgADAA8AAAAZAA8ABgACAA8AAAAYAA8ABgABAA8AAAAXAA8ABgAAAA8AAAAmAAsABgAAAAsAABAlAAsABgABAAsAABAkAAsABgACAAsAABAjAAsABgADAAsAABAiAAsABgAEAAsAABAhAAsABgAFAAsAABAgAAsABgAGAAsAABAfAAsABgAHAAsAABAmAAwABgAAAAwAABAlAAwABgABAAwAABAkAAwABgACAAwAABAjAAwABgADAAwAABAiAAwABgAEAAwAABAhAAwABgAFAAwAABAgAAwABgAGAAwAABAfAAwABgAHAAwAABAmAA0ABgAAAA0AABAlAA0ABgABAA0AABAkAA0ABgACAA0AABAjAA0ABgADAA0AABAiAA0ABgAEAA0AABAhAA0ABgAFAA0AABAgAA0ABgAGAA0AABAfAA0ABgAHAA0AABAmAA4ABgAAAA4AABAlAA4ABgABAA4AABAkAA4ABgACAA4AABAjAA4ABgADAA4AABAiAA4ABgAEAA4AABAhAA4ABgAFAA4AABAgAA4ABgAGAA4AABAfAA4ABgAHAA4AABAmAA8ABgAAAA8AABAlAA8ABgABAA8AABAkAA8ABgACAA8AABAjAA8ABgADAA8AABAiAA8ABgAEAA8AABAhAA8ABgAFAA8AABAgAA8ABgAGAA8AABAfAA8ABgAHAA8AABAuAAsABgAHAAsAAAAtAAsABgAGAAsAAAAsAAsABgAFAAsAAAArAAsABgAEAAsAAAAqAAsABgADAAsAAAApAAsABgACAAsAAAAoAAsABgABAAsAAAAnAAsABgAAAAsAAAAsAAwABgAFAAwAAAArAAwABgAEAAwAAAAqAAwABgADAAwAAAApAAwABgACAAwAAAAoAAwABgABAAwAAAAnAAwABgAAAAwAAAAsAA0ABgAFAA0AAAArAA0ABgAEAA0AAAAqAA0ABgADAA0AAAApAA0ABgACAA0AAAAoAA0ABgABAA0AAAAnAA0ABgAAAA0AAAAsAA4ABgAFAA4AAAArAA4ABgAEAA4AAAAqAA4ABgADAA4AAAApAA4ABgACAA4AAAAoAA4ABgABAA4AAAAnAA4ABgAAAA4AAAAsAA8ABgAFAA8AAAArAA8ABgAEAA8AAAAqAA8ABgADAA8AAAApAA8ABgACAA8AAAAoAA8ABgABAA8AAAAnAA8ABgAAAA8AAAA8AAsABgAEAAsAAAA7AAsABgADAAsAAAA6AAsABgACAAsAAAA5AAsABgABAAsAAAA4AAsABgAAAAsAAAA3AAsABgAAAAsAAAA+AAwABgAHAAwAAAA9AAwABgAGAAwAAAA8AAwABgAFAAwAAAA7AAwABgAEAAwAAAA6AAwABgADAAwAAAA5AAwABgACAAwAAAA+AA0ABgAHAA0AAAA9AA0ABgAGAA0AAAA8AA0ABgAFAA0AAAA7AA0ABgAEAA0AAAA6AA0ABgADAA0AAAA5AA0ABgACAA0AAAA+AA4ABgAHAA4AAAA9AA4ABgAGAA4AAAA8AA4ABgAFAA4AAAA7AA4ABgAEAA4AAAA6AA4ABgADAA4AAAA5AA4ABgACAA4AAAA+AA8ABgAHAA8AAAA9AA8ABgAGAA8AAAA8AA8ABgAFAA8AAAA7AA8ABgAEAA8AAAA6AA8ABgADAA8AAAA5AA8ABgACAA8AAABGAAsABgAPAAsAAABFAAsABgAOAAsAAABEAAsABgANAAsAAABDAAsABgAMAAsAAABCAAsABgALAAsAAABBAAsABgAKAAsAAABAAAsABgAJAAsAAAA/AAsABgAHAAsAAABFAAwABgAOAAwAAABEAAwABgANAAwAAABDAAwABgAMAAwAAABCAAwABgALAAwAAABBAAwABgAKAAwAAABAAAwABgAJAAwAAAA/AAwABgAIAAwAAABFAA0ABgAOAA0AAABEAA0ABgANAA0AAABDAA0ABgAMAA0AAABCAA0ABgALAA0AAABBAA0ABgAKAA0AAABAAA0ABgAJAA0AAAA/AA0ABgAIAA0AAABFAA4ABgAOAA4AAABEAA4ABgANAA4AAABDAA4ABgAMAA4AAABCAA4ABgALAA4AAABBAA4ABgAKAA4AAABAAA4ABgAJAA4AAAA/AA4ABgAIAA4AAABFAA8ABgAOAA8AAABEAA8ABgANAA8AAABDAA8ABgAMAA8AAABCAA8ABgALAA8AAABBAA8ABgAKAA8AAABAAA8ABgAJAA8AAAA/AA8ABgAIAA8AAABOAAsABgAXAAsAAABNAAsABgAWAAsAAABMAAsABgAVAAsAAABLAAsABgAUAAsAAABKAAsABgATAAsAAABJAAsABgASAAsAAABIAAsABgARAAsAAABHAAsABgAQAAsAAABWAAsABgAYAAsAABBVAAsABgAZAAsAABBUAAsABgAaAAsAABBTAAsABgAbAAsAABBSAAsABgAbAAsAAABRAAsABgAaAAsAAABQAAsABgAZAAsAAABPAAsABgAYAAsAAABeAAsABgAaAAsAABBdAAsABgAbAAsAABBcAAsABgASAAsAABBbAAsABgATAAsAABBaAAsABgAUAAsAABBZAAsABgAVAAsAABBYAAsABgAWAAsAABBXAAsABgAXAAsAABBCAAkABgALAAkAAABCAAoABgALAAoAAABDAAkABgAMAAkAAABDAAoABgAMAAoAAABEAAkABgANAAkAAABEAAoABgANAAoAAABFAAkABgAOAAkAAABFAAoABgAOAAoAAABFAAgABgAOAAgAAABGAAgABgAPAAgAAABGAAkABgAPAAkAAABGAAoABgAPAAoAAABHAAgABgAQAAgAAABHAAkABgAQAAkAAABHAAoABgAQAAoAAABIAAcABgARAAcAAABIAAgABgARAAgAAABIAAkABgARAAkAAABIAAoABgARAAoAAABJAAcABgASAAcAAABJAAgABgASAAgAAABJAAkABgASAAkAAABJAAoABgASAAoAAABKAAcABgATAAcAAABKAAgABgATAAgAAABKAAkABgATAAkAAABKAAoABgATAAoAAABLAAcABgAUAAcAAABLAAgABgAUAAgAAABLAAkABgAUAAkAAABLAAoABgAUAAoAAABMAAcABgAVAAcAAABMAAgABgAVAAgAAABMAAkABgAVAAkAAABMAAoABgAVAAoAAABNAAcABgAWAAcAAABNAAgABgAWAAgAAABNAAkABgAWAAkAAABNAAoABgAWAAoAAABOAAcABgAXAAcAAABOAAgABgAXAAgAAABOAAkABgAXAAkAAABOAAoABgAXAAoAAABPAAcABgAYAAcAAABPAAgABgAYAAgAAABPAAkABgAYAAkAAABPAAoABgAYAAoAAABQAAcABgAZAAcAAABQAAgABgAZAAgAAABQAAkABgAZAAkAAABQAAoABgAZAAoAAABRAAcABgAaAAcAAABRAAgABgAaAAgAAABRAAkABgAaAAkAAABRAAoABgAaAAoAAABSAAcABgAbAAcAAABSAAgABgAbAAgAAABSAAkABgAbAAkAAABSAAoABgAbAAoAAABcAAcABgASAAcAABBbAAcABgATAAcAABBaAAcABgAUAAcAABBZAAcABgAVAAcAABBYAAcABgAWAAcAABBXAAcABgAXAAcAABBWAAcABgAYAAcAABBVAAcABgAZAAcAABBUAAcABgAaAAcAABBTAAcABgAbAAcAABBcAAgABgASAAgAABBbAAgABgATAAgAABBaAAgABgAUAAgAABBZAAgABgAVAAgAABBYAAgABgAWAAgAABBXAAgABgAXAAgAABBWAAgABgAYAAgAABBVAAgABgAZAAgAABBUAAgABgAaAAgAABBTAAgABgAbAAgAABBcAAkABgASAAkAABBbAAkABgATAAkAABBaAAkABgAUAAkAABBZAAkABgAVAAkAABBYAAkABgAWAAkAABBXAAkABgAXAAkAABBWAAkABgAYAAkAABBVAAkABgAZAAkAABBUAAkABgAaAAkAABBTAAkABgAbAAkAABBcAAoABgASAAoAABBbAAoABgATAAoAABBaAAoABgAUAAoAABBZAAoABgAVAAoAABBYAAoABgAWAAoAABBXAAoABgAXAAoAABBWAAoABgAYAAoAABBVAAoABgAZAAoAABBUAAoABgAaAAoAABBTAAoABgAbAAoAABBmAAcABgASAAcAABBlAAcABgATAAcAABBkAAcABgAUAAcAABBjAAcABgAVAAcAABBiAAcABgAWAAcAABBhAAcABgAXAAcAABBgAAcABgAYAAcAABBfAAcABgAZAAcAABBeAAcABgAaAAcAABBdAAcABgAbAAcAABBmAAgABgASAAgAABBlAAgABgATAAgAABBkAAgABgAUAAgAABBjAAgABgAVAAgAABBiAAgABgAWAAgAABBhAAgABgAXAAgAABBgAAgABgAYAAgAABBfAAgABgAZAAgAABBeAAgABgAaAAgAABBdAAgABgAbAAgAABBmAAkABgASAAkAABBlAAkABgATAAkAABBkAAkABgAUAAkAABBjAAkABgAVAAkAABBiAAkABgAWAAkAABBhAAkABgAXAAkAABBgAAkABgAYAAkAABBfAAkABgAZAAkAABBeAAkABgAaAAkAABBdAAkABgAbAAkAABBmAAoABgASAAoAABBlAAoABgATAAoAABBkAAoABgAUAAoAABBjAAoABgAVAAoAABBiAAoABgAWAAoAABBhAAoABgAXAAoAABBgAAoABgAYAAoAABBfAAoABgAZAAoAABBeAAoABgAaAAoAABBdAAoABgAbAAoAABBmAAsABgASAAsAABBlAAsABgATAAsAABBkAAsABgAUAAsAABBjAAsABgAVAAsAABBiAAsABgAWAAsAABBhAAsABgAXAAsAABBgAAsABgAYAAsAABBfAAsABgAZAAsAABBwAAcABgAbAAcAAABvAAcABgAaAAcAAABuAAcABgAZAAcAAABtAAcABgAYAAcAAABsAAcABgAXAAcAAABrAAcABgAWAAcAAABqAAcABgAVAAcAAABpAAcABgAUAAcAAABoAAcABgATAAcAAABnAAcABgASAAcAAABuAAgABgAZAAgAAABtAAgABgAYAAgAAABsAAgABgAXAAgAAABrAAgABgAWAAgAAABqAAgABgAVAAgAAABpAAgABgAUAAgAAABoAAgABgATAAgAAABnAAgABgASAAgAAABuAAkABgAZAAkAAABtAAkABgAYAAkAAABsAAkABgAXAAkAAABrAAkABgAWAAkAAABqAAkABgAVAAkAAABpAAkABgAUAAkAAABoAAkABgATAAkAAABnAAkABgASAAkAAABuAAoABgAZAAoAAABtAAoABgAYAAoAAABsAAoABgAXAAoAAABrAAoABgAWAAoAAABqAAoABgAVAAoAAABpAAoABgAUAAoAAABoAAoABgATAAoAAABnAAoABgASAAoAAABuAAsABgAZAAsAAABtAAsABgAYAAsAAABsAAsABgAXAAsAAABrAAsABgAWAAsAAABqAAsABgAVAAsAAABpAAsABgAUAAsAAABoAAsABgATAAsAAABnAAsABgASAAsAAABGAAwABgAPAAwAAABHAAwABgAQAAwAAABIAA0ABgAQAAwAAABJAA0ABgAQAAwAAABKAA4ABgAQAAwAAABLAA8ABgAQAAwAAABMABAABgAQAAwAAABNABAABgAQAAwAAABOABEABgAQAAwAAABOABIABgAQAAwAAABPABMABgAQAAwAAABQABMABgAQAAwAAABRABMABgAQAAwAAABSABMABgAQAAwAAABTABMABgAQAAwAAABUABMABgAQAAwAAABVABMABgAQAAwAAABIAAwABgAQAAwAAABIAA4ABgAQAAwAAABIAA8ABgAQAAwAAABIABAABgAQAAwAAABIABEABgAQAAwAAABIABIABgAQAAwAAABIABMABgAQAAwAAABJAAwABgAQAAwAAABJAA4ABgAQAAwAAABJAA8ABgAQAAwAAABJABAABgAQAAwAAABJABEABgAQAAwAAABJABIABgAQAAwAAABJABMABgAQAAwAAABKAAwABgAQAAwAAABKAA0ABgAQAAwAAABKAA8ABgAQAAwAAABKABAABgAQAAwAAABKABEABgAQAAwAAABKABIABgAQAAwAAABKABMABgAQAAwAAABLAAwABgAQAAwAAABLAA0ABgAQAAwAAABLAA4ABgAQAAwAAABLABAABgAQAAwAAABLABEABgAQAAwAAABLABIABgAQAAwAAABLABMABgAQAAwAAABMAAwABgAQAAwAAABMAA0ABgAQAAwAAABMAA4ABgAQAAwAAABMAA8ABgAQAAwAAABMABEABgAQAAwAAABMABIABgAQAAwAAABMABMABgAQAAwAAABNAAwABgAQAAwAAABNAA0ABgAQAAwAAABNAA4ABgAQAAwAAABNAA8ABgAQAAwAAABNABEABgAQAAwAAABNABIABgAQAAwAAABNABMABgAQAAwAAABOAAwABgAQAAwAAABOAA0ABgAQAAwAAABOAA4ABgAQAAwAAABOAA8ABgAQAAwAAABOABAABgAQAAwAAABOABMABgAQAAwAAABPAAwABgAQAAwAAABPAA0ABgAQAAwAAABPAA4ABgAQAAwAAABPAA8ABgAQAAwAAABPABAABgAQAAwAAABPABEABgAQAAwAAABPABIABgAQAAwAAABQAAwABgAQAAwAAABQAA0ABgAQAAwAAABQAA4ABgAQAAwAAABQAA8ABgAQAAwAAABQABAABgAQAAwAAABQABEABgAQAAwAAABQABIABgAQAAwAAABRAAwABgAQAAwAAABRAA0ABgAQAAwAAABRAA4ABgAQAAwAAABRAA8ABgAQAAwAAABRABAABgAQAAwAAABRABEABgAQAAwAAABRABIABgAQAAwAAABSAAwABgAQAAwAAABSAA0ABgAQAAwAAABSAA4ABgAQAAwAAABSAA8ABgAQAAwAAABSABAABgAQAAwAAABSABEABgAQAAwAAABSABIABgAQAAwAAABTAAwABgAQAAwAAABTAA0ABgAQAAwAAABTAA4ABgAQAAwAAABTAA8ABgAQAAwAAABTABAABgAQAAwAAABTABEABgAQAAwAAABTABIABgAQAAwAAABUAAwABgAQAAwAAABUAA0ABgAQAAwAAABUAA4ABgAQAAwAAABUAA8ABgAQAAwAAABUABAABgAQAAwAAABUABEABgAQAAwAAABUABIABgAQAAwAAABVAAwABgAQAAwAAABVAA0ABgAQAAwAAABVAA4ABgAQAAwAAABVAA8ABgAQAAwAAABVABAABgAQAAwAAABVABEABgAQAAwAAABVABIABgAQAAwAAABWAAwABgAQAAwAAABWAA0ABgAQAAwAAABWAA4ABgAQAAwAAABWAA8ABgAQAAwAAABWABAABgAQAAwAAABWABEABgAQAAwAAABWABIABgAQAAwAAABWABMABgAQAAwAAABXAAwABgAQAAwAAABXAA0ABgAQAAwAAABXAA4ABgAQAAwAAABXAA8ABgAQAAwAAABXABAABgAQAAwAAABXABEABgAQAAwAAABXABIABgAQAAwAAABXABMABgAQAAwAAABYAAwABgAQAAwAAABYAA0ABgAQAAwAAABYAA4ABgAQAAwAAABYAA8ABgAQAAwAAABYABAABgAQAAwAAABYABEABgAQAAwAAABYABIABgAQAAwAAABYABMABgAQAAwAAABZAAwABgAQAAwAAABZAA0ABgAQAAwAAABZAA4ABgAQAAwAAABZAA8ABgAQAAwAAABZABAABgAQAAwAAABZABEABgAQAAwAAABZABIABgAQAAwAAABZABMABgAQAAwAAABaAAwABgAQAAwAAABaAA0ABgAQAAwAAABaAA4ABgAQAAwAAABaAA8ABgAQAAwAAABaABAABgAQAAwAAABaABEABgAQAAwAAABaABIABgAQAAwAAABaABMABgAQAAwAAABbAAwABgAQAAwAAABbAA0ABgAQAAwAAABbAA4ABgAQAAwAAABbAA8ABgAQAAwAAABbABAABgAQAAwAAABbABEABgAQAAwAAABbABIABgAQAAwAAABbABMABgAQAAwAAABcAAwABgAQAAwAAABcAA0ABgAQAAwAAABcAA4ABgAQAAwAAABcAA8ABgAQAAwAAABcABAABgAQAAwAAABcABEABgAQAAwAAABcABIABgAQAAwAAABcABMABgAQAAwAAABdAAwABgAQAAwAAABdAA0ABgAQAAwAAABdAA4ABgAQAAwAAABdAA8ABgAQAAwAAABdABAABgAQAAwAAABdABEABgAQAAwAAABdABIABgAQAAwAAABdABMABgAQAAwAAABeAAwABgAQAAwAAABeAA0ABgAQAAwAAABeAA4ABgAQAAwAAABeAA8ABgAQAAwAAABeABAABgAQAAwAAABeABEABgAQAAwAAABeABIABgAQAAwAAABeABMABgAQAAwAAABfAAwABgAQAAwAAABfAA0ABgAQAAwAAABfAA4ABgAQAAwAAABfAA8ABgAQAAwAAABfABAABgAQAAwAAABfABEABgAQAAwAAABfABIABgAQAAwAAABfABMABgAQAAwAAABgAAwABgAQAAwAAABgAA0ABgAQAAwAAABgAA4ABgAQAAwAAABgAA8ABgAQAAwAAABgABAABgAQAAwAAABgABEABgAQAAwAAABgABIABgAQAAwAAABgABMABgAQAAwAAABhAAwABgAQAAwAAABhAA0ABgAQAAwAAABhAA4ABgAQAAwAAABhAA8ABgAQAAwAAABhABAABgAQAAwAAABhABEABgAQAAwAAABhABIABgAQAAwAAABhABMABgAQAAwAAABiAAwABgAQAAwAAABiAA0ABgAQAAwAAABiAA4ABgAQAAwAAABiAA8ABgAQAAwAAABiABAABgAQAAwAAABiABEABgAQAAwAAABiABIABgAQAAwAAABiABMABgAQAAwAAABjAAwABgAQAAwAAABjAA0ABgAQAAwAAABjAA4ABgAQAAwAAABjAA8ABgAQAAwAAABjABAABgAQAAwAAABjABEABgAQAAwAAABjABIABgAQAAwAAABjABMABgAQAAwAAABkAAwABgAQAAwAAABkAA0ABgAQAAwAAABkAA4ABgAQAAwAAABkAA8ABgAQAAwAAABkABAABgAQAAwAAABkABEABgAQAAwAAABkABIABgAQAAwAAABkABMABgAQAAwAAABlAAwABgAQAAwAAABlAA0ABgAQAAwAAABlAA4ABgAQAAwAAABlAA8ABgAQAAwAAABlABAABgAQAAwAAABlABEABgAQAAwAAABlABIABgAQAAwAAABlABMABgAQAAwAAABmAAwABgAQAAwAAABmAA0ABgAQAAwAAABmAA4ABgAQAAwAAABmAA8ABgAQAAwAAABmABAABgAQAAwAAABmABEABgAQAAwAAABmABIABgAQAAwAAABmABMABgAQAAwAAABnAAwABgAQAAwAAABnAA0ABgAQAAwAAABnAA4ABgAQAAwAAABnAA8ABgAQAAwAAABnABAABgAQAAwAAABnABEABgAQAAwAAABnABIABgAQAAwAAABnABMABgAQAAwAAABoAAwABgAQAAwAAABoAA0ABgAQAAwAAABoAA4ABgAQAAwAAABoAA8ABgAQAAwAAABoABAABgAQAAwAAABoABEABgAQAAwAAABoABIABgAQAAwAAABoABMABgAQAAwAAABpAAwABgAQAAwAAABpAA0ABgAQAAwAAABpAA4ABgAQAAwAAABpAA8ABgAQAAwAAABpABAABgAQAAwAAABpABEABgAQAAwAAABpABIABgAQAAwAAABpABMABgAQAAwAAABqAAwABgAQAAwAAABqAA0ABgAQAAwAAABqAA4ABgAQAAwAAABqAA8ABgAQAAwAAABqABAABgAQAAwAAABqABEABgAQAAwAAABqABIABgAQAAwAAABqABMABgAQAAwAAABrAAwABgAQAAwAAABrAA0ABgAQAAwAAABrAA4ABgAQAAwAAABrAA8ABgAQAAwAAABrABAABgAQAAwAAABrABEABgAQAAwAAABrABIABgAQAAwAAABrABMABgAQAAwAAABsAAwABgAQAAwAAABsAA0ABgAQAAwAAABsAA4ABgAQAAwAAABsAA8ABgAQAAwAAABsABAABgAQAAwAAABsABEABgAQAAwAAABsABIABgAQAAwAAABsABMABgAQAAwAAABtAAwABgAQAAwAAABtAA0ABgAQAAwAAABtAA4ABgAQAAwAAABtAA8ABgAQAAwAAABtABAABgAQAAwAAABtABEABgAQAAwAAABtABIABgAQAAwAAABtABMABgAQAAwAAABuAAwABgAQAAwAAABuAA0ABgAQAAwAAABuAA4ABgAQAAwAAABuAA8ABgAQAAwAAABuABAABgAQAAwAAABuABEABgAQAAwAAABuABIABgAQAAwAAABuABMABgAQAAwAAABGAA0ABgAQAAwAAABGAA4ABgAQAAwAAABGAA8ABgAQAAwAAABGABAABgAQAAwAAABGABEABgAQAAwAAABGABIABgAQAAwAAABGABMABgAQAAwAAABHAA0ABgAQAAwAAABHAA4ABgAQAAwAAABHAA8ABgAQAAwAAABHABAABgAQAAwAAABHABEABgAQAAwAAABHABIABgAQAAwAAABHABMABgAQAAwAAAA5ABAABgAQAAwAAAA5ABEABgAPAA4AAAA5ABIABgAPAA4AAAA5ABMABgAPAA4AAAA6ABAABgAQAAwAAAA6ABEABgAPAA4AAAA6ABIABgAPAA4AAAA6ABMABgAPAA4AAAA7ABAABgAQAAwAAAA7ABEABgAPAA4AAAA7ABIABgAPAA4AAAA7ABMABgAPAA4AAAA8ABAABgAQAAwAAAA8ABEABgAPAA4AAAA8ABIABgAPAA4AAAA8ABMABgAPAA4AAAA9ABAABgAQAAwAAAA9ABEABgAPAA4AAAA9ABIABgAPAA4AAAA9ABMABgAQAAwAAAA+ABAABgAQAAwAAAA+ABEABgAPAA4AAAA+ABIABgAPAA4AAAA+ABMABgAPAA4AAAA/ABAABgAQAAwAAAA/ABEABgAQAAwAAAA/ABIABgAPAA4AAAA/ABMABgAPAA4AAABAABAABgAQAAwAAABAABEABgAQAAwAAABAABIABgAPAA4AAABAABMABgAPAA4AAABBABAABgAQAAwAAABBABEABgAQAAwAAABBABIABgAQAAwAAABBABMABgAQAAwAAABCABAABgAQAAwAAABCABEABgAQAAwAAABCABIABgAQAAwAAABCABMABgAQAAwAAABDABAABgAQAAwAAABDABEABgAQAAwAAABDABIABgAQAAwAAABDABMABgAQAAwAAABEABAABgAQAAwAAABEABEABgAQAAwAAABEABIABgAQAAwAAABEABMABgAQAAwAAABFABAABgAQAAwAAABFABEABgAQAAwAAABFABIABgAQAAwAAABFABMABgAQAAwAAAD3/xAABgAQAAwAAAD3/xEABgAQAAwAAAD3/xIABgAQAAwAAAD3/xMABgAQAAwAAAD4/xAABgAQAAwAAAD4/xEABgAQAAwAAAD4/xIABgAQAAwAAAD4/xMABgAQAAwAAAD5/xAABgAQAAwAAAD5/xEABgAQAAwAAAD5/xIABgAQAAwAAAD5/xMABgAQAAwAAAD6/xAABgAQAAwAAAD6/xEABgAQAAwAAAD6/xIABgAQAAwAAAD6/xMABgAQAAwAAAD7/xAABgAQAAwAAAD7/xEABgAQAAwAAAD7/xIABgAQAAwAAAD7/xMABgAQAAwAAAD8/xAABgAQAAwAAAD8/xEABgAQAAwAAAD8/xIABgAQAAwAAAD8/xMABgAQAAwAAAD9/xAABgAQAAwAAAD9/xEABgAQAAwAAAD9/xIABgAQAAwAAAD9/xMABgAQAAwAAAD+/xAABgAQAAwAAAD+/xEABgAQAAwAAAD+/xIABgAQAAwAAAD+/xMABgAQAAwAAAD//xAABgAQAAwAAAD//xEABgAQAAwAAAD//xIABgAQAAwAAAD//xMABgAQAAwAAAAAABAABgAQAAwAAAAAABEABgAQAAwAAAAAABIABgAQAAwAAAAAABMABgAQAAwAAAABABEABgAQAAwAAAABABIABgAQAAwAAAABABMABgAQAAwAAAACABAABgAQAAwAAAACABEABgAQAAwAAAACABIABgAQAAwAAAACABMABgAQAAwAAAADABAABgAQAAwAAAADABEABgAQAAwAAAADABIABgAQAAwAAAADABMABgAQAAwAAAAEABAABgAQAAwAAAAEABEABgAQAAwAAAAEABIABgAQAAwAAAAEABMABgAQAAwAAAAFABAABgAQAAwAAAAFABEABgAQAAwAAAAFABIABgAQAAwAAAAFABMABgAQAAwAAAAGABAABgAQAAwAAAAGABEABgAQAAwAAAAGABIABgAQAAwAAAAGABMABgAQAAwAAAAHABAABgAQAAwAAAAHABEABgAQAAwAAAAHABIABgAQAAwAAAAHABMABgAQAAwAAAAIABAABgAQAAwAAAAIABEABgAQAAwAAAAIABIABgAQAAwAAAAIABMABgAQAAwAAAAJABAABgAQAAwAAAAJABEABgAQAAwAAAAJABIABgAQAAwAAAAJABMABgAQAAwAAAAKABAABgAQAAwAAAAKABEABgAQAAwAAAAKABIABgAQAAwAAAAKABMABgAQAAwAAAALABAABgAQAAwAAAALABEABgAQAAwAAAALABIABgAQAAwAAAALABMABgAQAAwAAAAMABAABgAQAAwAAAAMABEABgAQAAwAAAAMABIABgAQAAwAAAAMABMABgAQAAwAAAANABAABgAQAAwAAAANABEABgAQAAwAAAANABIABgAQAAwAAAANABMABgAQAAwAAAAOABAABgAQAAwAAAAOABEABgAQAAwAAAAOABIABgAQAAwAAAAOABMABgAQAAwAAAAPABAABgAQAAwAAAAPABEABgAQAAwAAAAPABIABgAQAAwAAAAPABMABgAQAAwAAAAQABEABgAQAAwAAAAQABIABgAQAAwAAAAQABMABgAQAAwAAAARABEABgAQAAwAAAARABIABgAQAAwAAAARABMABgAQAAwAAAASABAABgAQAAwAAAASABEABgAQAAwAAAASABIABgAQAAwAAAASABMABgAQAAwAAAATABAABgAQAAwAAAATABEABgAQAAwAAAATABIABgAQAAwAAAATABMABgAQAAwAAAAUABAABgAQAAwAAAAUABEABgAQAAwAAAAUABIABgAQAAwAAAAUABMABgAQAAwAAAAVABAABgAQAAwAAAAVABEABgAQAAwAAAAVABIABgAQAAwAAAAVABMABgAQAAwAAAAWABAABgAQAAwAAAAWABEABgAQAAwAAAAWABIABgAQAAwAAAAWABMABgAQAAwAAAAXABAABgAQAAwAAAAXABEABgAQAAwAAAAXABIABgAQAAwAAAAXABMABgAQAAwAAAAYABAABgAQAAwAAAAYABEABgAQAAwAAAAYABIABgAQAAwAAAAYABMABgAQAAwAAAAZABAABgAQAAwAAAAZABEABgAQAAwAAAAZABIABgAQAAwAAAAZABMABgAQAAwAAAAaABAABgAQAAwAAAAaABEABgAQAAwAAAAaABIABgAQAAwAAAAaABMABgAQAAwAAAAbABAABgAQAAwAAAAbABEABgAQAAwAAAAbABIABgAQAAwAAAAbABMABgAQAAwAAAAcABAABgAQAAwAAAAcABEABgAQAAwAAAAcABIABgAQAAwAAAAcABMABgAQAAwAAAAdABAABgAQAAwAAAAdABEABgAQAAwAAAAdABIABgAQAAwAAAAdABMABgAQAAwAAAAeABAABgAQAAwAAAAeABEABgAQAAwAAAAeABIABgAQAAwAAAAeABMABgAQAAwAAAAfABAABgAQAAwAAAAfABEABgAQAAwAAAAfABIABgAQAAwAAAAfABMABgAQAAwAAAAgABAABgAQAAwAAAAgABEABgAQAAwAAAAgABIABgAQAAwAAAAgABMABgAQAAwAAAAhABAABgAQAAwAAAAhABEABgAQAAwAAAAhABIABgAQAAwAAAAhABMABgAQAAwAAAAiABAABgAQAAwAAAAiABEABgAQAAwAAAAiABIABgAQAAwAAAAiABMABgAQAAwAAAAjABAABgAQAAwAAAAjABEABgAQAAwAAAAjABIABgAQAAwAAAAjABMABgAQAAwAAAAkABAABgAQAAwAAAAkABEABgAQAAwAAAAkABIABgAQAAwAAAAkABMABgAQAAwAAAAlABAABgAQAAwAAAAlABEABgAQAAwAAAAlABIABgAQAAwAAAAlABMABgAQAAwAAAAmABAABgAQAAwAAAAmABEABgAQAAwAAAAmABIABgAQAAwAAAAmABMABgAQAAwAAAAnABAABgAQAAwAAAAnABEABgAQAAwAAAAnABIABgAQAAwAAAAnABMABgAQAAwAAAAoABAABgAQAAwAAAAoABEABgAQAAwAAAAoABIABgAQAAwAAAAoABMABgAQAAwAAAApABAABgAQAAwAAAApABEABgAQAAwAAAApABIABgAQAAwAAAApABMABgAQAAwAAAAqABAABgAQAAwAAAAqABEABgAQAAwAAAAqABIABgAQAAwAAAAqABMABgAQAAwAAAArABAABgAQAAwAAAArABEABgAQAAwAAAArABIABgAQAAwAAAArABMABgAPAA4AAAAsABAABgAQAAwAAAAsABEABgAQAAwAAAAsABIABgAPAA4AAAAsABMABgAPAA4AAAA9AAsABgAFAAsAAAA+AAsABgAGAAsAAAA4AAwABgABAAwAAAA4AA0ABgABAA0AAAA4AA4ABgABAA4AAAA4AA8ABgABAA8AAAA4ABAABgAPAA4AAAA4ABEABgAPAA4AAAA4ABIABgAPAA4AAAA4ABMABgAPAA4AAAA3ABMABgAPAA4AAAA3AAwABgAAAAwAAAA3AA0ABgAAAA0AAAA3AA4ABgAAAA4AAAA3AA8ABgAAAA8AAAA3ABAABgAPAA4AAAA3ABEABgAPAA4AAAA3ABIABgAPAA4AAAAtAAwABgAGAAwAAAAtAA0ABgAGAA0AAAAtAA4ABgAGAA4AAAAtAA8ABgAGAA8AAAAtABAABgAQAAwAAAAtABEABgAQAAwAAAAtABIABgAPAA4AAAAtABMABgAPAA4AAAAuAAwABgAHAAwAAAAuAA0ABgAHAA0AAAAuAA4ABgAHAA4AAAAuABEABgAPAA4AAAAuABIABgAPAA4AAAAuABMABgAPAA4AAAABABAABgAQAAwAAAA2AAsABgAIAAsAAAA2AAwABgAIAAwAAAA2AA0ABgAIAA0AAAA2AA4ABgAIAA4AAAA2AA8ABgAIAA8AAAA1AA4ABgAHAA4AAAA1AA8ABgAHAA8AAAA1AAsABgAIAAsAAAA1AAwABgAIAAwAAAA1AA0ABgAIAA0AAAA2ABMABgAPAA4AAAA1ABMABgAPAA4AAAA0ABMABgAPAA4AAAAzABMABgAPAA4AAAAyABMABgAPAA4AAAAxABMABgAPAA4AAAAwABMABgAPAA4AAAAvABMABgAPAA4AAAAvABIABgAPAA4AAAAwABIABgAPAA4AAAAxABIABgAPAA4AAAAyABIABgAPAA4AAAAzABIABgAPAA4AAAA0ABIABgAPAA4AAAA1ABIABgAPAA4AAAA2ABIABgAPAA4AAAA2ABEABgAPAA4AAAA1ABEABgAPAA4AAAA0ABEABgAPAA4AAAAzABEABgAPAA4AAAAyABEABgAPAA4AAAAxABEABgAPAA4AAAAwABEABgAPAA4AAAAvABEABgAPAA4AAAA2ABAABgAPAA4AAAA1ABAABgAPAA4AAAAQAA8ABgAGAA8AABAQABAABgAQAAwAAAARAA8ABgAFAA8AABARABAABgAQAAwAAAAuAA8ABgAHAA8AAAAuABAABgAQAAwAAAD3/wwABgAAAAwAAAD4/wwABgABAAwAAAD3/woABgAAAAsAAAD4/woABgABAAsAAAA=") +tile_map_data = PackedByteArray("AAD3/wsABgAAAAsAAAD3/w0ABgAAAA0AAAD3/w4ABgAAAA4AAAD3/w8ABgAAAA8AAAD4/wsABgABAAsAAAD4/w0ABgABAA0AAAD4/w4ABgABAA4AAAD4/w8ABgABAA8AAAD5/wsABgACAAsAAAD5/wwABgACAAwAAAD5/w0ABgACAA0AAAD5/w4ABgACAA4AAAD5/w8ABgACAA8AAAD6/wsABgADAAsAAAD6/wwABgADAAwAAAD6/w0ABgADAA0AAAD6/w4ABgADAA4AAAD6/w8ABgADAA8AAAD7/wsABgAEAAsAAAD7/wwABgAEAAwAAAD7/w0ABgAEAA0AAAD7/w4ABgAEAA4AAAD7/w8ABgAEAA8AAAD8/wsABgAFAAsAAAD8/wwABgAFAAwAAAD8/w0ABgAFAA0AAAD8/w4ABgAFAA4AAAD8/w8ABgAFAA8AAAD9/wsABgAGAAsAAAD9/wwABgAGAAwAAAD9/w0ABgAGAA0AAAD9/w4ABgAGAA4AAAD9/w8ABgAGAA8AAAD+/wsABgAHAAsAAAD+/wwABgAHAAwAAAD+/w0ABgAHAA0AAAD+/w4ABgAHAA4AAAD+/w8ABgAHAA8AAAAGAAsABgAAAAsAABAFAAsABgABAAsAABAEAAsABgACAAsAABADAAsABgADAAsAABACAAsABgAEAAsAABABAAsABgAFAAsAABAAAAsABgAGAAsAABD//wsABgAHAAsAABAGAAwABgAAAAwAABAFAAwABgABAAwAABAEAAwABgACAAwAABADAAwABgADAAwAABACAAwABgAEAAwAABABAAwABgAFAAwAABAAAAwABgAGAAwAABD//wwABgAHAAwAABAGAA0ABgAAAA0AABAFAA0ABgABAA0AABAEAA0ABgACAA0AABADAA0ABgADAA0AABACAA0ABgAEAA0AABABAA0ABgAFAA0AABAAAA0ABgAGAA0AABD//w0ABgAHAA0AABAGAA4ABgAAAA4AABAFAA4ABgABAA4AABAEAA4ABgACAA4AABADAA4ABgADAA4AABACAA4ABgAEAA4AABABAA4ABgAFAA4AABAAAA4ABgAGAA4AABD//w4ABgAHAA4AABAGAA8ABgAAAA8AABAFAA8ABgABAA8AABAEAA8ABgACAA8AABADAA8ABgADAA8AABACAA8ABgAEAA8AABABAA8ABgAFAA8AABAAAA8ABgAGAA8AABD//w8ABgAHAA8AABAOAAsABgAHAAsAAAANAAsABgAGAAsAAAAMAAsABgAFAAsAAAALAAsABgAEAAsAAAAKAAsABgADAAsAAAAJAAsABgACAAsAAAAIAAsABgABAAsAAAAHAAsABgAAAAsAAAAOAAwABgAHAAwAAAANAAwABgAGAAwAAAAMAAwABgAFAAwAAAALAAwABgAEAAwAAAAKAAwABgADAAwAAAAJAAwABgACAAwAAAAIAAwABgABAAwAAAAHAAwABgAAAAwAAAAOAA0ABgAHAA0AAAANAA0ABgAGAA0AAAAMAA0ABgAFAA0AAAALAA0ABgAEAA0AAAAKAA0ABgADAA0AAAAJAA0ABgACAA0AAAAIAA0ABgABAA0AAAAHAA0ABgAAAA0AAAAOAA4ABgAHAA4AAAANAA4ABgAGAA4AAAAMAA4ABgAFAA4AAAALAA4ABgAEAA4AAAAKAA4ABgADAA4AAAAJAA4ABgACAA4AAAAIAA4ABgABAA4AAAAHAA4ABgAAAA4AAAAOAA8ABgAHAA8AAAANAA8ABgAGAA8AAAAMAA8ABgAFAA8AAAALAA8ABgAEAA8AAAAKAA8ABgADAA8AAAAJAA8ABgACAA8AAAAIAA8ABgABAA8AAAAHAA8ABgAAAA8AAAAWAAsABgAAAAsAABAVAAsABgABAAsAABAUAAsABgACAAsAABATAAsABgADAAsAABASAAsABgAEAAsAABARAAsABgAFAAsAABAQAAsABgAGAAsAABAPAAsABgAHAAsAABAWAAwABgAAAAwAABAVAAwABgABAAwAABAUAAwABgACAAwAABATAAwABgADAAwAABASAAwABgAEAAwAABARAAwABgAFAAwAABAQAAwABgAGAAwAABAPAAwABgAHAAwAABAWAA0ABgAAAA0AABAVAA0ABgABAA0AABAUAA0ABgACAA0AABATAA0ABgADAA0AABASAA0ABgAEAA0AABARAA0ABgAFAA0AABAQAA0ABgAGAA0AABAPAA0ABgAHAA0AABAWAA4ABgAAAA4AABAVAA4ABgABAA4AABAUAA4ABgACAA4AABATAA4ABgADAA4AABASAA4ABgAEAA4AABARAA4ABgAFAA4AABAQAA4ABgAGAA4AABAPAA4ABgAHAA4AABAWAA8ABgAAAA8AABAVAA8ABgABAA8AABAUAA8ABgACAA8AABATAA8ABgADAA8AABASAA8ABgAEAA8AABAPAA8ABgAHAA8AABAeAAsABgAHAAsAAAAdAAsABgAGAAsAAAAcAAsABgAFAAsAAAAbAAsABgAEAAsAAAAaAAsABgADAAsAAAAZAAsABgACAAsAAAAYAAsABgABAAsAAAAXAAsABgAAAAsAAAAeAAwABgAHAAwAAAAdAAwABgAGAAwAAAAcAAwABgAFAAwAAAAbAAwABgAEAAwAAAAaAAwABgADAAwAAAAZAAwABgACAAwAAAAYAAwABgABAAwAAAAXAAwABgAAAAwAAAAeAA0ABgAHAA0AAAAdAA0ABgAGAA0AAAAcAA0ABgAFAA0AAAAbAA0ABgAEAA0AAAAaAA0ABgADAA0AAAAZAA0ABgACAA0AAAAYAA0ABgABAA0AAAAXAA0ABgAAAA0AAAAeAA4ABgAHAA4AAAAdAA4ABgAGAA4AAAAcAA4ABgAFAA4AAAAbAA4ABgAEAA4AAAAaAA4ABgADAA4AAAAZAA4ABgACAA4AAAAYAA4ABgABAA4AAAAXAA4ABgAAAA4AAAAeAA8ABgAHAA8AAAAdAA8ABgAGAA8AAAAcAA8ABgAFAA8AAAAbAA8ABgAEAA8AAAAaAA8ABgADAA8AAAAZAA8ABgACAA8AAAAYAA8ABgABAA8AAAAXAA8ABgAAAA8AAAAmAAsABgAAAAsAABAlAAsABgABAAsAABAkAAsABgACAAsAABAjAAsABgADAAsAABAiAAsABgAEAAsAABAhAAsABgAFAAsAABAgAAsABgAGAAsAABAfAAsABgAHAAsAABAmAAwABgAAAAwAABAlAAwABgABAAwAABAkAAwABgACAAwAABAjAAwABgADAAwAABAiAAwABgAEAAwAABAhAAwABgAFAAwAABAgAAwABgAGAAwAABAfAAwABgAHAAwAABAmAA0ABgAAAA0AABAlAA0ABgABAA0AABAkAA0ABgACAA0AABAjAA0ABgADAA0AABAiAA0ABgAEAA0AABAhAA0ABgAFAA0AABAgAA0ABgAGAA0AABAfAA0ABgAHAA0AABAmAA4ABgAAAA4AABAlAA4ABgABAA4AABAkAA4ABgACAA4AABAjAA4ABgADAA4AABAiAA4ABgAEAA4AABAhAA4ABgAFAA4AABAgAA4ABgAGAA4AABAfAA4ABgAHAA4AABAmAA8ABgAAAA8AABAlAA8ABgABAA8AABAkAA8ABgACAA8AABAjAA8ABgADAA8AABAiAA8ABgAEAA8AABAhAA8ABgAFAA8AABAgAA8ABgAGAA8AABAfAA8ABgAHAA8AABAuAAsABgAHAAsAAAAtAAsABgAGAAsAAAAsAAsABgAFAAsAAAArAAsABgAEAAsAAAAqAAsABgADAAsAAAApAAsABgACAAsAAAAoAAsABgABAAsAAAAnAAsABgAAAAsAAAAsAAwABgAFAAwAAAArAAwABgAEAAwAAAAqAAwABgADAAwAAAApAAwABgACAAwAAAAoAAwABgABAAwAAAAnAAwABgAAAAwAAAAsAA0ABgAFAA0AAAArAA0ABgAEAA0AAAAqAA0ABgADAA0AAAApAA0ABgACAA0AAAAoAA0ABgABAA0AAAAnAA0ABgAAAA0AAAAsAA4ABgAFAA4AAAArAA4ABgAEAA4AAAAqAA4ABgADAA4AAAApAA4ABgACAA4AAAAoAA4ABgABAA4AAAAnAA4ABgAAAA4AAAAsAA8ABgAFAA8AAAArAA8ABgAEAA8AAAAqAA8ABgADAA8AAAApAA8ABgACAA8AAAAoAA8ABgABAA8AAAAnAA8ABgAAAA8AAAA8AAsABgAEAAsAAAA7AAsABgADAAsAAAA6AAsABgACAAsAAAA5AAsABgABAAsAAAA4AAsABgAAAAsAAAA3AAsABgAAAAsAAAA+AAwABgAHAAwAAAA9AAwABgAGAAwAAAA8AAwABgAFAAwAAAA7AAwABgAEAAwAAAA6AAwABgADAAwAAAA5AAwABgACAAwAAAA+AA0ABgAHAA0AAAA9AA0ABgAGAA0AAAA8AA0ABgAFAA0AAAA7AA0ABgAEAA0AAAA6AA0ABgADAA0AAAA5AA0ABgACAA0AAAA+AA4ABgAHAA4AAAA9AA4ABgAGAA4AAAA8AA4ABgAFAA4AAAA7AA4ABgAEAA4AAAA6AA4ABgADAA4AAAA5AA4ABgACAA4AAAA+AA8ABgAHAA8AAAA9AA8ABgAGAA8AAAA8AA8ABgAFAA8AAAA7AA8ABgAEAA8AAAA6AA8ABgADAA8AAAA5AA8ABgACAA8AAABGAAsABgAPAAsAAABFAAsABgAOAAsAAABEAAsABgANAAsAAABDAAsABgAMAAsAAABCAAsABgALAAsAAABBAAsABgAKAAsAAABAAAsABgAJAAsAAAA/AAsABgAHAAsAAABFAAwABgAOAAwAAABEAAwABgANAAwAAABDAAwABgAMAAwAAABCAAwABgALAAwAAABBAAwABgAKAAwAAABAAAwABgAJAAwAAAA/AAwABgAIAAwAAABFAA0ABgAOAA0AAABEAA0ABgANAA0AAABDAA0ABgAMAA0AAABCAA0ABgALAA0AAABBAA0ABgAKAA0AAABAAA0ABgAJAA0AAAA/AA0ABgAIAA0AAABFAA4ABgAOAA4AAABEAA4ABgANAA4AAABDAA4ABgAMAA4AAABCAA4ABgALAA4AAABBAA4ABgAKAA4AAABAAA4ABgAJAA4AAAA/AA4ABgAIAA4AAABFAA8ABgAOAA8AAABEAA8ABgANAA8AAABDAA8ABgAMAA8AAABCAA8ABgALAA8AAABBAA8ABgAKAA8AAABAAA8ABgAJAA8AAAA/AA8ABgAIAA8AAABOAAsABgAXAAsAAABNAAsABgAWAAsAAABMAAsABgAVAAsAAABLAAsABgAUAAsAAABKAAsABgATAAsAAABJAAsABgASAAsAAABIAAsABgARAAsAAABHAAsABgAQAAsAAABWAAsABgAYAAsAABBVAAsABgAZAAsAABBUAAsABgAaAAsAABBTAAsABgAbAAsAABBSAAsABgAbAAsAAABRAAsABgAaAAsAAABQAAsABgAZAAsAAABPAAsABgAYAAsAAABeAAsABgAaAAsAABBdAAsABgAbAAsAABBcAAsABgASAAsAABBbAAsABgATAAsAABBaAAsABgAUAAsAABBZAAsABgAVAAsAABBYAAsABgAWAAsAABBXAAsABgAXAAsAABBCAAkABgALAAkAAABCAAoABgALAAoAAABDAAkABgAMAAkAAABDAAoABgAMAAoAAABEAAkABgANAAkAAABEAAoABgANAAoAAABFAAkABgAOAAkAAABFAAoABgAOAAoAAABFAAgABgAOAAgAAABGAAgABgAPAAgAAABGAAkABgAPAAkAAABGAAoABgAPAAoAAABHAAgABgAQAAgAAABHAAkABgAQAAkAAABHAAoABgAQAAoAAABIAAcABgARAAcAAABIAAgABgARAAgAAABIAAkABgARAAkAAABIAAoABgARAAoAAABJAAcABgASAAcAAABJAAgABgASAAgAAABJAAkABgASAAkAAABJAAoABgASAAoAAABKAAcABgATAAcAAABKAAgABgATAAgAAABKAAkABgATAAkAAABKAAoABgATAAoAAABLAAcABgAUAAcAAABLAAgABgAUAAgAAABLAAkABgAUAAkAAABLAAoABgAUAAoAAABMAAcABgAVAAcAAABMAAgABgAVAAgAAABMAAkABgAVAAkAAABMAAoABgAVAAoAAABNAAcABgAWAAcAAABNAAgABgAWAAgAAABNAAkABgAWAAkAAABNAAoABgAWAAoAAABOAAcABgAXAAcAAABOAAgABgAXAAgAAABOAAkABgAXAAkAAABOAAoABgAXAAoAAABPAAcABgAYAAcAAABPAAgABgAYAAgAAABPAAkABgAYAAkAAABPAAoABgAYAAoAAABQAAcABgAZAAcAAABQAAgABgAZAAgAAABQAAkABgAZAAkAAABQAAoABgAZAAoAAABRAAcABgAaAAcAAABRAAgABgAaAAgAAABRAAkABgAaAAkAAABRAAoABgAaAAoAAABSAAcABgAbAAcAAABSAAgABgAbAAgAAABSAAkABgAbAAkAAABSAAoABgAbAAoAAABcAAcABgASAAcAABBbAAcABgATAAcAABBaAAcABgAUAAcAABBZAAcABgAVAAcAABBYAAcABgAWAAcAABBXAAcABgAXAAcAABBWAAcABgAYAAcAABBVAAcABgAZAAcAABBUAAcABgAaAAcAABBTAAcABgAbAAcAABBcAAgABgASAAgAABBbAAgABgATAAgAABBaAAgABgAUAAgAABBZAAgABgAVAAgAABBYAAgABgAWAAgAABBXAAgABgAXAAgAABBWAAgABgAYAAgAABBVAAgABgAZAAgAABBUAAgABgAaAAgAABBTAAgABgAbAAgAABBcAAkABgASAAkAABBbAAkABgATAAkAABBaAAkABgAUAAkAABBZAAkABgAVAAkAABBYAAkABgAWAAkAABBXAAkABgAXAAkAABBWAAkABgAYAAkAABBVAAkABgAZAAkAABBUAAkABgAaAAkAABBTAAkABgAbAAkAABBcAAoABgASAAoAABBbAAoABgATAAoAABBaAAoABgAUAAoAABBZAAoABgAVAAoAABBYAAoABgAWAAoAABBXAAoABgAXAAoAABBWAAoABgAYAAoAABBVAAoABgAZAAoAABBUAAoABgAaAAoAABBTAAoABgAbAAoAABBmAAcABgASAAcAABBlAAcABgATAAcAABBkAAcABgAUAAcAABBjAAcABgAVAAcAABBiAAcABgAWAAcAABBhAAcABgAXAAcAABBgAAcABgAYAAcAABBfAAcABgAZAAcAABBeAAcABgAaAAcAABBdAAcABgAbAAcAABBmAAgABgASAAgAABBlAAgABgATAAgAABBkAAgABgAUAAgAABBjAAgABgAVAAgAABBiAAgABgAWAAgAABBhAAgABgAXAAgAABBgAAgABgAYAAgAABBfAAgABgAZAAgAABBeAAgABgAaAAgAABBdAAgABgAbAAgAABBmAAkABgASAAkAABBlAAkABgATAAkAABBkAAkABgAUAAkAABBjAAkABgAVAAkAABBiAAkABgAWAAkAABBhAAkABgAXAAkAABBgAAkABgAYAAkAABBfAAkABgAZAAkAABBeAAkABgAaAAkAABBdAAkABgAbAAkAABBmAAoABgASAAoAABBlAAoABgATAAoAABBkAAoABgAUAAoAABBjAAoABgAVAAoAABBiAAoABgAWAAoAABBhAAoABgAXAAoAABBgAAoABgAYAAoAABBfAAoABgAZAAoAABBeAAoABgAaAAoAABBdAAoABgAbAAoAABBmAAsABgASAAsAABBlAAsABgATAAsAABBkAAsABgAUAAsAABBjAAsABgAVAAsAABBiAAsABgAWAAsAABBhAAsABgAXAAsAABBgAAsABgAYAAsAABBfAAsABgAZAAsAABBwAAcABgAbAAcAAABvAAcABgAaAAcAAABuAAcABgAZAAcAAABtAAcABgAYAAcAAABsAAcABgAXAAcAAABrAAcABgAWAAcAAABqAAcABgAVAAcAAABpAAcABgAUAAcAAABoAAcABgATAAcAAABnAAcABgASAAcAAABuAAgABgAZAAgAAABtAAgABgAYAAgAAABsAAgABgAXAAgAAABrAAgABgAWAAgAAABqAAgABgAVAAgAAABpAAgABgAUAAgAAABoAAgABgATAAgAAABnAAgABgASAAgAAABuAAkABgAZAAkAAABtAAkABgAYAAkAAABsAAkABgAXAAkAAABrAAkABgAWAAkAAABqAAkABgAVAAkAAABpAAkABgAUAAkAAABoAAkABgATAAkAAABnAAkABgASAAkAAABuAAoABgAZAAoAAABtAAoABgAYAAoAAABsAAoABgAXAAoAAABrAAoABgAWAAoAAABqAAoABgAVAAoAAABpAAoABgAUAAoAAABoAAoABgATAAoAAABnAAoABgASAAoAAABuAAsABgAZAAsAAABtAAsABgAYAAsAAABsAAsABgAXAAsAAABrAAsABgAWAAsAAABqAAsABgAVAAsAAABpAAsABgAUAAsAAABoAAsABgATAAsAAABnAAsABgASAAsAAABGAAwABgAPAAwAAABHAAwABgAQAAwAAABIAA0ABgAQAAwAAABJAA0ABgAQAAwAAABKAA4ABgAQAAwAAABLAA8ABgAQAAwAAABMABAABgAQAAwAAABNABAABgAQAAwAAABOABEABgAQAAwAAABOABIABgAQAAwAAABPABMABgAQAAwAAABQABMABgAQAAwAAABRABMABgAQAAwAAABSABMABgAQAAwAAABTABMABgAQAAwAAABUABMABgAQAAwAAABVABMABgAQAAwAAABIAAwABgAQAAwAAABIAA4ABgAQAAwAAABIAA8ABgAQAAwAAABIABAABgAQAAwAAABIABEABgAQAAwAAABIABIABgAQAAwAAABIABMABgAQAAwAAABJAAwABgAQAAwAAABJAA4ABgAQAAwAAABJAA8ABgAQAAwAAABJABAABgAQAAwAAABJABEABgAQAAwAAABJABIABgAQAAwAAABJABMABgAQAAwAAABKAAwABgAQAAwAAABKAA0ABgAQAAwAAABKAA8ABgAQAAwAAABKABAABgAQAAwAAABKABEABgAQAAwAAABKABIABgAQAAwAAABKABMABgAQAAwAAABLAAwABgAQAAwAAABLAA0ABgAQAAwAAABLAA4ABgAQAAwAAABLABAABgAQAAwAAABLABEABgAQAAwAAABLABIABgAQAAwAAABLABMABgAQAAwAAABMAAwABgAQAAwAAABMAA0ABgAQAAwAAABMAA4ABgAQAAwAAABMAA8ABgAQAAwAAABMABEABgAQAAwAAABMABIABgAQAAwAAABMABMABgAQAAwAAABNAAwABgAQAAwAAABNAA0ABgAQAAwAAABNAA4ABgAQAAwAAABNAA8ABgAQAAwAAABNABEABgAQAAwAAABNABIABgAQAAwAAABNABMABgAQAAwAAABOAAwABgAQAAwAAABOAA0ABgAQAAwAAABOAA4ABgAQAAwAAABOAA8ABgAQAAwAAABOABAABgAQAAwAAABOABMABgAQAAwAAABPAAwABgAQAAwAAABPAA0ABgAQAAwAAABPAA4ABgAQAAwAAABPAA8ABgAQAAwAAABPABAABgAQAAwAAABPABEABgAQAAwAAABPABIABgAQAAwAAABQAAwABgAQAAwAAABQAA0ABgAQAAwAAABQAA4ABgAQAAwAAABQAA8ABgAQAAwAAABQABAABgAQAAwAAABQABEABgAQAAwAAABQABIABgAQAAwAAABRAAwABgAQAAwAAABRAA0ABgAQAAwAAABRAA4ABgAQAAwAAABRAA8ABgAQAAwAAABRABAABgAQAAwAAABRABEABgAQAAwAAABRABIABgAQAAwAAABSAAwABgAQAAwAAABSAA0ABgAQAAwAAABSAA4ABgAQAAwAAABSAA8ABgAQAAwAAABSABAABgAQAAwAAABSABEABgAQAAwAAABSABIABgAQAAwAAABTAAwABgAQAAwAAABTAA0ABgAQAAwAAABTAA4ABgAQAAwAAABTAA8ABgAQAAwAAABTABAABgAQAAwAAABTABEABgAQAAwAAABTABIABgAQAAwAAABUAAwABgAQAAwAAABUAA0ABgAQAAwAAABUAA4ABgAQAAwAAABUAA8ABgAQAAwAAABUABAABgAQAAwAAABUABEABgAQAAwAAABUABIABgAQAAwAAABVAAwABgAQAAwAAABVAA0ABgAQAAwAAABVAA4ABgAQAAwAAABVAA8ABgAQAAwAAABVABAABgAQAAwAAABVABEABgAQAAwAAABVABIABgAQAAwAAABWAAwABgAQAAwAAABWAA0ABgAQAAwAAABWAA4ABgAQAAwAAABWAA8ABgAQAAwAAABWABAABgAQAAwAAABWABEABgAQAAwAAABWABIABgAQAAwAAABWABMABgAQAAwAAABXAAwABgAQAAwAAABXAA0ABgAQAAwAAABXAA4ABgAQAAwAAABXAA8ABgAQAAwAAABXABAABgAQAAwAAABXABEABgAQAAwAAABXABIABgAQAAwAAABXABMABgAQAAwAAABYAAwABgAQAAwAAABYAA0ABgAQAAwAAABYAA4ABgAQAAwAAABYAA8ABgAQAAwAAABYABAABgAQAAwAAABYABEABgAQAAwAAABYABIABgAQAAwAAABYABMABgAQAAwAAABZAAwABgAQAAwAAABZAA0ABgAQAAwAAABZAA4ABgAQAAwAAABZAA8ABgAQAAwAAABZABAABgAQAAwAAABZABEABgAQAAwAAABZABIABgAQAAwAAABZABMABgAQAAwAAABaAAwABgAQAAwAAABaAA0ABgAQAAwAAABaAA4ABgAQAAwAAABaAA8ABgAQAAwAAABaABAABgAQAAwAAABaABEABgAQAAwAAABaABIABgAQAAwAAABaABMABgAQAAwAAABbAAwABgAQAAwAAABbAA0ABgAQAAwAAABbAA4ABgAQAAwAAABbAA8ABgAQAAwAAABbABAABgAQAAwAAABbABEABgAQAAwAAABbABIABgAQAAwAAABbABMABgAQAAwAAABcAAwABgAQAAwAAABcAA0ABgAQAAwAAABcAA4ABgAQAAwAAABcAA8ABgAQAAwAAABcABAABgAQAAwAAABcABEABgAQAAwAAABcABIABgAQAAwAAABcABMABgAQAAwAAABdAAwABgAQAAwAAABdAA0ABgAQAAwAAABdAA4ABgAQAAwAAABdAA8ABgAQAAwAAABdABAABgAQAAwAAABdABEABgAQAAwAAABdABIABgAQAAwAAABdABMABgAQAAwAAABeAAwABgAQAAwAAABeAA0ABgAQAAwAAABeAA4ABgAQAAwAAABeAA8ABgAQAAwAAABeABAABgAQAAwAAABeABEABgAQAAwAAABeABIABgAQAAwAAABeABMABgAQAAwAAABfAAwABgAQAAwAAABfAA0ABgAQAAwAAABfAA4ABgAQAAwAAABfAA8ABgAQAAwAAABfABAABgAQAAwAAABfABEABgAQAAwAAABfABIABgAQAAwAAABfABMABgAQAAwAAABgAAwABgAQAAwAAABgAA0ABgAQAAwAAABgAA4ABgAQAAwAAABgAA8ABgAQAAwAAABgABAABgAQAAwAAABgABEABgAQAAwAAABgABIABgAQAAwAAABgABMABgAQAAwAAABhAAwABgAQAAwAAABhAA0ABgAQAAwAAABhAA4ABgAQAAwAAABhAA8ABgAQAAwAAABhABAABgAQAAwAAABhABEABgAQAAwAAABhABIABgAQAAwAAABhABMABgAQAAwAAABiAAwABgAQAAwAAABiAA0ABgAQAAwAAABiAA4ABgAQAAwAAABiAA8ABgAQAAwAAABiABAABgAQAAwAAABiABEABgAQAAwAAABiABIABgAQAAwAAABiABMABgAQAAwAAABjAAwABgAQAAwAAABjAA0ABgAQAAwAAABjAA4ABgAQAAwAAABjAA8ABgAQAAwAAABjABAABgAQAAwAAABjABEABgAQAAwAAABjABIABgAQAAwAAABjABMABgAQAAwAAABkAAwABgAQAAwAAABkAA0ABgAQAAwAAABkAA4ABgAQAAwAAABkAA8ABgAQAAwAAABkABAABgAQAAwAAABkABEABgAQAAwAAABkABIABgAQAAwAAABkABMABgAQAAwAAABlAAwABgAQAAwAAABlAA0ABgAQAAwAAABlAA4ABgAQAAwAAABlAA8ABgAQAAwAAABlABAABgAQAAwAAABlABEABgAQAAwAAABlABIABgAQAAwAAABlABMABgAQAAwAAABmAAwABgAQAAwAAABmAA0ABgAQAAwAAABmAA4ABgAQAAwAAABmAA8ABgAQAAwAAABmABAABgAQAAwAAABmABEABgAQAAwAAABmABIABgAQAAwAAABmABMABgAQAAwAAABnAAwABgAQAAwAAABnAA0ABgAQAAwAAABnAA4ABgAQAAwAAABnAA8ABgAQAAwAAABnABAABgAQAAwAAABnABEABgAQAAwAAABnABIABgAQAAwAAABnABMABgAQAAwAAABoAAwABgAQAAwAAABoAA0ABgAQAAwAAABoAA4ABgAQAAwAAABoAA8ABgAQAAwAAABoABAABgAQAAwAAABoABEABgAQAAwAAABoABIABgAQAAwAAABoABMABgAQAAwAAABpAAwABgAQAAwAAABpAA0ABgAQAAwAAABpAA4ABgAQAAwAAABpAA8ABgAQAAwAAABpABAABgAQAAwAAABpABEABgAQAAwAAABpABIABgAQAAwAAABpABMABgAQAAwAAABqAAwABgAQAAwAAABqAA0ABgAQAAwAAABqAA4ABgAQAAwAAABqAA8ABgAQAAwAAABqABAABgAQAAwAAABqABEABgAQAAwAAABqABIABgAQAAwAAABqABMABgAQAAwAAABrAAwABgAQAAwAAABrAA0ABgAQAAwAAABrAA4ABgAQAAwAAABrAA8ABgAQAAwAAABrABAABgAQAAwAAABrABEABgAQAAwAAABrABIABgAQAAwAAABrABMABgAQAAwAAABsAAwABgAQAAwAAABsAA0ABgAQAAwAAABsAA4ABgAQAAwAAABsAA8ABgAQAAwAAABsABAABgAQAAwAAABsABEABgAQAAwAAABsABIABgAQAAwAAABsABMABgAQAAwAAABtAAwABgAQAAwAAABtAA0ABgAQAAwAAABtAA4ABgAQAAwAAABtAA8ABgAQAAwAAABtABAABgAQAAwAAABtABEABgAQAAwAAABtABIABgAQAAwAAABtABMABgAQAAwAAABuAAwABgAQAAwAAABuAA0ABgAQAAwAAABuAA4ABgAQAAwAAABuAA8ABgAQAAwAAABuABAABgAQAAwAAABuABEABgAQAAwAAABuABIABgAQAAwAAABuABMABgAQAAwAAABGAA0ABgAQAAwAAABGAA4ABgAQAAwAAABGAA8ABgAQAAwAAABGABAABgAQAAwAAABGABEABgAQAAwAAABGABIABgAQAAwAAABGABMABgAQAAwAAABHAA0ABgAQAAwAAABHAA4ABgAQAAwAAABHAA8ABgAQAAwAAABHABAABgAQAAwAAABHABEABgAQAAwAAABHABIABgAQAAwAAABHABMABgAQAAwAAAA5ABAABgAQAAwAAAA5ABEABgAPAA4AAAA5ABIABgAPAA4AAAA5ABMABgAPAA4AAAA6ABAABgAQAAwAAAA6ABEABgAPAA4AAAA6ABIABgAPAA4AAAA6ABMABgAPAA4AAAA7ABAABgAQAAwAAAA7ABEABgAPAA4AAAA7ABIABgAPAA4AAAA7ABMABgAPAA4AAAA8ABAABgAQAAwAAAA8ABEABgAPAA4AAAA8ABIABgAPAA4AAAA8ABMABgAPAA4AAAA9ABAABgAQAAwAAAA9ABEABgAPAA4AAAA9ABIABgAPAA4AAAA9ABMABgAQAAwAAAA+ABAABgAQAAwAAAA+ABEABgAPAA4AAAA+ABIABgAPAA4AAAA+ABMABgAPAA4AAAA/ABAABgAQAAwAAAA/ABEABgAQAAwAAAA/ABIABgAPAA4AAAA/ABMABgAPAA4AAABAABAABgAQAAwAAABAABEABgAQAAwAAABAABIABgAPAA4AAABAABMABgAPAA4AAABBABAABgAQAAwAAABBABEABgAQAAwAAABBABIABgAQAAwAAABBABMABgAQAAwAAABCABAABgAQAAwAAABCABEABgAQAAwAAABCABIABgAQAAwAAABCABMABgAQAAwAAABDABAABgAQAAwAAABDABEABgAQAAwAAABDABIABgAQAAwAAABDABMABgAQAAwAAABEABAABgAQAAwAAABEABEABgAQAAwAAABEABIABgAQAAwAAABEABMABgAQAAwAAABFABAABgAQAAwAAABFABEABgAQAAwAAABFABIABgAQAAwAAABFABMABgAQAAwAAAD3/xAABgAQAAwAAAD3/xEABgAQAAwAAAD3/xIABgAQAAwAAAD3/xMABgAQAAwAAAD4/xAABgAQAAwAAAD4/xEABgAQAAwAAAD4/xIABgAQAAwAAAD4/xMABgAQAAwAAAD5/xAABgAQAAwAAAD5/xEABgAQAAwAAAD5/xIABgAQAAwAAAD5/xMABgAQAAwAAAD6/xAABgAQAAwAAAD6/xEABgAQAAwAAAD6/xIABgAQAAwAAAD6/xMABgAQAAwAAAD7/xAABgAQAAwAAAD7/xEABgAQAAwAAAD7/xIABgAQAAwAAAD7/xMABgAQAAwAAAD8/xAABgAQAAwAAAD8/xEABgAQAAwAAAD8/xIABgAQAAwAAAD8/xMABgAQAAwAAAD9/xAABgAQAAwAAAD9/xEABgAQAAwAAAD9/xIABgAQAAwAAAD9/xMABgAQAAwAAAD+/xAABgAQAAwAAAD+/xEABgAQAAwAAAD+/xIABgAQAAwAAAD+/xMABgAQAAwAAAD//xAABgAQAAwAAAD//xEABgAQAAwAAAD//xIABgAQAAwAAAD//xMABgAQAAwAAAAAABAABgAQAAwAAAAAABEABgAQAAwAAAAAABIABgAQAAwAAAAAABMABgAQAAwAAAABABEABgAQAAwAAAABABIABgAQAAwAAAABABMABgAQAAwAAAACABAABgAQAAwAAAACABEABgAQAAwAAAACABIABgAQAAwAAAACABMABgAQAAwAAAADABAABgAQAAwAAAADABEABgAQAAwAAAADABIABgAQAAwAAAADABMABgAQAAwAAAAEABAABgAQAAwAAAAEABEABgAQAAwAAAAEABIABgAQAAwAAAAEABMABgAQAAwAAAAFABAABgAQAAwAAAAFABEABgAQAAwAAAAFABIABgAQAAwAAAAFABMABgAQAAwAAAAGABAABgAQAAwAAAAGABEABgAQAAwAAAAGABIABgAQAAwAAAAGABMABgAQAAwAAAAHABAABgAQAAwAAAAHABEABgAQAAwAAAAHABIABgAQAAwAAAAHABMABgAQAAwAAAAIABAABgAQAAwAAAAIABEABgAQAAwAAAAIABIABgAQAAwAAAAIABMABgAQAAwAAAAJABAABgAQAAwAAAAJABEABgAQAAwAAAAJABIABgAQAAwAAAAJABMABgAQAAwAAAAKABAABgAQAAwAAAAKABEABgAQAAwAAAAKABIABgAQAAwAAAAKABMABgAQAAwAAAALABAABgAQAAwAAAALABEABgAQAAwAAAALABIABgAQAAwAAAALABMABgAQAAwAAAAMABAABgAQAAwAAAAMABEABgAQAAwAAAAMABIABgAQAAwAAAAMABMABgAQAAwAAAANABAABgAQAAwAAAANABEABgAQAAwAAAANABIABgAQAAwAAAANABMABgAQAAwAAAAOABAABgAQAAwAAAAOABEABgAQAAwAAAAOABIABgAQAAwAAAAOABMABgAQAAwAAAAPABAABgAQAAwAAAAPABEABgAQAAwAAAAPABIABgAQAAwAAAAPABMABgAQAAwAAAAQABEABgAQAAwAAAAQABIABgAQAAwAAAAQABMABgAQAAwAAAARABEABgAQAAwAAAARABIABgAQAAwAAAARABMABgAQAAwAAAASABAABgAQAAwAAAASABEABgAQAAwAAAASABIABgAQAAwAAAASABMABgAQAAwAAAATABAABgAQAAwAAAATABEABgAQAAwAAAATABIABgAQAAwAAAATABMABgAQAAwAAAAUABAABgAQAAwAAAAUABEABgAQAAwAAAAUABIABgAQAAwAAAAUABMABgAQAAwAAAAVABAABgAQAAwAAAAVABEABgAQAAwAAAAVABIABgAQAAwAAAAVABMABgAQAAwAAAAWABAABgAQAAwAAAAWABEABgAQAAwAAAAWABIABgAQAAwAAAAWABMABgAQAAwAAAAXABAABgAQAAwAAAAXABEABgAQAAwAAAAXABIABgAQAAwAAAAXABMABgAQAAwAAAAYABAABgAQAAwAAAAYABEABgAQAAwAAAAYABIABgAQAAwAAAAYABMABgAQAAwAAAAZABAABgAQAAwAAAAZABEABgAQAAwAAAAZABIABgAQAAwAAAAZABMABgAQAAwAAAAaABAABgAQAAwAAAAaABEABgAQAAwAAAAaABIABgAQAAwAAAAaABMABgAQAAwAAAAbABAABgAQAAwAAAAbABEABgAQAAwAAAAbABIABgAQAAwAAAAbABMABgAQAAwAAAAcABAABgAQAAwAAAAcABEABgAQAAwAAAAcABIABgAQAAwAAAAcABMABgAQAAwAAAAdABAABgAQAAwAAAAdABEABgAQAAwAAAAdABIABgAQAAwAAAAdABMABgAQAAwAAAAeABAABgAQAAwAAAAeABEABgAQAAwAAAAeABIABgAQAAwAAAAeABMABgAQAAwAAAAfABAABgAQAAwAAAAfABEABgAQAAwAAAAfABIABgAQAAwAAAAfABMABgAQAAwAAAAgABAABgAQAAwAAAAgABEABgAQAAwAAAAgABIABgAQAAwAAAAgABMABgAQAAwAAAAhABAABgAQAAwAAAAhABEABgAQAAwAAAAhABIABgAQAAwAAAAhABMABgAQAAwAAAAiABAABgAQAAwAAAAiABEABgAQAAwAAAAiABIABgAQAAwAAAAiABMABgAQAAwAAAAjABAABgAQAAwAAAAjABEABgAQAAwAAAAjABIABgAQAAwAAAAjABMABgAQAAwAAAAkABAABgAQAAwAAAAkABEABgAQAAwAAAAkABIABgAQAAwAAAAkABMABgAQAAwAAAAlABAABgAQAAwAAAAlABEABgAQAAwAAAAlABIABgAQAAwAAAAlABMABgAQAAwAAAAmABAABgAQAAwAAAAmABEABgAQAAwAAAAmABIABgAQAAwAAAAmABMABgAQAAwAAAAnABAABgAQAAwAAAAnABEABgAQAAwAAAAnABIABgAQAAwAAAAnABMABgAQAAwAAAAoABAABgAQAAwAAAAoABEABgAQAAwAAAAoABIABgAQAAwAAAAoABMABgAQAAwAAAApABAABgAQAAwAAAApABEABgAQAAwAAAApABIABgAQAAwAAAApABMABgAQAAwAAAAqABAABgAQAAwAAAAqABEABgAQAAwAAAAqABIABgAQAAwAAAAqABMABgAQAAwAAAArABAABgAQAAwAAAArABEABgAQAAwAAAArABIABgAQAAwAAAArABMABgAPAA4AAAAsABAABgAQAAwAAAAsABEABgAQAAwAAAAsABIABgAPAA4AAAAsABMABgAPAA4AAAA9AAsABgAFAAsAAAA+AAsABgAGAAsAAAA4AAwABgABAAwAAAA4AA0ABgABAA0AAAA4AA4ABgABAA4AAAA4AA8ABgABAA8AAAA4ABAABgAPAA4AAAA4ABEABgAPAA4AAAA4ABIABgAPAA4AAAA4ABMABgAPAA4AAAA3ABMABgAPAA4AAAA3AAwABgAAAAwAAAA3AA0ABgAAAA0AAAA3AA4ABgAAAA4AAAA3AA8ABgAAAA8AAAA3ABAABgAPAA4AAAA3ABEABgAPAA4AAAA3ABIABgAPAA4AAAAtAAwABgAGAAwAAAAtAA0ABgAGAA0AAAAtAA4ABgAGAA4AAAAtAA8ABgAGAA8AAAAtABAABgAQAAwAAAAtABEABgAQAAwAAAAtABIABgAPAA4AAAAtABMABgAPAA4AAAAuAAwABgAHAAwAAAAuAA0ABgAHAA0AAAAuAA4ABgAHAA4AAAAuABEABgAPAA4AAAAuABIABgAPAA4AAAAuABMABgAPAA4AAAABABAABgAQAAwAAAA2AAsABgAIAAsAAAA2AAwABgAIAAwAAAA2AA0ABgAIAA0AAAA2AA4ABgAIAA4AAAA2AA8ABgAIAA8AAAA1AA4ABgAHAA4AAAA1AA8ABgAHAA8AAAA1AAsABgAIAAsAAAA1AAwABgAIAAwAAAA1AA0ABgAIAA0AAAA2ABMABgAPAA4AAAA1ABMABgAPAA4AAAA0ABMABgAPAA4AAAAzABMABgAPAA4AAAAyABMABgAPAA4AAAAxABMABgAPAA4AAAAwABMABgAPAA4AAAAvABMABgAPAA4AAAAvABIABgAPAA4AAAAwABIABgAPAA4AAAAxABIABgAPAA4AAAAyABIABgAPAA4AAAAzABIABgAPAA4AAAA0ABIABgAPAA4AAAA1ABIABgAPAA4AAAA2ABIABgAPAA4AAAA2ABEABgAPAA4AAAA1ABEABgAPAA4AAAA0ABEABgAPAA4AAAAzABEABgAPAA4AAAAyABEABgAPAA4AAAAxABEABgAPAA4AAAAwABEABgAPAA4AAAAvABEABgAPAA4AAAA2ABAABgAPAA4AAAA1ABAABgAPAA4AAAAQAA8ABgAGAA8AABAQABAABgAQAAwAAAARAA8ABgAFAA8AABARABAABgAQAAwAAAAuAA8ABgAHAA8AAAAuABAABgAQAAwAAAD3/wwABgAAAAwAAAD4/wwABgABAAwAAAD3/woA///////////4/woA//////////8=") tile_set = SubResource("TileSet_dly5q") [node name="deck" type="TileMapLayer" parent="."] @@ -1082,6 +1083,9 @@ position = Vector2(0, 320) [node name="NPC" parent="." instance=ExtResource("15_0xqio")] position = Vector2(-81, -64) +[node name="CrayfishNpc" parent="." instance=ExtResource("17_crayfish")] +position = Vector2(88, 286) + [node name="DataWhaleHome" parent="." instance=ExtResource("16_m4als")] position = Vector2(8, -128) @@ -1108,11 +1112,12 @@ y_sort_enabled = true position = Vector2(646, 3) [node name="NoticeBoard" parent="." instance=ExtResource("16_rixdf")] -position = Vector2(-216, -72) +y_sort_enabled = true +position = Vector2(-216, -42.7) [node name="WelcomeBoard" parent="." instance=ExtResource("16_edt5w")] y_sort_enabled = true -position = Vector2(128, -80) +position = Vector2(128, -35.875) script = ExtResource("16_u1t8b") [node name="Boundaries" type="Node2D" parent="."] diff --git a/scenes/characters/NPCController.gd b/scenes/characters/NPCController.gd index 212afca..d19a1b9 100644 --- a/scenes/characters/NPCController.gd +++ b/scenes/characters/NPCController.gd @@ -1,29 +1,55 @@ extends CharacterBody2D -signal interaction_happened(text) +# ============================================================================ +# 文件名: NPCController.gd +# 作用: 通用 NPC 控制器,负责角色待机表现与交互对话 +# +# 主要功能: +# - 播放 NPC 待机动画 +# - 响应玩家射线交互 +# - 触发聊天气泡与 NPC 对话事件 +# +# 依赖: EventSystem, EventNames, ChatBubble +# 作者: Codex +# 创建时间: 2026-03-10 +# ============================================================================ + +signal interaction_happened(text: String) + +const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn") @export var npc_name: String = "NPC" -@export var dialogue: String = "欢迎来到WhaleTown,我是镇长范鲸晶" +@export_multiline var dialogue: String = "欢迎来到WhaleTown,我是镇长范鲸晶" -func _ready(): - $Sprite2D.texture = preload("res://assets/characters/npc_286_241.png") - $Sprite2D.hframes = 4 - $Sprite2D.vframes = 4 - - # Start Idle Animation - if has_node("AnimationPlayer"): - $AnimationPlayer.play("idle") - - # Ensure interaction layer - collision_layer = 3 # Layer 1 & 2 (Blocking) +@onready var animation_player: AnimationPlayer = $AnimationPlayer + +func _ready() -> void: + # 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。 + if animation_player.has_animation("idle"): + animation_player.play("idle") + + # 保持 NPC 可被玩家射线与角色碰撞识别。 + collision_layer = 3 collision_mask = 3 -func interact(): +# 处理玩家交互,展示气泡并向全局事件系统广播。 +func interact() -> void: show_bubble(dialogue) + EventSystem.emit_event(EventNames.NPC_TALKED, { + "npc": self, + "npc_name": npc_name, + "dialogue": dialogue + }) interaction_happened.emit(dialogue) - return null -func show_bubble(text): - var bubble = preload("res://scenes/ui/ChatBubble.tscn").instantiate() +# 在 NPC 头顶生成一次性聊天气泡。 +# +# 参数: +# text: String - 要展示的对话内容 +func show_bubble(text: String) -> void: + var bubble: Control = CHAT_BUBBLE_SCENE.instantiate() as Control + if bubble == null: + return add_child(bubble) - bubble.set_text(text) + if bubble.has_method("set_text"): + bubble.call("set_text", text) diff --git a/scenes/characters/PlayerController.gd b/scenes/characters/PlayerController.gd index 925037b..6247994 100644 --- a/scenes/characters/PlayerController.gd +++ b/scenes/characters/PlayerController.gd @@ -81,6 +81,7 @@ func _handle_movement(_delta: float) -> void: # 发送移动事件 (如果位置发生明显变化) if velocity.length() > 0: + player_moved.emit(global_position) EventSystem.emit_event(EventNames.PLAYER_MOVED, { "position": global_position }) diff --git a/scenes/characters/crayfish_npc.tscn b/scenes/characters/crayfish_npc.tscn new file mode 100644 index 0000000..3f20bb1 --- /dev/null +++ b/scenes/characters/crayfish_npc.tscn @@ -0,0 +1,65 @@ +[gd_scene load_steps=7 format=3] + +[ext_resource type="Texture2D" path="res://assets/characters/crayfish_npc_256_256.png" id="1_texture"] +[ext_resource type="Script" path="res://scenes/characters/NPCController.gd" id="2_script"] + +[sub_resource type="RectangleShape2D" id="1_shape"] +size = Vector2(44, 22) + +[sub_resource type="Animation" id="2_reset"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Sprite2D:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [0] +} + +[sub_resource type="Animation" id="3_idle"] +resource_name = "idle" +length = 1.2 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Sprite2D:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0.0333333, 0.26666665, 0.4666667, 0.8, 1), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1), +"update": 1, +"values": [2, 1, 0, 4, 5] +} + +[sub_resource type="AnimationLibrary" id="4_library"] +_data = { +&"RESET": SubResource("2_reset"), +&"idle": SubResource("3_idle") +} + +[node name="CrayfishNpc" type="CharacterBody2D"] +script = ExtResource("2_script") +npc_name = "虾小满" +dialogue = "欢迎来到 WhaleTown!我是虾小满,负责看着喷泉边的水路和码头消息。想找热闹的地方,顺着水边走就对啦。" + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("1_texture") +hframes = 4 +vframes = 4 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +light_mask = 5 +visibility_layer = 5 +shape = SubResource("1_shape") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +&"": SubResource("4_library") +} diff --git a/scenes/prefabs/items/notice_board.tscn b/scenes/prefabs/items/notice_board.tscn index 3f03262..c621f9e 100644 --- a/scenes/prefabs/items/notice_board.tscn +++ b/scenes/prefabs/items/notice_board.tscn @@ -1,20 +1,35 @@ -[gd_scene load_steps=4 format=3 uid="uid://rdmrm7j4iokr"] +[gd_scene load_steps=6 format=3 uid="uid://rdmrm7j4iokr"] [ext_resource type="Script" uid="uid://pnlgf420wktn" path="res://scenes/prefabs/items/NoticeBoard.gd" id="1_script"] [ext_resource type="Texture2D" uid="uid://b4aildrnhbpl4" path="res://assets/materials/NoticeBoard.png" id="2_sprite"] -[sub_resource type="RectangleShape2D" id="RectangleShape2D_nb"] -size = Vector2(160, 40) +[sub_resource type="RectangleShape2D" id="RectangleShape2D_left_post"] +size = Vector2(14, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_right_post"] +size = Vector2(14, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_center_bar"] +size = Vector2(125, 4.666666) [node name="NoticeBoard" type="StaticBody2D"] +z_index = 1 scale = Vector2(0.6, 0.6) script = ExtResource("1_script") [node name="Sprite2D" type="Sprite2D" parent="."] -position = Vector2(0, -16) +position = Vector2(0, -45.3) scale = Vector2(0.5, 0.5) texture = ExtResource("2_sprite") -[node name="CollisionShape2D" type="CollisionShape2D" parent="."] -position = Vector2(0, 6.6666665) -shape = SubResource("RectangleShape2D_nb") +[node name="LeftPostCollision" type="CollisionShape2D" parent="."] +position = Vector2(-75, 10) +shape = SubResource("RectangleShape2D_left_post") + +[node name="RightPostCollision" type="CollisionShape2D" parent="."] +position = Vector2(73.33333, 6.666667) +shape = SubResource("RectangleShape2D_right_post") + +[node name="CenterBottomCollision" type="CollisionShape2D" parent="."] +position = Vector2(-2.5, -2.6666667) +shape = SubResource("RectangleShape2D_center_bar") diff --git a/scenes/prefabs/items/welcome_board.tscn b/scenes/prefabs/items/welcome_board.tscn index fcdf14a..4c41ac4 100644 --- a/scenes/prefabs/items/welcome_board.tscn +++ b/scenes/prefabs/items/welcome_board.tscn @@ -1,17 +1,33 @@ -[gd_scene load_steps=3 format=3 uid="uid://c7k8yay002w4"] +[gd_scene load_steps=5 format=3 uid="uid://c7k8yay002w4"] [ext_resource type="Texture2D" uid="uid://v7loa3smfkrd" path="res://assets/materials/WelcomeBoard.png" id="2_sprite"] -[sub_resource type="RectangleShape2D" id="RectangleShape2D_board"] -size = Vector2(112, 18.5) +[sub_resource type="RectangleShape2D" id="RectangleShape2D_left_post"] +size = Vector2(16, 20) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_right_post"] +size = Vector2(16, 20) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_center_bar"] +size = Vector2(76, 4) [node name="WelcomeBoard" type="StaticBody2D"] +z_index = 1 collision_layer = 3 [node name="Sprite2D" type="Sprite2D" parent="."] +position = Vector2(0, -44.125) scale = Vector2(0.25, 0.25) texture = ExtResource("2_sprite") -[node name="CollisionShape2D" type="CollisionShape2D" parent="."] -position = Vector2(0, 14.75) -shape = SubResource("RectangleShape2D_board") +[node name="LeftPostCollision" type="CollisionShape2D" parent="."] +position = Vector2(-49, -10) +shape = SubResource("RectangleShape2D_left_post") + +[node name="RightPostCollision" type="CollisionShape2D" parent="."] +position = Vector2(47, -10) +shape = SubResource("RectangleShape2D_right_post") + +[node name="CenterBottomCollision" type="CollisionShape2D" parent="."] +position = Vector2(-1, -22) +shape = SubResource("RectangleShape2D_center_bar") diff --git a/scenes/ui/ChatBubble.tscn b/scenes/ui/ChatBubble.tscn index 13b13fc..8c06ff8 100644 --- a/scenes/ui/ChatBubble.tscn +++ b/scenes/ui/ChatBubble.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=4 format=3] [ext_resource type="Script" uid="uid://b4aorojcbwkmb" path="res://scenes/ui/ChatBubble.gd" id="1_script"] +[ext_resource type="Theme" path="res://assets/ui/world_text_theme.tres" id="2_theme"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bubble_modern"] bg_color = Color(1, 1, 1, 0.9) @@ -8,6 +9,10 @@ corner_radius_top_left = 5 corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 +content_margin_left = 10.0 +content_margin_top = 6.0 +content_margin_right = 10.0 +content_margin_bottom = 6.0 shadow_color = Color(0, 0, 0, 0.2) shadow_size = 2 @@ -15,6 +20,7 @@ shadow_size = 2 layout_mode = 3 anchors_preset = 0 script = ExtResource("1_script") +theme = ExtResource("2_theme") [node name="PanelContainer" type="PanelContainer" parent="."] layout_mode = 1 @@ -23,10 +29,10 @@ anchor_left = 0.5 anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 -offset_left = -75.0 -offset_top = -60.0 -offset_right = 75.0 -offset_bottom = -20.0 +offset_left = -120.0 +offset_top = -86.0 +offset_right = 120.0 +offset_bottom = -26.0 grow_horizontal = 2 grow_vertical = 0 theme_override_styles/panel = SubResource("StyleBoxFlat_bubble_modern") @@ -34,9 +40,9 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_bubble_modern") [node name="Label" type="Label" parent="PanelContainer"] layout_mode = 2 theme_override_colors/font_color = Color(0.1, 0.1, 0.1, 1) -theme_override_font_sizes/font_size = 8 +theme_override_font_sizes/font_size = 12 text = "..." horizontal_alignment = 1 vertical_alignment = 1 autowrap_mode = 3 -custom_minimum_size = Vector2(150, 0) +custom_minimum_size = Vector2(220, 0) diff --git a/scenes/ui/ChatUI.gd b/scenes/ui/ChatUI.gd index eb4f2ec..8010639 100644 --- a/scenes/ui/ChatUI.gd +++ b/scenes/ui/ChatUI.gd @@ -1,7 +1,7 @@ extends Control # ============================================================================ -# ChatUI.gd - 聊天界面控制器(Enter 显示/隐藏版本) +# ChatUI.gd - 聊天界面控制器(T 唤起 / Enter 发送) # ============================================================================ # 聊天系统的用户界面控制器 # @@ -9,8 +9,10 @@ extends Control # - 显示聊天消息历史 # - 处理用户输入 # - 显示连接状态 -# - Enter 显示/隐藏 + 点击外部取消输入状态 + 5秒自动隐藏 -# - 只有按 Enter 才会取消倒计时 +# - T 唤起聊天框,Enter 发送消息 +# - 点击聊天框外部隐藏聊天框 +# - 失焦后 5 秒自动隐藏 +# - 显示/隐藏带 0.5s 过渡动画 # - Call Down: 通过 EventSystem 订阅聊天事件 # # 使用方式: @@ -46,6 +48,8 @@ extends Control # 聊天消息场景 @onready var chat_message_scene: PackedScene = preload("res://scenes/prefabs/ui/ChatMessage.tscn") +const CHAT_TRANSITION_DURATION: float = 0.5 + # ============================================================================ # 成员变量 # ============================================================================ @@ -59,6 +63,9 @@ var _hide_timer: Timer = null # 是否在输入中(输入时不隐藏) var _is_typing: bool = false +# 显示/隐藏过渡动画 +var _transition_tween: Tween = null + # 当前用户名 var _current_username: String = "" @@ -69,7 +76,7 @@ var _current_username: String = "" # 准备就绪 func _ready() -> void: # 初始隐藏聊天框 - hide_chat() + hide_chat(true) # 创建隐藏计时器 _create_hide_timer() @@ -80,6 +87,9 @@ func _ready() -> void: # 连接 UI 信号 _connect_ui_signals() + # 尽可能保持回车提交后输入框继续编辑状态(Godot 4.6+ 支持该属性) + _enable_keep_editing_on_submit() + # 清理 func _exit_tree() -> void: # 取消事件订阅 @@ -93,30 +103,48 @@ func _exit_tree() -> void: if _hide_timer: _hide_timer.queue_free() + if is_instance_valid(_transition_tween): + _transition_tween.kill() + _transition_tween = null + # ============================================================================ # 输入处理 # ============================================================================ # 处理全局输入 func _input(event: InputEvent) -> void: - # 检查是否按下 Enter 键 - if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER): + if not (event is InputEventKey): + return + + var key_event := event as InputEventKey + if not key_event.pressed or key_event.echo: + return + + # T 键用于唤起聊天(输入框聚焦时不拦截) + if key_event.keycode == KEY_T and not chat_input.has_focus(): + _handle_t_pressed() + return + + # Enter 键用于发送聊天 + if key_event.keycode == KEY_ENTER or key_event.keycode == KEY_KP_ENTER: _handle_enter_pressed() +# 处理 T 键按下 +func _handle_t_pressed() -> void: + if not _is_chat_visible: + show_chat() + + # 延迟聚焦,避免同一输入事件周期冲突 + call_deferred("_grab_input_focus") + # 处理 Enter 键按下 func _handle_enter_pressed() -> void: - # 如果聊天框未显示,显示它 + # 聊天框未显示时不处理 Enter if not _is_chat_visible: - show_chat() - # 使用 call_deferred 避免在同一个事件周期内触发 LineEdit 的 text_submitted 信号 - call_deferred("_grab_input_focus") return - # 如果聊天框已显示且输入框有焦点,检查输入框内容 + # 输入框有焦点时,发送逻辑交给 LineEdit 的 text_submitted 统一处理 if chat_input.has_focus(): - # 如果输入框有内容,发送消息 - if not chat_input.text.is_empty(): - _on_send_button_pressed() return # 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时) @@ -136,11 +164,12 @@ func _gui_input(event: InputEvent) -> void: # 处理点击聊天框外部区域 func _handle_click_outside() -> void: + if not _is_chat_visible: + return + # 检查点击是否在聊天面板外部 if not chat_panel.get_global_rect().has_point(get_global_mouse_position()): - # 延迟释放输入框焦点,避免事件冲突 - if chat_input.has_focus(): - call_deferred("_release_input_focus") + hide_chat() # 延迟释放输入框焦点(由 call_deferred 调用) func _release_input_focus() -> void: @@ -152,20 +181,32 @@ func _release_input_focus() -> void: # ============================================================================ # 显示聊天框 -func show_chat() -> void: +func show_chat(immediate: bool = false) -> void: _is_chat_visible = true - if is_instance_valid(chat_panel): - chat_panel.show() # 停止隐藏计时器 _stop_hide_timer() + if not is_instance_valid(chat_panel): + return + + _stop_transition_tween() + chat_panel.show() + + if immediate: + chat_panel.modulate.a = 1.0 + return + + chat_panel.modulate.a = 0.0 + _transition_tween = create_tween() + _transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) + _transition_tween.tween_property(chat_panel, "modulate:a", 1.0, CHAT_TRANSITION_DURATION) + _transition_tween.finished.connect(_on_show_transition_finished) + # 隐藏聊天框 -func hide_chat() -> void: +func hide_chat(immediate: bool = false) -> void: _is_chat_visible = false _is_typing = false - if is_instance_valid(chat_panel): - chat_panel.hide() if is_instance_valid(chat_input) and chat_input.has_focus(): chat_input.release_focus() @@ -173,6 +214,25 @@ func hide_chat() -> void: # 停止隐藏计时器 _stop_hide_timer() + if not is_instance_valid(chat_panel): + return + + _stop_transition_tween() + + if immediate: + chat_panel.hide() + chat_panel.modulate.a = 1.0 + return + + if not chat_panel.visible: + chat_panel.modulate.a = 1.0 + return + + _transition_tween = create_tween() + _transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) + _transition_tween.tween_property(chat_panel, "modulate:a", 0.0, CHAT_TRANSITION_DURATION) + _transition_tween.finished.connect(_on_hide_transition_finished) + # 创建隐藏计时器 func _create_hide_timer() -> void: _hide_timer = Timer.new() @@ -202,6 +262,20 @@ func _stop_hide_timer() -> void: func _on_hide_timeout() -> void: hide_chat() +func _on_show_transition_finished() -> void: + _transition_tween = null + +func _on_hide_transition_finished() -> void: + _transition_tween = null + if not _is_chat_visible and is_instance_valid(chat_panel): + chat_panel.hide() + chat_panel.modulate.a = 1.0 + +func _stop_transition_tween() -> void: + if is_instance_valid(_transition_tween): + _transition_tween.kill() + _transition_tween = null + # ============================================================================ # UI 事件处理 # ============================================================================ @@ -243,12 +317,30 @@ func _on_send_button_pressed() -> void: # 清空输入框 chat_input.clear() - # 重新聚焦输入框 + # 发送后延迟重新聚焦,避免被 LineEdit 的提交事件在同一帧内抢走焦点 + call_deferred("_focus_input_after_send") + +func _focus_input_after_send() -> void: + if not _is_chat_visible: + return + if not is_instance_valid(chat_input): + return chat_input.grab_focus() # 聊天输入提交(回车键)处理 -func _on_chat_input_submitted(text: String) -> void: +func _on_chat_input_submitted(_text: String) -> void: _on_send_button_pressed() + # 即便提交的是空串,也保持输入焦点,便于连续输入 + call_deferred("_focus_input_after_send") + +func _enable_keep_editing_on_submit() -> void: + if not is_instance_valid(chat_input): + return + + for property in chat_input.get_property_list(): + if property.get("name", "") == "keep_editing_on_text_submit": + chat_input.set("keep_editing_on_text_submit", true) + return # ============================================================================ # 事件订阅(Call Down) @@ -290,10 +382,11 @@ func _on_chat_error(data: Dictionary) -> void: var error_code: String = data.get("error_code", "") var message: String = data.get("message", "") - push_error("ChatUI: [%s] %s" % [error_code, message]) + # 聊天发送失败通常是业务校验(频率/内容)导致,不应按引擎级错误处理。 + push_warning("ChatUI: [%s] %s" % [error_code, message]) # 处理连接状态变化 -func _on_connection_state_changed(data: Dictionary) -> void: +func _on_connection_state_changed(_data: Dictionary) -> void: # 连接状态变化处理(当前不更新UI) pass @@ -319,7 +412,6 @@ func add_message_to_history(from_user: String, content: String, timestamp: float # 每条消息用一行容器包起来,方便左右对齐且不挤在一起 var row := HBoxContainer.new() - row.layout_mode = 2 # 让 VBoxContainer 接管布局,否则会重叠在同一位置 row.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN row.alignment = BoxContainer.ALIGNMENT_END if is_self else BoxContainer.ALIGNMENT_BEGIN diff --git a/scenes/ui/NoticeDialog.gd b/scenes/ui/NoticeDialog.gd index eea43f1..9bd5cd1 100644 --- a/scenes/ui/NoticeDialog.gd +++ b/scenes/ui/NoticeDialog.gd @@ -32,7 +32,7 @@ var current_page = 0 var tween: Tween var mock_pages = [] var _chat_ui: Control -var _chat_ui_prev_mouse_filter: int = Control.MOUSE_FILTER_STOP +var _chat_ui_prev_mouse_filter: Control.MouseFilter = Control.MOUSE_FILTER_STOP var _chat_ui_mouse_disabled: bool = false func _ready(): diff --git a/scenes/ui/WelcomeDialog.gd b/scenes/ui/WelcomeDialog.gd index bf17bb4..5f7bf04 100644 --- a/scenes/ui/WelcomeDialog.gd +++ b/scenes/ui/WelcomeDialog.gd @@ -1,7 +1,7 @@ extends CanvasLayer var _chat_ui: Control -var _chat_ui_prev_mouse_filter: int = Control.MOUSE_FILTER_STOP +var _chat_ui_prev_mouse_filter: Control.MouseFilter = Control.MOUSE_FILTER_STOP var _chat_ui_mouse_disabled: bool = false func _ready(): diff --git a/scenes/ui/notice_dialog.tscn b/scenes/ui/notice_dialog.tscn index 3acdff8..111d03b 100644 --- a/scenes/ui/notice_dialog.tscn +++ b/scenes/ui/notice_dialog.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=3 format=3 uid="uid://rdmro1jxs6ga"] +[gd_scene load_steps=4 format=3 uid="uid://rdmro1jxs6ga"] [ext_resource type="Script" uid="uid://c227m0okmjt2t" path="res://scenes/ui/NoticeDialog.gd" id="1_script"] +[ext_resource type="Theme" path="res://assets/ui/world_text_theme.tres" id="2_theme"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rounded"] bg_color = Color(0.95, 0.95, 0.95, 1) @@ -33,6 +34,7 @@ grow_vertical = 2 [node name="PanelContainer" type="PanelContainer" parent="CenterContainer"] custom_minimum_size = Vector2(480, 420) layout_mode = 2 +theme = ExtResource("2_theme") theme_override_styles/panel = SubResource("StyleBoxFlat_rounded") [node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"] @@ -96,13 +98,13 @@ vertical_alignment = 1 [node name="TextPanel" type="MarginContainer" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer"] layout_mode = 2 size_flags_vertical = 3 -theme_override_constants/margin_left = 16 -theme_override_constants/margin_right = 16 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_right = 20 [node name="ContentLabel" type="RichTextLabel" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer/TextPanel"] layout_mode = 2 theme_override_colors/default_color = Color(0.3, 0.3, 0.3, 1) -theme_override_font_sizes/normal_font_size = 16 +theme_override_font_sizes/normal_font_size = 17 bbcode_enabled = true text = "Announcement Content..." diff --git a/scenes/ui/welcome_dialog.tscn b/scenes/ui/welcome_dialog.tscn index 0ce8033..fbb69db 100644 --- a/scenes/ui/welcome_dialog.tscn +++ b/scenes/ui/welcome_dialog.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=5 format=3 uid="uid://d8mam0n1a3b5"] +[gd_scene load_steps=6 format=3 uid="uid://d8mam0n1a3b5"] -[ext_resource type="Script" uid="uid://cohijfo0yeo34" path="res://scenes/prefabs/ui/WelcomeDialog.gd" id="1_vs5b1"] +[ext_resource type="Script" uid="uid://cohijfo0yeo34" path="res://scenes/ui/WelcomeDialog.gd" id="1_vs5b1"] [ext_resource type="Texture2D" uid="uid://v7loa3smfkrd" path="res://assets/materials/WelcomeBoard.png" id="2_dy5hw"] +[ext_resource type="Theme" path="res://assets/ui/world_text_theme.tres" id="3_theme"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_card"] bg_color = Color(1, 1, 1, 1) @@ -44,6 +45,7 @@ grow_vertical = 2 [node name="PanelContainer" type="PanelContainer" parent="CenterContainer"] custom_minimum_size = Vector2(400, 350) layout_mode = 2 +theme = ExtResource("3_theme") theme_override_styles/panel = SubResource("StyleBoxFlat_card") [node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"] @@ -90,7 +92,7 @@ stretch_mode = 5 [node name="BodyText" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer"] layout_mode = 2 theme_override_colors/font_color = Color(0.3, 0.3, 0.3, 1) -theme_override_font_sizes/font_size = 14 +theme_override_font_sizes/font_size = 16 text = "连接·共生·见证 Datawhale Town —— 学习者的赛博家园与精神坐标。 ✨ 实时广场:看大家都在学什么。 diff --git a/tests/unit/test_chat_ui_input_regressions.gd b/tests/unit/test_chat_ui_input_regressions.gd new file mode 100644 index 0000000..f3576a6 --- /dev/null +++ b/tests/unit/test_chat_ui_input_regressions.gd @@ -0,0 +1,85 @@ +extends SceneTree + +# ChatUI 输入交互回归测试(无需 GUT) +# +# 运行方式(Godot 4.6): +# "D:\technology\biancheng\Godot_v4.6-stable_win64.exe\Godot_v4.6-stable_win64_console.exe" --headless --path . --script tests/unit/test_chat_ui_input_regressions.gd + +var _failures: Array[String] = [] + +func _init() -> void: + call_deferred("_run") + +func _run() -> void: + await _test_t_opens_enter_sends_outside_click_hides() + + if _failures.is_empty(): + print("PASS: test_chat_ui_input_regressions") + quit(0) + return + + for failure in _failures: + push_error(failure) + print("FAIL: test_chat_ui_input_regressions (%d)" % _failures.size()) + quit(1) + +func _test_t_opens_enter_sends_outside_click_hides() -> void: + var packed_scene: PackedScene = load("res://scenes/ui/ChatUI.tscn") + _assert(packed_scene != null, "应能加载 ChatUI.tscn") + if packed_scene == null: + return + + var chat_ui = packed_scene.instantiate() + root.add_child(chat_ui) + await process_frame + await process_frame + + var chat_panel = chat_ui.chat_panel + var chat_input = chat_ui.chat_input + + _assert(not chat_panel.visible, "初始化时聊天框应隐藏") + _assert(not chat_ui._is_chat_visible, "初始化时 _is_chat_visible 应为 false") + + var enter_event := InputEventKey.new() + enter_event.pressed = true + enter_event.keycode = KEY_ENTER + chat_ui._input(enter_event) + await process_frame + _assert(not chat_panel.visible, "聊天框隐藏时按 Enter 不应唤起聊天框") + + var t_event := InputEventKey.new() + t_event.pressed = true + t_event.keycode = KEY_T + chat_ui._input(t_event) + await create_timer(0.7).timeout + + _assert(chat_panel.visible, "按 T 后聊天框应显示") + _assert(chat_ui._is_chat_visible, "按 T 后 _is_chat_visible 应为 true") + _assert(chat_input.has_focus(), "按 T 后输入框应获得焦点") + + chat_input.text = "hello world" + chat_ui._on_chat_input_submitted(chat_input.text) + await process_frame + _assert(chat_input.text.is_empty(), "输入框有内容时按 Enter 应触发发送并清空输入框") + _assert(chat_input.has_focus(), "按 Enter 发送后输入框应自动保持焦点,便于连续输入") + + # 将鼠标移动到左上角,通常在聊天框外 + var viewport := root.get_viewport() + if viewport: + viewport.warp_mouse(Vector2.ZERO) + + var mouse_event := InputEventMouseButton.new() + mouse_event.button_index = MOUSE_BUTTON_LEFT + mouse_event.pressed = true + chat_ui._gui_input(mouse_event) + await create_timer(0.7).timeout + + _assert(not chat_ui._is_chat_visible, "点击聊天框外部后 _is_chat_visible 应为 false") + _assert(not chat_panel.visible, "点击聊天框外部后聊天框应隐藏") + + chat_ui.queue_free() + await process_frame + +func _assert(condition: bool, message: String) -> void: + if not condition: + _failures.append(message) diff --git a/web_assets/index.html b/web_assets/index.html index e71b65e..f85c8e2 100644 --- a/web_assets/index.html +++ b/web_assets/index.html @@ -112,7 +112,7 @@ body {