This commit is contained in:
WangXiang
2025-12-22 18:57:51 +08:00
commit 6119faf53e
159 changed files with 19162 additions and 0 deletions

BIN
Scripts/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,169 @@
extends Node
signal connected
signal disconnected
signal login_ok(my_id, players)
signal player_joined(id, name, x, y, skin_id)
signal player_left(id)
signal player_moved(id, x, y, flip_h, frame)
signal chat_message_received(sender_id, text)
signal private_chat_message_received(sender_id, text)
signal notices_received(data)
const DEFAULT_SERVER_URL = "ws://127.0.0.1:8910"
var socket := WebSocketPeer.new()
var my_id: int = -1
var player_info := {"name": "Player", "skin_id": 0}
var players := {} # id -> {name, x, y, skin_id}
var _connected := false
var next_spawn_position: Vector2 = Vector2.ZERO
func _ready():
process_mode = Node.PROCESS_MODE_ALWAYS
func _process(_delta):
socket.poll()
var state = socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
if not _connected:
_connected = true
_on_connected()
while socket.get_available_packet_count():
var packet = socket.get_packet()
_on_message(packet.get_string_from_utf8())
elif state == WebSocketPeer.STATE_CLOSED:
if _connected:
_connected = false
_on_disconnected()
func connect_to_server(url: String = ""):
if url.is_empty():
url = DEFAULT_SERVER_URL
var err = socket.connect_to_url(url)
if err != OK:
print("Failed to connect: ", err)
return err
func _on_connected():
print("Connected to server!")
# Send login
send_json({
"type": "login",
"name": player_info["name"],
"skin_id": player_info["skin_id"]
})
connected.emit()
func _on_disconnected():
print("Disconnected from server")
my_id = -1
players.clear()
disconnected.emit()
func _on_message(data: String):
var json = JSON.new()
var err = json.parse(data)
if err != OK:
print("JSON parse error: ", data)
return
var msg = json.data
# if msg.get("type") == "player_move" and int(msg.get("id", -1)) != my_id:
# print("RAW NET: ", data)
var msg_type = msg.get("type", "")
match msg_type:
"login_ok":
my_id = int(msg["id"])
for p in msg["players"]:
players[int(p["id"])] = {
"name": p["name"],
"x": p["x"],
"y": p["y"],
"skin_id": p["skin_id"]
}
login_ok.emit(my_id, players)
"player_join":
var id = int(msg["id"])
players[id] = {
"name": msg["name"],
"x": msg["x"],
"y": msg["y"],
"skin_id": msg["skin_id"]
}
player_joined.emit(id, msg["name"], msg["x"], msg["y"], msg["skin_id"])
"player_leave":
var id = int(msg["id"])
players.erase(id)
player_left.emit(id)
"player_move":
var id = int(msg["id"])
if id != my_id: # Only log others to reduce spam, or log all? Let's log all for now if issue is critical
print("Net: Recv Move for ID: ", id, " Frame: ", msg.get("frame", 1))
if players.has(id):
players[id]["x"] = msg["x"]
players[id]["y"] = msg["y"]
player_moved.emit(id, msg["x"], msg["y"], msg.get("flip_h", false), int(msg.get("frame", 1)))
"chat":
var id = int(msg["id"])
chat_message_received.emit(id, msg["text"])
"private_chat":
var id = int(msg["sender_id"])
private_chat_message_received.emit(id, msg["text"])
"notices_list":
notices_received.emit(msg.get("data", []))
"scene_peers":
# Refresh player list for new scene
players.clear()
for p in msg["players"]:
players[int(p["id"])] = {
"name": p["name"],
"x": p["x"],
"y": p["y"],
"skin_id": p["skin_id"]
}
# Emit login_ok to trigger refresh (or we could add a scene_loaded signal)
login_ok.emit(my_id, players)
func send_json(obj: Dictionary):
if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
socket.send_text(JSON.stringify(obj))
func send_move(x: float, y: float, flip_h: bool, frame: int):
send_json({"type": "move", "x": x, "y": y, "flip_h": flip_h, "frame": frame})
func send_change_scene(scene_name: String, pos: Vector2 = Vector2(-20000, -20000)):
var data = {"type": "change_scene", "scene": scene_name}
if pos.x != -20000:
data["x"] = pos.x
data["y"] = pos.y
send_json(data)
func send_message(text: String):
send_json({"type": "chat", "text": text})
func send_private_message(target_id: int, text: String):
send_json({"type": "private_chat", "target_id": target_id, "text": text})
func request_notices():
send_json({"type": "get_notices"})
func set_player_name(n: String):
player_info["name"] = n
func set_skin_id(id: int):
player_info["skin_id"] = id

