forked from datawhale/whale-town-front
合并主场景和个人小屋
This commit is contained in:
@@ -76,7 +76,6 @@ func show_main_game():
|
||||
main_game_ui.visible = true
|
||||
user_label.text = "当前用户: " + current_user
|
||||
update_player_status()
|
||||
print("进入主游戏界面")
|
||||
|
||||
func update_player_status():
|
||||
level_label.text = "等级: " + str(player_level)
|
||||
@@ -87,48 +86,66 @@ func update_player_status():
|
||||
func _on_login_success(username: String):
|
||||
# 登录成功后的处理
|
||||
current_user = username
|
||||
print("用户 ", username, " 登录成功!")
|
||||
|
||||
# 连接到聊天服务器(在进入游戏界面之前)
|
||||
# 注意:token 已在 AuthScene._on_controller_login_success 中设置
|
||||
print("🔌 开始连接聊天服务器...")
|
||||
ChatManager.connect_to_chat_server()
|
||||
|
||||
show_main_game()
|
||||
# 连接到位置同步服务器
|
||||
LocationManager.connect_to_server()
|
||||
|
||||
# 登录成功后隐藏聊天框(需要按Enter才显示)
|
||||
chat_ui.hide_chat()
|
||||
# 直接进入游戏地图(不显示MainGameUI)
|
||||
_setup_game_environment()
|
||||
|
||||
func _on_logout_pressed():
|
||||
# 登出处理
|
||||
current_user = ""
|
||||
|
||||
# 断开聊天服务器连接
|
||||
print("🔌 断开聊天服务器...")
|
||||
ChatManager.disconnect_from_chat_server()
|
||||
|
||||
show_auth_scene()
|
||||
|
||||
# 游戏功能按钮处理
|
||||
func _on_explore_pressed():
|
||||
print("探索小镇功能")
|
||||
show_game_message("🗺️ 探索功能开发中...")
|
||||
|
||||
func _on_inventory_pressed():
|
||||
print("背包功能")
|
||||
show_game_message("🎒 背包功能开发中...")
|
||||
|
||||
func _on_shop_pressed():
|
||||
print("商店功能")
|
||||
show_game_message("🏪 商店功能开发中...")
|
||||
|
||||
func _on_friends_pressed():
|
||||
print("好友功能")
|
||||
show_game_message("👥 好友功能开发中...")
|
||||
|
||||
func show_game_message(message: String):
|
||||
print("游戏消息: ", message)
|
||||
# 这里可以添加UI提示框显示消息
|
||||
func show_game_message(_message: String):
|
||||
# TODO: 用 Toast 或游戏内提示框替代占位实现
|
||||
pass
|
||||
|
||||
# 设置游戏环境(登录后直接加载地图)
|
||||
func _setup_game_environment():
|
||||
# 防止登录切场景时输入状态残留导致角色“卡移动键”
|
||||
_release_movement_actions()
|
||||
|
||||
# 1. 隐藏UI
|
||||
current_state = GameState.MAIN_GAME
|
||||
auth_scene.visible = false
|
||||
main_game_ui.visible = false
|
||||
|
||||
# 2. 隐藏聊天框(需要按Enter才显示)
|
||||
if is_instance_valid(chat_ui) and chat_ui.has_method("hide_chat"):
|
||||
chat_ui.hide_chat()
|
||||
|
||||
# 3. 使用 SceneManager 切换到广场地图
|
||||
SceneManager.change_scene("square", false) # false = 不使用过渡效果
|
||||
|
||||
func _release_movement_actions() -> void:
|
||||
Input.action_release("move_left")
|
||||
Input.action_release("move_right")
|
||||
Input.action_release("move_up")
|
||||
Input.action_release("move_down")
|
||||
Input.flush_buffered_events()
|
||||
|
||||
# 处理全局输入
|
||||
func _input(event):
|
||||
|
||||
287
scenes/Maps/BaseLevel.gd
Normal file
287
scenes/Maps/BaseLevel.gd
Normal file
@@ -0,0 +1,287 @@
|
||||
class_name BaseLevel
|
||||
extends Node2D
|
||||
|
||||
# 基础关卡脚本
|
||||
# 负责处理通用的关卡逻辑,如玩家生成
|
||||
const CHAT_UI_SCENE: PackedScene = preload("res://scenes/ui/ChatUI.tscn")
|
||||
const CHAT_UI_NODE_NAME: String = "ChatUI"
|
||||
const UI_LAYER_NODE_NAME: String = "UILayer"
|
||||
const UI_LAYER_ORDER: int = 10
|
||||
|
||||
func _ready():
|
||||
_ensure_chat_ui()
|
||||
|
||||
# 延时一帧确保所有子节点就绪
|
||||
call_deferred("_spawn_player")
|
||||
|
||||
# 连接到多人会话
|
||||
# 获取当前场景名字作为 Session ID
|
||||
_current_session_id = SceneManager.get_current_scene_name()
|
||||
if _current_session_id == "":
|
||||
_current_session_id = "square"
|
||||
|
||||
# 如果是私人场景,生成唯一的 Session ID (e.g., room_123)
|
||||
if _current_session_id in PRIVATE_SCENES:
|
||||
_current_session_id = _current_session_id + "_" + str(AuthManager.current_user_id)
|
||||
|
||||
# 如果 WebSocket 已连接,直接加入
|
||||
if LocationManager._socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
||||
|
||||
_join_session_with_player(_current_session_id)
|
||||
else:
|
||||
# 否则等待连接成功信号
|
||||
LocationManager.connected_to_server.connect(func(): _join_session_with_player(_current_session_id))
|
||||
|
||||
# 连接远程玩家相关信号
|
||||
LocationManager.session_joined.connect(_on_session_joined)
|
||||
LocationManager.user_joined.connect(_on_user_joined)
|
||||
LocationManager.user_left.connect(_on_user_left)
|
||||
LocationManager.position_updated.connect(_on_position_updated)
|
||||
|
||||
func _ensure_chat_ui() -> void:
|
||||
var ui_layer := get_node_or_null(UI_LAYER_NODE_NAME) as CanvasLayer
|
||||
if ui_layer == null:
|
||||
ui_layer = CanvasLayer.new()
|
||||
ui_layer.name = UI_LAYER_NODE_NAME
|
||||
ui_layer.layer = UI_LAYER_ORDER
|
||||
add_child(ui_layer)
|
||||
|
||||
var chat_ui := ui_layer.get_node_or_null(CHAT_UI_NODE_NAME)
|
||||
if chat_ui == null:
|
||||
chat_ui = CHAT_UI_SCENE.instantiate()
|
||||
chat_ui.name = CHAT_UI_NODE_NAME
|
||||
ui_layer.add_child(chat_ui)
|
||||
|
||||
if chat_ui.has_method("hide_chat"):
|
||||
chat_ui.call_deferred("hide_chat")
|
||||
|
||||
var remote_players: Dictionary = {} # userId -> RemotePlayer instance
|
||||
var remote_player_scene = preload("res://scenes/characters/remote_player.tscn")
|
||||
var _player_spawned: bool = false # Track if local player has been spawned
|
||||
var _local_player: Node = null # Reference to the local player node
|
||||
var _position_update_timer: float = 0.0 # Throttle timer for position updates
|
||||
var _current_session_id: String = "" # Cache session ID to avoid race condition on exit
|
||||
const POSITION_UPDATE_INTERVAL: float = 0.125 # Send at most 8 times per second
|
||||
const PRIVATE_SCENES = ["room"] # List of scenes that should be private instances
|
||||
|
||||
func _on_session_joined(data: Dictionary):
|
||||
# 对账远程玩家列表,使用 position.mapId 过滤
|
||||
if not data.has("users"):
|
||||
return
|
||||
|
||||
var current_map = _current_session_id
|
||||
if current_map == "":
|
||||
current_map = SceneManager.get_current_scene_name()
|
||||
if current_map == "":
|
||||
current_map = "square"
|
||||
|
||||
# 1. 收集服务器返回的、且 mapId 匹配当前场景的用户ID
|
||||
var valid_user_ids: Array = []
|
||||
var valid_users: Dictionary = {} # userId -> user data
|
||||
|
||||
for user in data.users:
|
||||
if not user.has("userId") or str(user.userId) == str(AuthManager.current_user_id):
|
||||
continue
|
||||
|
||||
# 检查 position.mapId 是否匹配当前场景
|
||||
var user_map_id = ""
|
||||
if user.has("position") and user.position != null:
|
||||
if typeof(user.position) == TYPE_DICTIONARY and user.position.has("mapId"):
|
||||
user_map_id = str(user.position.mapId)
|
||||
|
||||
if user_map_id == current_map:
|
||||
var uid = str(user.userId)
|
||||
valid_user_ids.append(uid)
|
||||
valid_users[uid] = user
|
||||
|
||||
# 2. 清理幽灵:移除本地有但不在有效列表中的玩家
|
||||
var local_user_ids = remote_players.keys()
|
||||
for user_id in local_user_ids:
|
||||
if str(user_id) not in valid_user_ids:
|
||||
_remove_remote_player(user_id)
|
||||
|
||||
# 3. 添加或更新有效的玩家
|
||||
for uid in valid_user_ids:
|
||||
var user = valid_users[uid]
|
||||
if remote_players.has(uid):
|
||||
# 已存在,更新位置
|
||||
_update_remote_player_position(user)
|
||||
else:
|
||||
_add_remote_player(user)
|
||||
|
||||
func _on_user_joined(data: Dictionary):
|
||||
var user = data.get("user", {})
|
||||
if user.has("userId"):
|
||||
if user.userId == AuthManager.current_user_id:
|
||||
return
|
||||
|
||||
# 将 position 数据合并到 user 字典中,以便 _add_remote_player 统一处理
|
||||
if data.has("position"):
|
||||
user["position"] = data.position
|
||||
|
||||
_add_remote_player(user)
|
||||
|
||||
func _on_user_left(data: Dictionary):
|
||||
var user_id = data.get("userId")
|
||||
if user_id:
|
||||
_remove_remote_player(user_id)
|
||||
|
||||
func _on_position_updated(data: Dictionary):
|
||||
var user_id = data.get("userId")
|
||||
|
||||
if user_id and remote_players.has(user_id):
|
||||
var player = remote_players[user_id]
|
||||
# 数据可能直接是位置(扁平)或者包含在 position 字段中
|
||||
# 根据后端协议: { userId:..., position: {x,y...}, ... }
|
||||
var pos_data = data.get("position", {})
|
||||
if pos_data.is_empty():
|
||||
pos_data = data
|
||||
|
||||
# 检查 mapId 是否匹配当前场景
|
||||
if pos_data.has("mapId") and str(pos_data.mapId) != "":
|
||||
var current_map = _current_session_id
|
||||
if current_map == "":
|
||||
current_map = "square"
|
||||
|
||||
if str(pos_data.mapId) != current_map:
|
||||
_remove_remote_player(user_id)
|
||||
return
|
||||
|
||||
if player.has_method("update_position") and pos_data.has("x") and pos_data.has("y"):
|
||||
player.update_position(Vector2(pos_data.x, pos_data.y))
|
||||
|
||||
func _add_remote_player(user_data: Dictionary):
|
||||
var user_id = str(user_data.get("userId", ""))
|
||||
if user_id == "":
|
||||
return
|
||||
|
||||
# 防止重复创建
|
||||
if remote_players.has(user_id):
|
||||
return
|
||||
|
||||
var remote_player = remote_player_scene.instantiate()
|
||||
|
||||
# 使用统一的 setup 方法
|
||||
if remote_player.has_method("setup"):
|
||||
remote_player.setup(user_data)
|
||||
else:
|
||||
# 回退到手动设置位置
|
||||
remote_player.position = Vector2.ZERO
|
||||
if user_data.has("position"):
|
||||
var p = user_data.position
|
||||
if p.has("x") and p.has("y"):
|
||||
remote_player.position = Vector2(p.x, p.y)
|
||||
|
||||
# 添加到场景玩家容器
|
||||
if has_node("Objects/Players"):
|
||||
$Objects/Players.add_child(remote_player)
|
||||
else:
|
||||
add_child(remote_player)
|
||||
|
||||
remote_players[user_id] = remote_player
|
||||
|
||||
func _remove_remote_player(user_id):
|
||||
var uid = str(user_id)
|
||||
if remote_players.has(uid):
|
||||
var player = remote_players[uid]
|
||||
if is_instance_valid(player):
|
||||
player.queue_free()
|
||||
remote_players.erase(uid)
|
||||
|
||||
func _update_remote_player_position(user: Dictionary):
|
||||
var user_id = str(user.get("userId", ""))
|
||||
var player = remote_players.get(user_id)
|
||||
if not player or not is_instance_valid(player):
|
||||
return
|
||||
if user.has("position"):
|
||||
var pos = user.position
|
||||
if pos.has("x") and pos.has("y") and player.has_method("update_position"):
|
||||
player.update_position(Vector2(pos.x, pos.y))
|
||||
|
||||
func _join_session_with_player(session_id: String):
|
||||
# 检查是否有Token,如果没有则等待
|
||||
if LocationManager._auth_token == "":
|
||||
# 轮询等待Token就绪 (简单重试机制)
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_join_session_with_player(session_id)
|
||||
return
|
||||
|
||||
# 等待玩家生成完毕
|
||||
if not _player_spawned or not _local_player:
|
||||
await get_tree().process_frame
|
||||
_join_session_with_player(session_id)
|
||||
return
|
||||
|
||||
var pos = _local_player.global_position if is_instance_valid(_local_player) else Vector2.ZERO
|
||||
|
||||
LocationManager.join_session(session_id, pos)
|
||||
|
||||
# 进入会话后立即同步一次位置
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
LocationManager.send_position_update(session_id, pos)
|
||||
|
||||
func _process(delta):
|
||||
# 发送位置更新 (节流机制)
|
||||
if not _player_spawned or not _local_player:
|
||||
return # Wait for player to be spawned
|
||||
|
||||
if LocationManager._socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
||||
return # WebSocket not connected
|
||||
|
||||
if not is_instance_valid(_local_player):
|
||||
return # Player was freed
|
||||
|
||||
# 检查 velocity 属性
|
||||
if not "velocity" in _local_player:
|
||||
return
|
||||
|
||||
# 只有在移动时才更新计时器和发送
|
||||
if _local_player.velocity.length() > 0:
|
||||
_position_update_timer += delta
|
||||
if _position_update_timer >= POSITION_UPDATE_INTERVAL:
|
||||
_position_update_timer = 0.0
|
||||
var map_id = _current_session_id if _current_session_id != "" else "square"
|
||||
LocationManager.send_position_update(map_id, _local_player.global_position)
|
||||
|
||||
func _spawn_player():
|
||||
# 1. 确定出生位置
|
||||
var spawn_pos = Vector2.ZERO
|
||||
var spawn_name = SceneManager.get_next_spawn_name()
|
||||
|
||||
# 查找逻辑:优先查找名为 spawn_name 的节点,其次找 DefaultSpawn
|
||||
var target_node_name = spawn_name if spawn_name != "" else "DefaultSpawn"
|
||||
var marker_node = null
|
||||
|
||||
# 策略 A: 在 SpawnPoints 容器中查找
|
||||
if has_node("SpawnPoints"):
|
||||
marker_node = $SpawnPoints.get_node_or_null(target_node_name)
|
||||
|
||||
# 策略 B: 如果没找到,在当前节点(根节点)下查找
|
||||
if marker_node == null:
|
||||
marker_node = get_node_or_null(target_node_name)
|
||||
|
||||
# 如果找到了标记点,使用其位置
|
||||
if marker_node:
|
||||
spawn_pos = marker_node.global_position
|
||||
else:
|
||||
# 策略 C: 检查 SceneManager 是否有备用坐标
|
||||
var pos_param = SceneManager.get_next_scene_position()
|
||||
if pos_param != null:
|
||||
spawn_pos = pos_param
|
||||
else:
|
||||
push_warning("BaseLevel: 出生点 '%s' 不存在,使用默认坐标 (0, 0)" % target_node_name)
|
||||
|
||||
# 2. 实例化玩家
|
||||
var player_scene = preload("res://scenes/characters/player.tscn")
|
||||
var player = player_scene.instantiate()
|
||||
player.global_position = spawn_pos
|
||||
|
||||
# 添加到场景
|
||||
# 如果有Objects/Players容器则添加进去,否则直接添加到当前节点
|
||||
if has_node("Objects/Players"):
|
||||
$Objects/Players.add_child(player)
|
||||
else:
|
||||
add_child(player)
|
||||
|
||||
_local_player = player
|
||||
_player_spawned = true
|
||||
1
scenes/Maps/BaseLevel.gd.uid
Normal file
1
scenes/Maps/BaseLevel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b43tvo8cykfrq
|
||||
29
scenes/Maps/DoorTeleport.gd
Normal file
29
scenes/Maps/DoorTeleport.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
extends Area2D
|
||||
|
||||
# 场景名称
|
||||
@export var target_scene_name: String = ""
|
||||
@export var target_position: Vector2 = Vector2.ZERO # 目标场景的生成位置 (Vector2.ZERO 表示不设置)
|
||||
@export var target_spawn_name: String = "" # 目标场景的 Marker2D 名称 (例如 "FromSquare")
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
# 连接 body_entered 信号
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
func _on_body_entered(body: Node2D) -> void:
|
||||
# 检查进入对象是否为玩家
|
||||
if body.has_method("_handle_movement"):
|
||||
_teleport_player()
|
||||
|
||||
func _teleport_player() -> void:
|
||||
if target_scene_name == "":
|
||||
push_error("DoorTeleport: target_scene_name 为空")
|
||||
return
|
||||
|
||||
# 设置参数
|
||||
if target_spawn_name != "":
|
||||
SceneManager.set_next_spawn_name(target_spawn_name)
|
||||
elif target_position != Vector2.ZERO:
|
||||
SceneManager.set_next_scene_position(target_position)
|
||||
|
||||
SceneManager.change_scene(target_scene_name)
|
||||
1
scenes/Maps/DoorTeleport.gd.uid
Normal file
1
scenes/Maps/DoorTeleport.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://rlkavptfhr4y
|
||||
14
scenes/Maps/community.tscn
Normal file
14
scenes/Maps/community.tscn
Normal file
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dscbaqkb1klwl"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://cle66our01dq1" path="res://assets/sprites/environment/community_512_512.png" id="1_jrtph"]
|
||||
|
||||
[node name="Community" type="StaticBody2D"]
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
position = Vector2(4.7683716e-07, -336)
|
||||
scale = Vector2(1.28125, 1.28125)
|
||||
texture = ExtResource("1_jrtph")
|
||||
|
||||
[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="."]
|
||||
position = Vector2(152, 16)
|
||||
polygon = PackedVector2Array(-456, -48, -192, -48, -192, -112, -120, -112, -120, -56, 168, -48, 168, -696, -472, -688, -472, -48)
|
||||
13
scenes/Maps/datawhale_home.tscn
Normal file
13
scenes/Maps/datawhale_home.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bvfyllcy5fi8o"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://bxmbnywn7pd35" path="res://assets/sprites/environment/house_384_256.png" id="1_xrxds"]
|
||||
|
||||
[node name="DataWhaleHome" type="StaticBody2D"]
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
position = Vector2(16, -160)
|
||||
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)
|
||||
14
scenes/Maps/fountain.tscn
Normal file
14
scenes/Maps/fountain.tscn
Normal file
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://vq5qgk3k6t7e"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://dujutnr03apoj" path="res://assets/sprites/environment/fountain_256_192.png" id="1_utxq6"]
|
||||
|
||||
[node name="Fountain" type="StaticBody2D"]
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
position = Vector2(0, -128)
|
||||
scale = Vector2(1.3125001, 1.3061225)
|
||||
texture = ExtResource("1_utxq6")
|
||||
|
||||
[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="."]
|
||||
position = Vector2(-8, 16)
|
||||
polygon = PackedVector2Array(-64, -80, -104, -112, -112, -152, -96, -192, -88, -216, -64, -216, -24, -224, -16, -256, 32, -248, 24, -216, 32, -216, 40, -240, 56, -224, 72, -232, 72, -208, 96, -216, 104, -208, 120, -160, 128, -128, 96, -96, 80, -112, 0, -88, -48, -104, -64, -88)
|
||||
727
scenes/Maps/room.tscn
Normal file
727
scenes/Maps/room.tscn
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
29
scenes/characters/NPCController.gd
Normal file
29
scenes/characters/NPCController.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
signal interaction_happened(text)
|
||||
|
||||
@export var npc_name: String = "NPC"
|
||||
@export 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)
|
||||
collision_mask = 3
|
||||
|
||||
func interact():
|
||||
show_bubble(dialogue)
|
||||
interaction_happened.emit(dialogue)
|
||||
return null
|
||||
|
||||
func show_bubble(text):
|
||||
var bubble = preload("res://scenes/ui/ChatBubble.tscn").instantiate()
|
||||
add_child(bubble)
|
||||
bubble.set_text(text)
|
||||
1
scenes/characters/NPCController.gd.uid
Normal file
1
scenes/characters/NPCController.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dy3uf1rlu4h1u
|
||||
116
scenes/characters/PlayerController.gd
Normal file
116
scenes/characters/PlayerController.gd
Normal file
@@ -0,0 +1,116 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
# 信号定义
|
||||
signal player_moved(position: Vector2)
|
||||
|
||||
# 常量定义
|
||||
const MOVE_SPEED = 200.0
|
||||
|
||||
# 节点引用
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
@onready var ray_cast: RayCast2D = $RayCast2D
|
||||
|
||||
var last_direction := "down"
|
||||
|
||||
func _ready() -> void:
|
||||
_reset_movement_input_state()
|
||||
|
||||
# 检查是否有初始位置设置
|
||||
call_deferred("_check_spawn_position")
|
||||
|
||||
# 播放初始动画
|
||||
if animation_player.has_animation("idle"):
|
||||
animation_player.play("idle")
|
||||
|
||||
# Initialize RayCast
|
||||
ray_cast.add_exception(self) # Ignore local player
|
||||
ray_cast.enabled = true
|
||||
ray_cast.target_position = Vector2(0, 60)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_APPLICATION_FOCUS_IN:
|
||||
_reset_movement_input_state()
|
||||
|
||||
func _reset_movement_input_state() -> void:
|
||||
Input.action_release("move_left")
|
||||
Input.action_release("move_right")
|
||||
Input.action_release("move_up")
|
||||
Input.action_release("move_down")
|
||||
Input.flush_buffered_events()
|
||||
|
||||
func _check_spawn_position() -> void:
|
||||
var spawn_pos = SceneManager.get_next_scene_position()
|
||||
if spawn_pos != null:
|
||||
global_position = spawn_pos
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_handle_movement(delta)
|
||||
_handle_interaction()
|
||||
|
||||
func _handle_interaction() -> void:
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
if ray_cast.is_colliding():
|
||||
var collider = ray_cast.get_collider()
|
||||
if collider and collider.has_method("interact"):
|
||||
collider.interact()
|
||||
|
||||
func _handle_movement(_delta: float) -> void:
|
||||
# 输入框获得焦点时禁止移动,避免聊天/表单输入影响角色
|
||||
if _is_text_input_focused():
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
# 获取移动向量 (参考 docs/02-开发规范/输入映射配置.md)
|
||||
var direction := Input.get_vector(
|
||||
"move_left", "move_right",
|
||||
"move_up", "move_down"
|
||||
)
|
||||
|
||||
# 应用移动
|
||||
if direction != Vector2.ZERO:
|
||||
velocity = direction * MOVE_SPEED
|
||||
_update_animation_state(direction)
|
||||
else:
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
|
||||
move_and_slide()
|
||||
|
||||
# 发送移动事件 (如果位置发生明显变化)
|
||||
if velocity.length() > 0:
|
||||
EventSystem.emit_event(EventNames.PLAYER_MOVED, {
|
||||
"position": global_position
|
||||
})
|
||||
|
||||
func _update_animation_state(direction: Vector2) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
# Determine primary direction
|
||||
if abs(direction.x) > abs(direction.y):
|
||||
if direction.x > 0:
|
||||
last_direction = "right"
|
||||
ray_cast.target_position = Vector2(60, 0)
|
||||
else:
|
||||
last_direction = "left"
|
||||
ray_cast.target_position = Vector2(-60, 0)
|
||||
else:
|
||||
if direction.y > 0:
|
||||
last_direction = "down"
|
||||
ray_cast.target_position = Vector2(0, 60)
|
||||
else:
|
||||
last_direction = "up"
|
||||
ray_cast.target_position = Vector2(0, -60)
|
||||
|
||||
animation_player.play("walk_" + last_direction)
|
||||
|
||||
func _play_idle_animation() -> void:
|
||||
if animation_player:
|
||||
animation_player.play("idle_" + last_direction)
|
||||
|
||||
func _is_text_input_focused() -> bool:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
return focus_owner is LineEdit or focus_owner is TextEdit
|
||||
1
scenes/characters/PlayerController.gd.uid
Normal file
1
scenes/characters/PlayerController.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://fdswi18nel8n
|
||||
82
scenes/characters/RemotePlayer.gd
Normal file
82
scenes/characters/RemotePlayer.gd
Normal file
@@ -0,0 +1,82 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
# 远程玩家脚本
|
||||
# 负责处理位置同步和动画播放
|
||||
# 严格遵循 Visual Only 原则:无输入处理,无物理碰撞
|
||||
|
||||
# 公共属性 (snake_case)
|
||||
var user_id: String = ""
|
||||
var target_position: Vector2 = Vector2.ZERO
|
||||
|
||||
# 内部状态
|
||||
var last_direction: String = "down"
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
|
||||
func _ready():
|
||||
# 初始化时确保无物理处理
|
||||
set_physics_process(false)
|
||||
# 初始位置设为当前位置
|
||||
target_position = global_position
|
||||
|
||||
# 确保禁用物理碰撞 (双重保险)
|
||||
if has_node("CollisionShape2D"):
|
||||
$CollisionShape2D.disabled = true
|
||||
|
||||
func _process(delta: float):
|
||||
# 1. 平滑移动插值
|
||||
var current_pos = global_position
|
||||
var dist = current_pos.distance_to(target_position)
|
||||
|
||||
if dist > 1.0:
|
||||
# 简单的线性插值,速度系数 10.0 可根据需要调整
|
||||
var new_pos = current_pos.lerp(target_position, 10.0 * delta)
|
||||
|
||||
# 计算移动向量用于动画朝向
|
||||
var move_vec = new_pos - current_pos
|
||||
_update_animation(move_vec)
|
||||
|
||||
global_position = new_pos
|
||||
else:
|
||||
# 距离很近时直接吸附并播放待机动画
|
||||
global_position = target_position
|
||||
_play_idle_animation()
|
||||
|
||||
# 统一初始化方法
|
||||
# data: 包含 camelCase 字段的字典 (userId, username, position 等)
|
||||
func setup(data: Dictionary):
|
||||
if data.has("userId"):
|
||||
user_id = data.userId
|
||||
|
||||
if data.has("position"):
|
||||
var pos_data = data.position
|
||||
if pos_data.has("x") and pos_data.has("y"):
|
||||
var new_pos = Vector2(pos_data.x, pos_data.y)
|
||||
global_position = new_pos
|
||||
target_position = new_pos
|
||||
|
||||
# 更新目标位置
|
||||
func update_position(new_pos: Vector2):
|
||||
target_position = new_pos
|
||||
|
||||
func _update_animation(move_vec: Vector2):
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
# 确定主方向
|
||||
if abs(move_vec.x) > abs(move_vec.y):
|
||||
if move_vec.x > 0:
|
||||
last_direction = "right"
|
||||
else:
|
||||
last_direction = "left"
|
||||
else:
|
||||
if move_vec.y > 0:
|
||||
last_direction = "down"
|
||||
else:
|
||||
last_direction = "up"
|
||||
|
||||
animation_player.play("walk_" + last_direction)
|
||||
|
||||
func _play_idle_animation():
|
||||
if animation_player:
|
||||
animation_player.play("idle_" + last_direction)
|
||||
1
scenes/characters/RemotePlayer.gd.uid
Normal file
1
scenes/characters/RemotePlayer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtbajfsljdht5
|
||||
64
scenes/characters/npc.tscn
Normal file
64
scenes/characters/npc.tscn
Normal file
@@ -0,0 +1,64 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://npc2282a2new"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://brko2ik6t6ib5" path="res://assets/characters/npc_286_241.png" id="1_2r34a"]
|
||||
[ext_resource type="Script" uid="uid://dy3uf1rlu4h1u" path="res://scenes/characters/NPCController.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_npc"]
|
||||
size = Vector2(48, 24)
|
||||
|
||||
[sub_resource type="Animation" id="Animation_2r34a"]
|
||||
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="Animation_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="AnimationLibrary_npc"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_2r34a"),
|
||||
&"idle": SubResource("Animation_idle")
|
||||
}
|
||||
|
||||
[node name="NPC" type="CharacterBody2D"]
|
||||
position = Vector2(-8, 0)
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture = ExtResource("1_2r34a")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
light_mask = 5
|
||||
visibility_layer = 5
|
||||
shape = SubResource("RectangleShape2D_npc")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_npc")
|
||||
}
|
||||
1
scenes/characters/player.gd.uid
Normal file
1
scenes/characters/player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://btka26hrcvgen
|
||||
179
scenes/characters/player.tscn
Normal file
179
scenes/characters/player.tscn
Normal file
@@ -0,0 +1,179 @@
|
||||
[gd_scene load_steps=13 format=3 uid="uid://b2f8e24plwqgj"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://fdswi18nel8n" path="res://scenes/characters/PlayerController.gd" id="1_script"]
|
||||
[ext_resource type="Texture2D" uid="uid://cghab1hkx5lg5" path="res://assets/characters/player_spritesheet.png" id="2_texture"]
|
||||
|
||||
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_1"]
|
||||
radius = 21.0
|
||||
height = 48.0
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_down"]
|
||||
resource_name = "idle_down"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_left"]
|
||||
resource_name = "idle_left"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [12]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_right"]
|
||||
resource_name = "idle_right"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [8]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_up"]
|
||||
resource_name = "idle_up"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [4]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_down"]
|
||||
resource_name = "walk_down"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [0, 1, 2, 3]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_left"]
|
||||
resource_name = "walk_left"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [12, 13, 14, 15]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_right"]
|
||||
resource_name = "walk_right"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [8, 9, 10, 11]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_up"]
|
||||
resource_name = "walk_up"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [4, 5, 6, 7]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_1"]
|
||||
_data = {
|
||||
&"idle_down": SubResource("Animation_idle_down"),
|
||||
&"idle_left": SubResource("Animation_idle_left"),
|
||||
&"idle_right": SubResource("Animation_idle_right"),
|
||||
&"idle_up": SubResource("Animation_idle_up"),
|
||||
&"walk_down": SubResource("Animation_walk_down"),
|
||||
&"walk_left": SubResource("Animation_walk_left"),
|
||||
&"walk_right": SubResource("Animation_walk_right"),
|
||||
&"walk_up": SubResource("Animation_walk_up")
|
||||
}
|
||||
|
||||
[node name="Player" type="CharacterBody2D"]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
position = Vector2(1.5000005, -24.5)
|
||||
texture = ExtResource("2_texture")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(2, -24)
|
||||
shape = SubResource("CapsuleShape2D_1")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_1")
|
||||
}
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
zoom = Vector2(2, 2)
|
||||
|
||||
[node name="RayCast2D" type="RayCast2D" parent="."]
|
||||
175
scenes/characters/remote_player.tscn
Normal file
175
scenes/characters/remote_player.tscn
Normal file
@@ -0,0 +1,175 @@
|
||||
[gd_scene load_steps=13 format=3 uid="uid://chb8mcqhfnkkr"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtbajfsljdht5" path="res://scenes/characters/RemotePlayer.gd" id="1_mu86i"]
|
||||
[ext_resource type="Texture2D" uid="uid://cghab1hkx5lg5" path="res://assets/characters/player_spritesheet.png" id="2_7oc7u"]
|
||||
|
||||
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_1"]
|
||||
radius = 21.0
|
||||
height = 48.0
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_down"]
|
||||
resource_name = "idle_down"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_left"]
|
||||
resource_name = "idle_left"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [12]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_right"]
|
||||
resource_name = "idle_right"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [8]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_up"]
|
||||
resource_name = "idle_up"
|
||||
length = 0.1
|
||||
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),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [4]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_down"]
|
||||
resource_name = "walk_down"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [0, 1, 2, 3]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_left"]
|
||||
resource_name = "walk_left"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [12, 13, 14, 15]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_right"]
|
||||
resource_name = "walk_right"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [8, 9, 10, 11]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_up"]
|
||||
resource_name = "walk_up"
|
||||
length = 0.8
|
||||
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, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [4, 5, 6, 7]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_1"]
|
||||
_data = {
|
||||
&"idle_down": SubResource("Animation_idle_down"),
|
||||
&"idle_left": SubResource("Animation_idle_left"),
|
||||
&"idle_right": SubResource("Animation_idle_right"),
|
||||
&"idle_up": SubResource("Animation_idle_up"),
|
||||
&"walk_down": SubResource("Animation_walk_down"),
|
||||
&"walk_left": SubResource("Animation_walk_left"),
|
||||
&"walk_right": SubResource("Animation_walk_right"),
|
||||
&"walk_up": SubResource("Animation_walk_up")
|
||||
}
|
||||
|
||||
[node name="RemotePlayer" type="CharacterBody2D"]
|
||||
script = ExtResource("1_mu86i")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
position = Vector2(1.5000005, -24.5)
|
||||
texture = ExtResource("2_7oc7u")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(2, -24)
|
||||
shape = SubResource("CapsuleShape2D_1")
|
||||
disabled = true
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_1")
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func _validate_texture():
|
||||
return false
|
||||
|
||||
# ============================================================================
|
||||
# 调试方法
|
||||
# 信息方法
|
||||
# ============================================================================
|
||||
|
||||
# 获取瓦片信息
|
||||
@@ -113,10 +113,3 @@ func get_tile_info() -> Dictionary:
|
||||
"texture_size": texture.get_size() if texture else Vector2.ZERO,
|
||||
"auto_snap": auto_snap
|
||||
}
|
||||
|
||||
# 打印瓦片信息
|
||||
func print_info():
|
||||
var info = get_tile_info()
|
||||
print("=== 草地瓦片信息 ===")
|
||||
for key in info:
|
||||
print(key, ": ", info[key])
|
||||
11
scenes/prefabs/items/NoticeBoard.gd
Normal file
11
scenes/prefabs/items/NoticeBoard.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
extends StaticBody2D
|
||||
|
||||
func interact():
|
||||
# Check if dialog already exists
|
||||
if get_tree().root.has_node("NoticeDialog"):
|
||||
return
|
||||
|
||||
var dialog = preload("res://scenes/ui/notice_dialog.tscn").instantiate()
|
||||
dialog.name = "NoticeDialog"
|
||||
get_tree().root.add_child(dialog)
|
||||
return null # No bubble text needed
|
||||
1
scenes/prefabs/items/NoticeBoard.gd.uid
Normal file
1
scenes/prefabs/items/NoticeBoard.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://pnlgf420wktn
|
||||
13
scenes/prefabs/items/WelcomeBoard.gd
Normal file
13
scenes/prefabs/items/WelcomeBoard.gd
Normal file
@@ -0,0 +1,13 @@
|
||||
extends StaticBody2D
|
||||
|
||||
func interact():
|
||||
# Prevent multiple dialogs
|
||||
if get_tree().root.has_node("WelcomeDialog"):
|
||||
return null
|
||||
|
||||
# Spawn the Welcome Dialog
|
||||
var dialog = preload("res://scenes/ui/welcome_dialog.tscn").instantiate()
|
||||
dialog.name = "WelcomeDialog"
|
||||
# Add to the Scene Root (World) or CanvasLayer if it has one
|
||||
get_tree().root.add_child(dialog)
|
||||
return null # Return null prevents Player from showing a bubble
|
||||
1
scenes/prefabs/items/WelcomeBoard.gd.uid
Normal file
1
scenes/prefabs/items/WelcomeBoard.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d2od22agputjt
|
||||
20
scenes/prefabs/items/notice_board.tscn
Normal file
20
scenes/prefabs/items/notice_board.tscn
Normal file
@@ -0,0 +1,20 @@
|
||||
[gd_scene load_steps=4 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, 53.333332)
|
||||
|
||||
[node name="NoticeBoard" type="StaticBody2D"]
|
||||
scale = Vector2(0.6, 0.6)
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
position = Vector2(0, -16)
|
||||
scale = Vector2(0.5, 0.5)
|
||||
texture = ExtResource("2_sprite")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(0, 13.333335)
|
||||
shape = SubResource("RectangleShape2D_nb")
|
||||
19
scenes/prefabs/items/welcome_board.tscn
Normal file
19
scenes/prefabs/items/welcome_board.tscn
Normal file
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://c7k8yay002w4"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d2od22agputjt" path="res://scenes/prefabs/items/WelcomeBoard.gd" id="1_script"]
|
||||
[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, 26.5)
|
||||
|
||||
[node name="WelcomeBoard" type="StaticBody2D"]
|
||||
collision_layer = 3
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
scale = Vector2(0.25, 0.25)
|
||||
texture = ExtResource("2_sprite")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(0, 18.75)
|
||||
shape = SubResource("RectangleShape2D_board")
|
||||
@@ -209,11 +209,3 @@ func _format_timestamp(timestamp: float) -> String:
|
||||
|
||||
# 格式化为 HH:MM
|
||||
return "%02d:%02d" % [datetime.hour, datetime.minute]
|
||||
|
||||
# 获取所有子节点名称(调试用)
|
||||
func _get_all_children_names(node: Node, _indent: int = 0) -> String:
|
||||
var result := ""
|
||||
for child in node.get_children():
|
||||
result += " ".repeat(_indent) + child.name + "\n"
|
||||
result += _get_all_children_names(child, _indent + 1)
|
||||
return result
|
||||
|
||||
@@ -102,9 +102,7 @@ func _ready():
|
||||
_setup_controllers() # 初始化控制器
|
||||
_connect_signals() # 连接信号
|
||||
_setup_ui() # 设置UI初始状态
|
||||
|
||||
print("认证场景视图已加载")
|
||||
|
||||
|
||||
# 测试网络连接
|
||||
auth_manager.test_network_connection()
|
||||
|
||||
@@ -329,6 +327,8 @@ func _on_login_enter(_text: String):
|
||||
|
||||
# 登录成功处理
|
||||
func _on_controller_login_success(username: String) -> void:
|
||||
_release_focus_owner()
|
||||
|
||||
# 清空表单
|
||||
login_username.text = ""
|
||||
login_password.text = ""
|
||||
@@ -341,11 +341,18 @@ func _on_controller_login_success(username: String) -> void:
|
||||
var token: String = auth_manager.get_access_token()
|
||||
if not token.is_empty():
|
||||
ChatManager.set_game_token(token)
|
||||
print("✅ 已设置 ChatManager token: ", token.substr(0, 20) + "...")
|
||||
|
||||
# 同时设置 LocationManager token(用于位置同步)
|
||||
LocationManager.set_auth_token(token)
|
||||
|
||||
# 发送登录成功信号给上层
|
||||
login_success.emit(username)
|
||||
|
||||
func _release_focus_owner() -> void:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if is_instance_valid(focus_owner):
|
||||
focus_owner.release_focus()
|
||||
|
||||
# 登录失败处理
|
||||
func _on_controller_login_failed(_message: String):
|
||||
# 登录失败时不需要额外处理,Toast已经显示了错误信息
|
||||
@@ -399,9 +406,9 @@ func _on_controller_form_validation_failed(field: String, message: String):
|
||||
register_confirm.grab_focus()
|
||||
|
||||
# 网络状态变化处理
|
||||
func _on_controller_network_status_changed(network_connected: bool, message: String):
|
||||
func _on_controller_network_status_changed(_network_connected: bool, _message: String):
|
||||
# 可以在这里添加网络状态指示器
|
||||
print("网络状态: ", "连接" if network_connected else "断开", " - ", message)
|
||||
pass
|
||||
|
||||
# 按钮状态变化处理
|
||||
func _on_controller_button_state_changed(button_name: String, is_loading: bool, text: String):
|
||||
|
||||
9
scenes/ui/ChatBubble.gd
Normal file
9
scenes/ui/ChatBubble.gd
Normal file
@@ -0,0 +1,9 @@
|
||||
extends Control
|
||||
|
||||
@onready var label = $PanelContainer/Label
|
||||
|
||||
func set_text(text):
|
||||
label.text = text
|
||||
# Destroy after 5 seconds
|
||||
await get_tree().create_timer(5.0).timeout
|
||||
queue_free()
|
||||
1
scenes/ui/ChatBubble.gd.uid
Normal file
1
scenes/ui/ChatBubble.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4aorojcbwkmb
|
||||
42
scenes/ui/ChatBubble.tscn
Normal file
42
scenes/ui/ChatBubble.tscn
Normal file
@@ -0,0 +1,42 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b4aorojcbwkmb" path="res://scenes/ui/ChatBubble.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bubble_modern"]
|
||||
bg_color = Color(1, 1, 1, 0.9)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
corner_radius_bottom_left = 5
|
||||
shadow_color = Color(0, 0, 0, 0.2)
|
||||
shadow_size = 2
|
||||
|
||||
[node name="ChatBubble" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 7
|
||||
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
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
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
|
||||
text = "..."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
||||
custom_minimum_size = Vector2(150, 0)
|
||||
@@ -100,7 +100,7 @@ func _exit_tree() -> void:
|
||||
# 处理全局输入
|
||||
func _input(event: InputEvent) -> void:
|
||||
# 检查是否按下 Enter 键
|
||||
if event is InputEventKey and event.keycode == KEY_ENTER:
|
||||
if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER):
|
||||
_handle_enter_pressed()
|
||||
|
||||
# 处理 Enter 键按下
|
||||
@@ -153,22 +153,22 @@ func _release_input_focus() -> void:
|
||||
|
||||
# 显示聊天框
|
||||
func show_chat() -> void:
|
||||
if _is_chat_visible:
|
||||
return
|
||||
|
||||
_is_chat_visible = true
|
||||
chat_panel.show()
|
||||
if is_instance_valid(chat_panel):
|
||||
chat_panel.show()
|
||||
|
||||
# 停止隐藏计时器
|
||||
_stop_hide_timer()
|
||||
|
||||
# 隐藏聊天框
|
||||
func hide_chat() -> void:
|
||||
if not _is_chat_visible:
|
||||
return
|
||||
|
||||
_is_chat_visible = false
|
||||
chat_panel.hide()
|
||||
_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()
|
||||
|
||||
# 停止隐藏计时器
|
||||
_stop_hide_timer()
|
||||
@@ -185,13 +185,17 @@ func _create_hide_timer() -> void:
|
||||
func _start_hide_timer() -> void:
|
||||
if _is_typing:
|
||||
return # 输入时不隐藏
|
||||
if not is_instance_valid(_hide_timer):
|
||||
return
|
||||
if not _hide_timer.is_inside_tree():
|
||||
return
|
||||
|
||||
_stop_hide_timer() # 先停止之前的计时器
|
||||
_hide_timer.start()
|
||||
|
||||
# 停止隐藏倒计时
|
||||
func _stop_hide_timer() -> void:
|
||||
if _hide_timer:
|
||||
if is_instance_valid(_hide_timer) and _hide_timer.is_inside_tree():
|
||||
_hide_timer.stop()
|
||||
|
||||
# 隐藏计时器超时
|
||||
@@ -219,6 +223,8 @@ func _on_input_focus_entered() -> void:
|
||||
# 输入框失去焦点
|
||||
func _on_input_focus_exited() -> void:
|
||||
_is_typing = false
|
||||
if not is_inside_tree():
|
||||
return
|
||||
# 开始 5 秒倒计时
|
||||
if not _is_chat_visible:
|
||||
return
|
||||
@@ -284,7 +290,7 @@ func _on_chat_error(data: Dictionary) -> void:
|
||||
var error_code: String = data.get("error_code", "")
|
||||
var message: String = data.get("message", "")
|
||||
|
||||
print("❌ ChatUI 错误: [", error_code, "] ", message)
|
||||
push_error("ChatUI: [%s] %s" % [error_code, message])
|
||||
|
||||
# 处理连接状态变化
|
||||
func _on_connection_state_changed(data: Dictionary) -> void:
|
||||
|
||||
156
scenes/ui/NoticeDialog.gd
Normal file
156
scenes/ui/NoticeDialog.gd
Normal file
@@ -0,0 +1,156 @@
|
||||
extends CanvasLayer
|
||||
|
||||
@onready var content_label = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer/TextPanel/ContentLabel
|
||||
@onready var prev_btn = $CenterContainer/PanelContainer/VBoxContainer/Footer/PrevButton
|
||||
@onready var next_btn = $CenterContainer/PanelContainer/VBoxContainer/Footer/NextButton
|
||||
@onready var dots_container = $CenterContainer/PanelContainer/VBoxContainer/Footer/DotsContainer
|
||||
@onready var content_container = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer
|
||||
|
||||
# Mock Data
|
||||
var pages = [
|
||||
{
|
||||
"text": "欢迎来到 [color=#3399ff]Datawhale Town[/color]!\n\n这里是开源学习者的家园。在这里,我们一同探索知识,分享成长。\n\n[center]🐋[/center]",
|
||||
# 使用社区图片作为封面
|
||||
"image_path": "res://assets/sprites/environment/community_512_512.png",
|
||||
"image_color": Color(0.9, 0.9, 0.9) # 保留作为后备选项
|
||||
},
|
||||
{
|
||||
"text": "最新活动:\n\n- 镇长刚刚搬进来了,就在喷泉左边。\n- 欢迎板已经设立,查看最新动态。\n- 玩家名字现在显示在头顶了!",
|
||||
# 使用喷泉图片对应"喷泉左边"的描述
|
||||
"image_path": "res://assets/sprites/environment/fountain_256_192.png",
|
||||
"image_color": Color(0.8, 0.9, 0.8)
|
||||
},
|
||||
{
|
||||
"text": "操作提示:\n\n- 按 [color=#ffaa00]F[/color] 键可以与物体互动。\n- 在下方输入框输入文字并在气泡中显示。\n- 点击右下角按钮发送聊天。",
|
||||
# 使用公告板图片对应"操作提示"
|
||||
"image_path": "res://assets/sprites/environment/board.png",
|
||||
"image_color": Color(0.9, 0.8, 0.8)
|
||||
}
|
||||
]
|
||||
|
||||
var current_page = 0
|
||||
var tween: Tween
|
||||
var mock_pages = []
|
||||
|
||||
func _ready():
|
||||
# Pause the game
|
||||
get_tree().paused = true
|
||||
|
||||
$CenterContainer/PanelContainer/VBoxContainer/Header/RightContainer/CloseButton.pressed.connect(_on_close_pressed)
|
||||
prev_btn.pressed.connect(_on_prev_pressed)
|
||||
next_btn.pressed.connect(_on_next_pressed)
|
||||
|
||||
# Network Integration - Use direct callback for better error handling
|
||||
# Short timeout (2.0s) so mock data appears quickly if server is down
|
||||
NetworkManager.get_request("/notices", _on_notices_response, 2.0)
|
||||
|
||||
# Initial Setup (with generic "Loading" state)
|
||||
mock_pages = pages.duplicate(true)
|
||||
pages = [{"text": "[center]Loading notices...[/center]", "image_color": Color(0.9, 0.9, 0.9)}]
|
||||
_setup_dots()
|
||||
_update_ui(false)
|
||||
|
||||
func _on_notices_response(success: bool, data: Dictionary, _error_info: Dictionary):
|
||||
var new_pages = []
|
||||
if success and data.has("data") and data["data"] is Array:
|
||||
new_pages = data["data"]
|
||||
|
||||
if new_pages.is_empty():
|
||||
pages = mock_pages
|
||||
else:
|
||||
pages = new_pages
|
||||
# Handle color strings from JSON if necessary
|
||||
for p in pages:
|
||||
if p.has("image_color") and p["image_color"] is String:
|
||||
p["image_color"] = Color(p["image_color"])
|
||||
|
||||
current_page = 0
|
||||
_setup_dots()
|
||||
_update_ui(true)
|
||||
|
||||
|
||||
func _setup_dots():
|
||||
for child in dots_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for i in range(pages.size()):
|
||||
var dot = ColorRect.new()
|
||||
dot.custom_minimum_size = Vector2(10, 10) # Base size
|
||||
dots_container.add_child(dot)
|
||||
|
||||
func _update_ui(animate: bool = true):
|
||||
if pages.is_empty():
|
||||
return
|
||||
|
||||
# Update Buttons
|
||||
prev_btn.disabled = (current_page == 0)
|
||||
next_btn.disabled = (current_page == pages.size() - 1)
|
||||
|
||||
# Update Dots Logic
|
||||
var dots = dots_container.get_children()
|
||||
for i in range(dots.size()):
|
||||
if i == current_page:
|
||||
dots[i].color = Color(0.2, 0.2, 0.2, 1) # Dark Active
|
||||
dots[i].custom_minimum_size = Vector2(12, 12) # Active Slightly Larger
|
||||
else:
|
||||
dots[i].color = Color(0.8, 0.8, 0.8, 1) # Light Inactive
|
||||
dots[i].custom_minimum_size = Vector2(10, 10)
|
||||
|
||||
# Update Content
|
||||
if animate:
|
||||
_animate_content_change()
|
||||
else:
|
||||
_set_content_immediate()
|
||||
|
||||
@onready var image_rect = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel/ImageRect
|
||||
@onready var image_label = $CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel/ImageLabel
|
||||
|
||||
func _set_content_immediate():
|
||||
var page = pages[current_page]
|
||||
content_label.text = page.get("text", "")
|
||||
|
||||
if page.has("image_path") and page["image_path"] != "":
|
||||
var path = page["image_path"]
|
||||
if ResourceLoader.exists(path):
|
||||
image_rect.texture = load(path)
|
||||
image_label.visible = false
|
||||
else:
|
||||
image_rect.texture = null
|
||||
image_label.visible = true
|
||||
image_label.text = "Image Not Found"
|
||||
else:
|
||||
image_rect.texture = null
|
||||
image_label.visible = true
|
||||
image_label.text = "No Image"
|
||||
|
||||
|
||||
|
||||
func _animate_content_change():
|
||||
if tween and tween.is_valid():
|
||||
tween.kill()
|
||||
|
||||
tween = create_tween()
|
||||
|
||||
# Fade Out
|
||||
tween.tween_property(content_container, "modulate:a", 0.0, 0.15)
|
||||
|
||||
# Callback to change text
|
||||
tween.tween_callback(self._set_content_immediate)
|
||||
|
||||
# Fade In
|
||||
tween.tween_property(content_container, "modulate:a", 1.0, 0.15)
|
||||
|
||||
func _on_prev_pressed():
|
||||
if current_page > 0:
|
||||
current_page -= 1
|
||||
_update_ui()
|
||||
|
||||
func _on_next_pressed():
|
||||
if current_page < pages.size() - 1:
|
||||
current_page += 1
|
||||
_update_ui()
|
||||
|
||||
func _on_close_pressed():
|
||||
# Unpause the game
|
||||
get_tree().paused = false
|
||||
queue_free()
|
||||
1
scenes/ui/NoticeDialog.gd.uid
Normal file
1
scenes/ui/NoticeDialog.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cxi5rchnmk07p
|
||||
20
scenes/ui/WelcomeDialog.gd
Normal file
20
scenes/ui/WelcomeDialog.gd
Normal file
@@ -0,0 +1,20 @@
|
||||
extends CanvasLayer
|
||||
|
||||
func _ready():
|
||||
# Connect close button (X)
|
||||
var header_close = find_child("CloseButton", true, false)
|
||||
if header_close:
|
||||
header_close.pressed.connect(_on_close_pressed)
|
||||
|
||||
# Connect Start button
|
||||
var start_btn = find_child("StartButton", true, false)
|
||||
if start_btn:
|
||||
start_btn.pressed.connect(_on_close_pressed)
|
||||
|
||||
func _on_close_pressed():
|
||||
queue_free()
|
||||
|
||||
func _input(event):
|
||||
# Allow ESC to close
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
queue_free()
|
||||
1
scenes/ui/WelcomeDialog.gd.uid
Normal file
1
scenes/ui/WelcomeDialog.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cu6x4dxhsylw2
|
||||
132
scenes/ui/notice_dialog.tscn
Normal file
132
scenes/ui/notice_dialog.tscn
Normal file
@@ -0,0 +1,132 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://rdmro1jxs6ga"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cxi5rchnmk07p" path="res://scenes/ui/NoticeDialog.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rounded"]
|
||||
bg_color = Color(0.95, 0.95, 0.95, 1)
|
||||
corner_radius_top_left = 16
|
||||
corner_radius_top_right = 16
|
||||
corner_radius_bottom_right = 16
|
||||
corner_radius_bottom_left = 16
|
||||
shadow_color = Color(0, 0, 0, 0.2)
|
||||
shadow_size = 8
|
||||
|
||||
[node name="NoticeDialog" type="CanvasLayer"]
|
||||
process_mode = 3
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Dimmer" type="ColorRect" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0, 0, 0, 0.5)
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="CenterContainer"]
|
||||
custom_minimum_size = Vector2(480, 420)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_rounded")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="HeaderSpacer" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 4)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LeftSpacer" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Title" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.2, 0.2, 0.2, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "公告板"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="RightContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
alignment = 2
|
||||
|
||||
[node name="CloseButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Header/RightContainer"]
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
layout_mode = 2
|
||||
text = "X"
|
||||
flat = true
|
||||
|
||||
[node name="RightMargin" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header/RightContainer"]
|
||||
custom_minimum_size = Vector2(8, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ContentContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="ImagePanel" type="PanelContainer" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer"]
|
||||
custom_minimum_size = Vector2(0, 200)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ImageRect" type="TextureRect" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel"]
|
||||
layout_mode = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="ImageLabel" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/ContentContainer/ImagePanel"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 1)
|
||||
text = "Image Placeholder"
|
||||
horizontal_alignment = 1
|
||||
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
|
||||
|
||||
[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
|
||||
bbcode_enabled = true
|
||||
text = "Announcement Content..."
|
||||
|
||||
[node name="Footer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 48)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
alignment = 1
|
||||
|
||||
[node name="PrevButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Footer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
text = "<"
|
||||
|
||||
[node name="DotsContainer" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer/Footer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="NextButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Footer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
text = ">"
|
||||
|
||||
[node name="BottomSpacer" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 4)
|
||||
layout_mode = 2
|
||||
117
scenes/ui/welcome_dialog.tscn
Normal file
117
scenes/ui/welcome_dialog.tscn
Normal file
@@ -0,0 +1,117 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://d8mam0n1a3b5"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cu6x4dxhsylw2" 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"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_card"]
|
||||
bg_color = Color(1, 1, 1, 1)
|
||||
corner_radius_top_left = 10
|
||||
corner_radius_top_right = 10
|
||||
corner_radius_bottom_right = 10
|
||||
corner_radius_bottom_left = 10
|
||||
shadow_color = Color(0, 0, 0, 0.2)
|
||||
shadow_size = 10
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_btn_rounded"]
|
||||
bg_color = Color(0.95, 0.95, 0.95, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 20
|
||||
corner_radius_top_right = 20
|
||||
corner_radius_bottom_right = 20
|
||||
corner_radius_bottom_left = 20
|
||||
|
||||
[node name="WelcomeDialog" type="CanvasLayer"]
|
||||
script = ExtResource("1_vs5b1")
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0, 0, 0, 0.4)
|
||||
|
||||
[node name="CenterContainer" type="CenterContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="CenterContainer"]
|
||||
custom_minimum_size = Vector2(400, 350)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_card")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/PanelContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Title" type="Label" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "欢迎来到 Datawhale Town!"
|
||||
|
||||
[node name="Spacer2" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="CloseButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/Header"]
|
||||
custom_minimum_size = Vector2(30, 30)
|
||||
layout_mode = 2
|
||||
text = "X"
|
||||
flat = true
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LogoContainer" type="CenterContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TextureRect" type="TextureRect" parent="CenterContainer/PanelContainer/VBoxContainer/LogoContainer"]
|
||||
custom_minimum_size = Vector2(300, 100)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("2_dy5hw")
|
||||
expand_mode = 1
|
||||
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
|
||||
text = "连接·共生·见证
|
||||
Datawhale Town —— 学习者的赛博家园与精神坐标。
|
||||
✨ 实时广场:看大家都在学什么。
|
||||
🏠 个人空间:展示你的学习笔记与作品。
|
||||
🤝 开源营地:更有氛围的组队学习体验。"
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="Spacer3" type="Control" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ActionContainer" type="CenterContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StartButton" type="Button" parent="CenterContainer/PanelContainer/VBoxContainer/ActionContainer"]
|
||||
custom_minimum_size = Vector2(150, 40)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0, 0, 0, 1)
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_btn_rounded")
|
||||
text = "开始探索"
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/PanelContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
Reference in New Issue
Block a user