合并主场景和个人小屋

This commit is contained in:
王浩
2026-02-07 14:11:00 +08:00
parent 603e7d9fc6
commit 326ab7ce5c
360 changed files with 4913 additions and 21701 deletions

View File

@@ -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
View 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

View File

@@ -0,0 +1 @@
uid://b43tvo8cykfrq

View 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)

View File

@@ -0,0 +1 @@
uid://rlkavptfhr4y

View 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)

View 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
View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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)

View File

@@ -0,0 +1 @@
uid://dy3uf1rlu4h1u

View 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

View File

@@ -0,0 +1 @@
uid://fdswi18nel8n

View 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)

View File

@@ -0,0 +1 @@
uid://dtbajfsljdht5

View 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")
}

View File

@@ -0,0 +1 @@
uid://btka26hrcvgen

View 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="."]

View 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")
}

View File

@@ -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])

View 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

View File

@@ -0,0 +1 @@
uid://pnlgf420wktn

View 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

View File

@@ -0,0 +1 @@
uid://d2od22agputjt

View 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")

View 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")

View File

@@ -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

View File

@@ -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
View 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()

View File

@@ -0,0 +1 @@
uid://b4aorojcbwkmb

42
scenes/ui/ChatBubble.tscn Normal file
View 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)

View File

@@ -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
View 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()

View File

@@ -0,0 +1 @@
uid://cxi5rchnmk07p

View 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()

View File

@@ -0,0 +1 @@
uid://cu6x4dxhsylw2

View 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

View 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