View File

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

View File

@@ -0,0 +1,13 @@
extends Node
# Placeholder skins
var skins: Dictionary = {
0: "res://icon.svg", # Fallback
}
func get_skin_texture(id: int) -> Texture2D:
if skins.has(id):
# In a real scenario, this would preload or load form cache
# For now, using load() is fine for prototype
return load(skins[id])
return load(skins[0])

View File

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

48
Scripts/Chair.gd Normal file
View File

@@ -0,0 +1,48 @@
extends StaticBody2D
signal on_player_entered_range
signal on_player_exited_range
@onready var sit_marker = $SitMarker
@onready var interaction_area = $InteractionArea
var is_occupied: bool = false
var current_player: Node2D = null
var player_in_range: Node2D = null
func _ready():
interaction_area.body_entered.connect(_on_body_entered)
interaction_area.body_exited.connect(_on_body_exited)
func _input(event):
if is_occupied:
return
if player_in_range and event is InputEventKey and event.pressed and event.keycode == KEY_E:
_interact(player_in_range)
func _interact(player: Node2D):
if is_occupied:
return
is_occupied = true
current_player = player
# Assume player has this method (we will implement it next)
if player.has_method("start_sitting"):
player.start_sitting(sit_marker.global_position, self)
# Called by player when they stand up
func on_player_stand_up():
is_occupied = false
current_player = null
func _on_body_entered(body):
if body.has_method("start_sitting"):
player_in_range = body
emit_signal("on_player_entered_range")
func _on_body_exited(body):
if body == player_in_range:
player_in_range = null
emit_signal("on_player_exited_range")

1
Scripts/Chair.gd.uid Normal file
View File

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

30
Scripts/NPCMayor.gd Normal file
View File

@@ -0,0 +1,30 @@
extends CharacterBody2D
signal interaction_happened(text)
@export var npc_name: String = "Mayor"
@export var dialogue: String = "欢迎来到WhaleTown我是镇长范鲸鱼"
func _ready():
$Sprite2D.texture = preload("res://Assets/MayorWhale.png")
# Sprite Sheet setup for idle animation (if needed later)
# For now, frame 0 is fine
$Sprite2D.hframes = 3
$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)
return null
func show_bubble(text):
var bubble = preload("res://Scenes/UI/ChatBubble.tscn").instantiate()
add_child(bubble)
bubble.set_text(text)

1
Scripts/NPCMayor.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://5tvkhbf0237e

13
Scripts/NoticeBoard.gd Normal file
View File

@@ -0,0 +1,13 @@
extends StaticBody2D
func interact():
print("Interacted with Notice Board")
# Check if dialog already exists
if get_tree().root.has_node("NoticeDialog"):
return
var dialog = preload("res://Scenes/UI/NoticeDialog.tscn").instantiate()
dialog.name = "NoticeDialog"
get_tree().root.add_child(dialog)
return null # No bubble text needed

View File

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

222
Scripts/Player.gd Normal file
View File

