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 {