@@ -0,0 +1,222 @@
extends CharacterBody2D
@export var grid_size := 16
var player_id: int = -1
var is_local := false
var is_moving := false
var server_position := Vector2.ZERO
signal interaction_request(target_id, target_name)
@onready var ray = $RayCast2D
func _ready():
server_position = position
# Initial sprite setup
$Sprite2D.scale = Vector2(0.5, 0.5)
$Sprite2D.frame = 1
# Physics Collision (Movement)
collision_layer = 2 # Force Player to be on Layer 2
collision_mask = 1 # Only collide with World (Layer 1), walk through other Players (Layer 2)
# RayCast Setup
ray.enabled = true
ray.hit_from_inside = true # Detect if overlapping
ray.collision_mask = 3 # Layers 1 (World) and 2 (Player)
ray.add_exception(self) # Ignore local player
if is_local:
var cam = Camera2D.new()
cam.zoom = Vector2(1.0, 1.0)
cam.position_smoothing_enabled = true
add_child(cam)
NetworkManager.chat_message_received.connect(_on_chat_message)
# Set Name Label
var name_label = $NameLabel
if is_local:
name_label.text = NetworkManager.player_info["name"]
name_label.modulate = Color(0.5, 1, 0.5) # Highlight local player
elif player_id != -1 and NetworkManager.players.has(player_id):
name_label.text = NetworkManager.players[player_id].name
func _physics_process(_delta):
if is_local:
_process_input()
else:
_process_remote_movement()
# Interaction Check (F Key) moved to _input to prevent spam
var is_sitting := false
var current_chair: Node2D = null
func _process_input():
# Stop input if chatting
var focus_owner = get_viewport().gui_get_focus_owner()
if focus_owner is LineEdit:
return
if is_moving or is_sitting:
return
var input_dir := Vector2.ZERO
if Input.is_action_pressed("ui_up") or Input.is_key_pressed(KEY_W):
input_dir = Vector2.UP
$Sprite2D.frame = 4 # Row 1, Center (Back view)
$Sprite2D.flip_h = false
ray.target_position = Vector2(0, -96)
elif Input.is_action_pressed("ui_down") or Input.is_key_pressed(KEY_S):
input_dir = Vector2.DOWN
$Sprite2D.frame = 1 # Row 0, Center (Front view)
$Sprite2D.flip_h = false
ray.target_position = Vector2(0, 96)
elif Input.is_action_pressed("ui_left") or Input.is_key_pressed(KEY_A):
input_dir = Vector2.LEFT
$Sprite2D.frame = 3 # Row 1, Left (Side view)
$Sprite2D.flip_h = false
ray.target_position = Vector2(-96, 0)
elif Input.is_action_pressed("ui_right") or Input.is_key_pressed(KEY_D):
input_dir = Vector2.RIGHT
$Sprite2D.frame = 3 # Use same side view
$Sprite2D.flip_h = true # Flip it
ray.target_position = Vector2(96, 0)
if input_dir != Vector2.ZERO:
_attempt_move(input_dir)
func _input(event):
if not is_local:
return
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F:
# Check if ignoring input (chatting)
var focus_owner = get_viewport().gui_get_focus_owner()
if focus_owner is LineEdit:
return
if is_sitting:
stand_up()
return
ray.force_raycast_update()
if ray.is_colliding():
var collider = ray.get_collider()
# Check if collider is a player (ID-based name)
if collider:
if collider.has_method("interact"):
var text = collider.interact()
if text:
show_bubble(text) # Show response locally for now
elif collider.name.is_valid_int():
var target_id = int(str(collider.name))
var p_info = NetworkManager.players.get(target_id)
var t_name = p_info["name"] if p_info else "Unknown"
interaction_request.emit(target_id, t_name)
func start_sitting(target_pos: Vector2, chair: Node2D):
if is_sitting:
return
is_sitting = true
is_moving = false # Force stop moving
current_chair = chair
# Disable physics/collision
set_physics_process(false)
$CollisionShape2D.set_deferred("disabled", true)
# Tween to seat
var tween = create_tween()
tween.tween_property(self, "position", target_pos, 0.3).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
# Visuals
$Sprite2D.frame = 1 # Sit/Idle Front
$Sprite2D.flip_h = false
# You might want to offset Z-index if chair has backrest vs stool
# z_index = 0 # Default
func stand_up():
if not is_sitting:
return
is_sitting = false
# Notify chair
if current_chair and current_chair.has_method("on_player_stand_up"):
current_chair.on_player_stand_up()
current_chair = null
# Restore physics
set_physics_process(true)
$CollisionShape2D.set_deferred("disabled", false)
# Optional: Move slightly forward to avoid clipping immediately if collision enables
var exit_pos = position + Vector2(0, 10)
position = exit_pos
NetworkManager.send_move(position.x, position.y, $Sprite2D.flip_h, $Sprite2D.frame)
func _attempt_move(dir: Vector2):
var target_pos = position + dir * grid_size
# Save state
var old_target = ray.target_position
var old_mask = ray.collision_mask
var old_hfi = ray.hit_from_inside
var old_cwa = ray.collide_with_areas
# Configure for movement check (World Only, Short Range, Bodies Only)
ray.target_position = dir * grid_size
ray.collision_mask = 1
ray.hit_from_inside = false
ray.collide_with_areas = false # Ignore Areas (like Doors), only hit Walls (Bodies)
ray.force_raycast_update()
var blocked = ray.is_colliding()
# Restore state for Chat Interaction
ray.target_position = old_target
ray.collision_mask = old_mask
ray.hit_from_inside = old_hfi
ray.collide_with_areas = old_cwa
if not blocked:
_tween_movement(target_pos)
# Send to server WITH orientation
NetworkManager.send_move(target_pos.x, target_pos.y, $Sprite2D.flip_h, $Sprite2D.frame)
func _process_remote_movement():
if position != server_position and not is_moving:
_tween_movement(server_position)
func set_remote_state(pos: Vector2, flip_h: bool, frame: int):
if is_local:
return # Do not let remote updates override local input
server_position = pos
$Sprite2D.flip_h = flip_h
$Sprite2D.frame = frame
func _tween_movement(target_pos: Vector2):
is_moving = true
var tween = create_tween()
tween.tween_property(self, "position", target_pos, 0.2)
tween.tween_callback(func(): is_moving = false)
func _on_chat_message(sender_id, text):
if sender_id == player_id:
show_bubble(text)
func show_bubble(text):
$NameLabel.hide()
var bubble = preload("res://Scenes/UI/ChatBubble.tscn").instantiate()
add_child(bubble)
bubble.set_text(text)
bubble.tree_exited.connect(func(): $NameLabel.show())

1
Scripts/Player.gd.uid Normal file
View File

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

91
Scripts/Room.gd Normal file
View File

@@ -0,0 +1,91 @@
extends Node2D
@onready var tile_map = $TileMapLayer
@onready var players_container = $PlayersContainer
var player_nodes := {} # id -> Node
func _ready():
# Visual setup
tile_map.scale = Vector2(0.5, 0.5)
NetworkManager.login_ok.connect(_on_login_ok)
NetworkManager.player_joined.connect(_on_player_joined)
NetworkManager.player_left.connect(_on_player_left)
NetworkManager.player_moved.connect(_on_player_moved)
NetworkManager.disconnected.connect(_on_disconnected) # Handle disconnect
NetworkManager.chat_message_received.connect(_on_chat_message_received) # Reuse chat logic if desired
# Spawn myself manually since scene_peers excludes self
_spawn_player(NetworkManager.my_id, NetworkManager.player_info["name"], 0, 0, NetworkManager.player_info["skin_id"])
# Spawn existing players (including self if already in list, but we usually spawn self manually or via list)
# Since we just transitioned, NetworkManager.players contains the new list from 'scene_peers' message
for id in NetworkManager.players:
var p = NetworkManager.players[id]
_spawn_player(id, p["name"], p["x"], p["y"], p.get("skin_id", 0))
# Chat UI
var chat = preload("res://Scenes/UI/ChatHUD.tscn").instantiate()
add_child(chat)
func _spawn_player(id, _p_name, x, y, _skin_id):
if player_nodes.has(id): return # Prevent duplicate
var p = preload("res://Scenes/Player.tscn").instantiate()
p.name = str(id)
# For local player in Room, we might want a fixed spawn point (0,0) or the one from server
# If it's local player, we usually force it to (0,0) on entry
if id == NetworkManager.my_id:
p.position = Vector2(0, 0)
else:
p.position = Vector2(x, y)
p.player_id = id
p.is_local = (id == NetworkManager.my_id)
players_container.add_child(p)
player_nodes[id] = p
if p.is_local:
var camera = Camera2D.new()
camera.make_current()
p.add_child(camera)
func _on_login_ok(_my_id, _players):
# Refresh players if we get a full update
for id in player_nodes:
if id != NetworkManager.my_id:
player_nodes[id].queue_free()
player_nodes.clear()
for id in NetworkManager.players:
var p = NetworkManager.players[id]
_spawn_player(id, p["name"], p["x"], p["y"], p.get("skin_id", 0))
func _on_player_joined(id, p_name, x, y, skin_id):
_spawn_player(id, p_name, x, y, skin_id)
func _on_player_left(id):
if player_nodes.has(id):
player_nodes[id].queue_free()
player_nodes.erase(id)
func _on_player_moved(id, x, y, flip_h, frame):
if player_nodes.has(id):
player_nodes[id].set_remote_state(Vector2(x, y), flip_h, frame)
func _on_chat_message_received(sender_id, text):
# Chat HUD handles it via signals usually, but if we need bubbles:
if player_nodes.has(sender_id):
player_nodes[sender_id].show_bubble(text)
func _on_disconnected():
get_tree().change_scene_to_file("res://Scenes/StartMenu.tscn")
func _on_exit_body_entered(body):
if body.name == str(NetworkManager.my_id):
# Return to the world
NetworkManager.send_change_scene("world", NetworkManager.next_spawn_position)
get_tree().change_scene_to_file("res://Scenes/World.tscn")

1
Scripts/Room.gd.uid Normal file
View File

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

31
Scripts/StartMenu.gd Normal file
View File

@@ -0,0 +1,31 @@
extends CanvasLayer
@onready var name_input = $CenterContainer/Card/Content/Form/NameInput
const SERVER_URL = "ws://127.0.0.1:8910"
func _ready():
name_input.grab_focus()
func _unhandled_input(event):
if event.is_action_pressed("ui_accept"):
_on_join_pressed()
func _on_join_pressed():
if name_input.text.strip_edges().is_empty():
return
NetworkManager.set_player_name(name_input.text)
# Connect with hardcoded URL
var err = NetworkManager.connect_to_server(SERVER_URL)
if err == OK:
# Disable button to prevent double click?
# Wait for login_ok before changing scene
if not NetworkManager.login_ok.is_connected(_on_login_ok):
NetworkManager.login_ok.connect(_on_login_ok, CONNECT_ONE_SHOT)
else:
print("Connection failed: ", err)
func _on_login_ok(_my_id, _players):
get_tree().change_scene_to_file("res://Scenes/World.tscn")

1
Scripts/StartMenu.gd.uid Normal file
View File

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

9
Scripts/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://bpddnrpvlcf7l

71
Scripts/UI/ChatHUD.gd Normal file
View File

@@ -0,0 +1,71 @@
extends CanvasLayer
@onready var chat_log = $PanelContainer/VBoxContainer/RichTextLabel
@onready var input_field = $PanelContainer/VBoxContainer/LineEdit
var private_target_id: int = -1
var private_target_name: String = ""
func _ready():
NetworkManager.chat_message_received.connect(_on_chat_message_received)
NetworkManager.private_chat_message_received.connect(_on_private_message_received)
input_field.text_submitted.connect(_on_text_submitted)
func _input(event):
if event.is_action_pressed("ui_cancel"): # ESC
set_private_target(-1, "")
input_field.release_focus()
elif event.is_action_pressed("ui_accept"): # Enter
if not input_field.has_focus():
input_field.grab_focus()
get_viewport().set_input_as_handled()
func set_private_target(id: int, p_name: String):
private_target_id = id
private_target_name = p_name
if id != -1:
input_field.placeholder_text = "[Private] To " + p_name + "..."
input_field.add_theme_color_override("font_color", Color(1, 0.5, 1)) # Pink for private
# input_field.grab_focus() # Don't grab focus automatically, let user move
else:
input_field.placeholder_text = "Press Enter to chat..."
input_field.remove_theme_color_override("font_color")
func _on_text_submitted(new_text):
if new_text.strip_edges().is_empty():
input_field.release_focus()
return
if private_target_id != -1:
NetworkManager.send_private_message(private_target_id, new_text)
# Show my own private message in log
chat_log.append_text("[color=#FF88FF][Private -> %s]: %s[/color]\n" % [private_target_name, new_text])
else:
NetworkManager.send_message(new_text)
input_field.clear()
input_field.release_focus()
func _on_chat_message_received(sender_id, text):
var sender_name = str(sender_id)
print("Chat Debug: Msg from ", sender_id, " (Type: ", typeof(sender_id), ")")
print("Known Players: ", NetworkManager.players.keys())
if NetworkManager.players.has(sender_id):
sender_name = NetworkManager.players[sender_id].get("name", str(sender_id))
# Debug empty name
if sender_name == "":
sender_name = "Player_" + str(sender_id)
else:
print("Chat Debug: Player ID not found in NetworkManager.players")
chat_log.append_text("[color=yellow]%s[/color]: %s\n" % [sender_name, text])
func _on_private_message_received(sender_id, text):
var sender_name = str(sender_id)
if NetworkManager.players.has(sender_id):
sender_name = NetworkManager.players[sender_id].get("name", str(sender_id))
chat_log.append_text("[color=#FF88FF][Private From %s]: %s[/color]\n" % [sender_name, text])

View File

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

144
Scripts/UI/NoticeDialog.gd Normal file
View File

@@ -0,0 +1,144 @@
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_color": Color(0.9, 0.9, 0.9) # Mock image placeholder
},
{
"text": "最新活动:\n\n- 镇长刚刚搬进来了,就在喷泉左边。\n- 欢迎板已经设立,查看最新动态。\n- 玩家名字现在显示在头顶了!",
"image_color": Color(0.8, 0.9, 0.8)
},
{
"text": "操作提示:\n\n- 按 [color=#ffaa00]F[/color] 键可以与物体互动。\n- 在下方输入框输入文字并在气泡中显示。\n- 点击右下角按钮发送聊天。",
"image_color": Color(0.9, 0.8, 0.8)
}
]
var current_page = 0
var tween: Tween
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
NetworkManager.notices_received.connect(_on_notices_received)
NetworkManager.request_notices()
# Initial Setup (with generic "Loading" state)
pages = [{"text": "[center]Loading notices...[/center]", "image_color": Color(0.9, 0.9, 0.9)}]
_setup_dots()
_update_ui(false)
func _on_notices_received(data: Array):
if data.is_empty():
pages = [{"text": "[center]No notices available.[/center]", "image_color": Color(0.9, 0.9, 0.9)}]
else:
pages = data
# 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://cy3n8ccmeolgd

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://djjy58mh6kl4o

13
Scripts/WelcomeBoard.gd Normal file
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/WelcomeDialog.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://cfiinv7n2spwd

159
Scripts/World.gd Normal file
View File

@@ -0,0 +1,159 @@
extends Node2D
@onready var players_container = $PlayersContainer
@onready var tile_map = $Ground
var player_nodes := {} # id -> Node
func _ready():
$Ground.scale = Vector2(1, 1)
if has_node("Water"):
$Water.scale = Vector2(1, 1)
# Clean setup: trust the scene file
var entrance = get_node_or_null("NeighborhoodEntrance")
if entrance:
entrance.monitorable = true
entrance.monitoring = true
# Ensure it detects Player (Layer 2) OR Layer 1 just in case
entrance.collision_mask = 3 # Layers 1 and 2
if not entrance.body_entered.is_connected(_on_neighborhood_entrance_body_entered):
entrance.body_entered.connect(_on_neighborhood_entrance_body_entered)
if not entrance.body_exited.is_connected(_on_neighborhood_entrance_body_exited):
entrance.body_exited.connect(_on_neighborhood_entrance_body_exited)
NetworkManager.login_ok.connect(_on_login_ok)
NetworkManager.player_joined.connect(_on_player_joined)
NetworkManager.player_left.connect(_on_player_left)
NetworkManager.player_moved.connect(_on_player_moved)
NetworkManager.disconnected.connect(_on_disconnected)
# Explicitly spawn local player if logged in
if NetworkManager.my_id != -1:
_spawn_player(NetworkManager.my_id, NetworkManager.player_info["name"], 0, 0, NetworkManager.player_info["skin_id"])
for id in NetworkManager.players:
var p = NetworkManager.players[id]
_spawn_player(id, p["name"], p["x"], p["y"], p.get("skin_id", 0))
var chat = preload("res://Scenes/UI/ChatHUD.tscn").instantiate()
add_child(chat)
var player_in_entrance := false
func _on_neighborhood_entrance_body_entered(body):
print("Body ENTERED Trigger: ", body.name, " (My ID: ", NetworkManager.my_id, ")")
# Check if it's the local player
if body.name == str(NetworkManager.my_id):
player_in_entrance = true
print("Player ENTERED entrance area")
func _on_neighborhood_entrance_body_exited(body):
if body.name == str(NetworkManager.my_id):
player_in_entrance = false
print("Player EXITED entrance area")
func _physics_process(_delta):
if player_in_entrance:
# Check for Up input while inside the entrance
if Input.is_action_pressed("ui_up") or Input.is_key_pressed(KEY_W):
_do_room_transition()
func _do_room_transition():
var entrance = get_node_or_null("NeighborhoodEntrance")
if not entrance:
return
var local_player = player_nodes.get(NetworkManager.my_id)
if not local_player:
return
player_in_entrance = false # Prevent re-trigger
NetworkManager.next_spawn_position = local_player.position + Vector2(0, 32)
NetworkManager.send_change_scene("room_" + str(NetworkManager.my_id), Vector2(0, 0))
get_tree().change_scene_to_file("res://Scenes/Room.tscn")
func _fill_background():
# Fill base stone floor (standard blue brick at 0,0)
# Procedural generation for background
# Procedural generation for background
for x in range(-50, 150):
for y in range(-50, 100):
var tile_pos = Vector2i(x, y)
# Plaza in the center (0 to 15)
if x >= 0 and x < 15 and y >= 0 and y < 15:
# Use light gray stone coordinates confirmed in TownTileset.tres
var rand_var = randi() % 3
tile_map.set_cell(tile_pos, 5, Vector2i(4 + rand_var, 0)) # (4,0), (5,0), (6,0)
else:
# Regular floor (blue-ish / darker)
if (x + y) % 2 == 0:
tile_map.set_cell(tile_pos, 5, Vector2i(1, 0))
else:
tile_map.set_cell(tile_pos, 5, Vector2i(2, 0))
func _on_login_ok(_my_id, _players):
pass
func _on_player_joined(id, p_name, x, y, skin_id):
_spawn_player(id, p_name, x, y, skin_id)
func _on_player_left(id):
if player_nodes.has(id):
player_nodes[id].queue_free()
player_nodes.erase(id)
func _on_player_moved(id, x, y, flip_h, frame):
if player_nodes.has(id):
player_nodes[id].set_remote_state(Vector2(x, y), flip_h, frame)
func _on_disconnected():
get_tree().change_scene_to_file("res://Scenes/StartMenu.tscn")
func _spawn_player(id, _p_name, x, y, _skin_id):
# Anti-Ghosting: Check if this player already exists in the tree
if players_container.has_node(str(id)):
var old_node = players_container.get_node(str(id))
old_node.name = "Ghost_" + str(id) # Rename to avoid duplicate name error during queue_free
old_node.queue_free()
# Also clean dictionary
if player_nodes.has(id):
player_nodes.erase(id)
var p = preload("res://Scenes/Player.tscn").instantiate()
p.name = str(id)
# Override position if returning from another scene
var spawn_pos = Vector2(x, y)
if id == NetworkManager.my_id and NetworkManager.next_spawn_position != Vector2.ZERO:
spawn_pos = NetworkManager.next_spawn_position
# Reset after use
NetworkManager.next_spawn_position = Vector2.ZERO
p.position = spawn_pos
p.player_id = id
p.is_local = (id == NetworkManager.my_id)
print("Spawning Player ID: ", id, " Name: ", _p_name, " Is Local: ", p.is_local, " (My ID: ", NetworkManager.my_id, ")")
if p.is_local:
p.interaction_request.connect(_on_interaction_request)
players_container.add_child(p)
player_nodes[id] = p
func _on_interaction_request(target_id, target_name):
print("World received interaction request for: ", target_name)
var chat = get_node_or_null("ChatHUD")
if chat:
if chat.has_method("set_private_target"):
chat.set_private_target(target_id, target_name)
else:
print("ChatHUD missing set_private_target method")

1
Scripts/World.gd.uid Normal file
View File

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