Initial WhaleTown V2 frontend

This commit is contained in:
2026-07-19 22:42:20 +08:00
commit 435c578dde
1421 changed files with 54486 additions and 0 deletions

285
scenes/Maps/CafeInterior.gd Normal file
View File

@@ -0,0 +1,285 @@
class_name CafeInterior
extends Node2D
# ============================================================================
# CafeInterior.gd - 鲸鱼咖啡馆室内场景控制器
# ============================================================================
# 承载咖啡馆挂机服务大厅,负责玩家出生点、相机边界和返回工作区。
# ============================================================================
const CAMERA_ZOOM: Vector2 = Vector2(1.65, 1.65)
const CAMERA_LIMIT_LEFT: int = -768
const CAMERA_LIMIT_TOP: int = -512
const CAMERA_LIMIT_RIGHT: int = 768
const CAMERA_LIMIT_BOTTOM: int = 512
const WORK_ZONE_CAFE_RETURN_POSITION: Vector2 = Vector2(-1093, 560)
const CAFE_DOOR_POSITION: Vector2 = Vector2(0, 392)
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
const COMPANION_NAMEPLATE_RENDER_SCALE: float = 0.5
const COMPANION_NAMEPLATE_FONT_SIZE: int = 24
const COMPANION_NAMEPLATE_VISUAL_HEIGHT: int = 22
const COMPANION_NAMEPLATE_VISUAL_MIN_WIDTH: int = 76
const COMPANION_NAMEPLATE_VISUAL_MAX_WIDTH: int = 118
const COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
const NPC_NAMEPLATE_OFFSET_Y: float = -136.0
const HIRED_PLAYER_NAMEPLATE_OFFSET_Y: float = -96.0
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
@onready var hiredPlayerCompanionTarget: CafeCompanionTarget = $YSortWorld/Characters/Players/Player/CafeHiredCompanionTarget
@onready var hiredPlayerNameplate: Label = $YSortWorld/Characters/Players/Player/CafeHiredCompanionNameplate
@onready var exitToWorkZoneArea: Area2D = $InteractionAreas/ExitToWorkZoneArea
@onready var cafeRecruitmentLogoArea: Area2D = $InteractionAreas/CafeRecruitmentLogoArea
@onready var serviceIdlePoint01: Marker2D = $Markers/ServiceIdlePoints/ServiceIdlePoint01
@onready var cafeWhaleBaristaNpc: NPCController = $YSortWorld/Characters/CafeWhaleBaristaNpc
@onready var cafeCompanionNameplate: Label = $YSortWorld/Characters/CafeWhaleBaristaNpc/CafeCompanionNameplate
var _isChangingScene: bool = false
var _lastRecruitmentClickMsec: int = 0
func _ready() -> void:
_align_service_occupants()
_configure_static_companion_nameplates()
_apply_spawn_point()
_configure_camera()
_connect_exit_area()
_connect_recruitment_area()
_connect_cafe_companion_events()
func _exit_tree() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem != null:
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_cafe_companion_agent_registered, self)
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_cafe_companion_employment_resigned, self)
func _align_service_occupants() -> void:
cafeWhaleBaristaNpc.global_position = serviceIdlePoint01.global_position
func _apply_spawn_point() -> void:
var spawnName: String = SceneManager.get_next_spawn_name()
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
var marker := $Markers.get_node_or_null(markerName) as Marker2D
if marker == null:
marker = $Markers/DefaultSpawn
player.global_position = marker.global_position
func _configure_camera() -> void:
playerCamera.enabled = true
playerCamera.make_current()
playerCamera.zoom = CAMERA_ZOOM
playerCamera.position_smoothing_enabled = true
playerCamera.limit_left = CAMERA_LIMIT_LEFT
playerCamera.limit_top = CAMERA_LIMIT_TOP
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
playerCamera.limit_smoothed = true
func _connect_exit_area() -> void:
if not exitToWorkZoneArea.body_entered.is_connected(_on_exit_area_body_entered):
exitToWorkZoneArea.body_entered.connect(_on_exit_area_body_entered)
func _connect_recruitment_area() -> void:
cafeRecruitmentLogoArea.input_pickable = true
if not cafeRecruitmentLogoArea.input_event.is_connected(_on_recruitment_area_input_event):
cafeRecruitmentLogoArea.input_event.connect(_on_recruitment_area_input_event)
set_process_unhandled_input(true)
func _connect_cafe_companion_events() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem == null:
return
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_cafe_companion_agent_registered, self)
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_cafe_companion_employment_resigned, self)
func _on_exit_area_body_entered(body: Node2D) -> void:
if _isChangingScene or body != player:
return
_isChangingScene = true
SceneManager.set_next_scene_position(WORK_ZONE_CAFE_RETURN_POSITION)
SceneManager.change_scene("work_zone")
func _on_recruitment_area_input_event(_viewport: Viewport, event: InputEvent, _shapeIdx: int) -> void:
if not _is_primary_mouse_click(event):
return
get_viewport().set_input_as_handled()
_try_emit_recruitment_selected()
func _unhandled_input(event: InputEvent) -> void:
if not _is_primary_mouse_click(event):
return
if not _area_contains_global_point(cafeRecruitmentLogoArea, get_global_mouse_position()):
return
get_viewport().set_input_as_handled()
_try_emit_recruitment_selected()
func _try_emit_recruitment_selected() -> void:
var now := Time.get_ticks_msec()
if _lastRecruitmentClickMsec > 0 and now - _lastRecruitmentClickMsec < 120:
return
_lastRecruitmentClickMsec = now
_emit_recruitment_selected()
func _emit_recruitment_selected() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem == null:
return
eventSystem.call("emit_event", EventNames.CAFE_COMPANION_RECRUITMENT_SELECTED, {
"cafe_id": "whale_cafe",
"terminal_id": "front_reception_whale_logo",
"title": "咖啡店陪伴招聘",
"anchor_position": cafeRecruitmentLogoArea.global_position,
})
func _is_primary_mouse_click(event: InputEvent) -> bool:
if not (event is InputEventMouseButton):
return false
var mouseEvent := event as InputEventMouseButton
return mouseEvent.button_index == MOUSE_BUTTON_LEFT and mouseEvent.pressed
func _area_contains_global_point(area: Area2D, globalPoint: Vector2) -> bool:
for child in area.get_children():
var shapeNode := child as CollisionShape2D
if shapeNode == null or shapeNode.disabled or shapeNode.shape == null:
continue
if _shape_contains_point(shapeNode, globalPoint):
return true
return false
func _shape_contains_point(shapeNode: CollisionShape2D, globalPoint: Vector2) -> bool:
var localPoint := shapeNode.to_local(globalPoint)
var shape := shapeNode.shape
if shape is RectangleShape2D:
var rectangle := shape as RectangleShape2D
return Rect2(rectangle.size * -0.5, rectangle.size).has_point(localPoint)
if shape is CircleShape2D:
var circle := shape as CircleShape2D
return localPoint.length() <= circle.radius
return false
func _on_cafe_companion_agent_registered(data: Dictionary) -> void:
var companionData := _extract_companion_data(data)
if companionData.is_empty():
return
var servicePointId := str(companionData.get("service_point_id", "")).strip_edges()
var servicePointMarker := _service_point_marker(servicePointId)
if servicePointMarker == null:
push_warning("CafeInterior: 服务点不存在,无法摆放被雇佣玩家: %s" % servicePointId)
return
var personaName := str(companionData.get("persona_name", "陪伴机器人")).strip_edges()
player.global_position = servicePointMarker.global_position
player.velocity = Vector2.ZERO
player.lastDirection = "down"
player.set_movement_locked(true)
player.call("_play_idle_animation")
player.call("_update_world_sort_z")
_configure_hired_player_companion_target(companionData, servicePointId, personaName)
_configure_hired_player_nameplate(personaName)
func _on_cafe_companion_employment_resigned(_data: Dictionary) -> void:
player.set_movement_locked(false)
_disable_hired_player_companion_target()
hiredPlayerNameplate.visible = false
player.global_position = CAFE_DOOR_POSITION
player.velocity = Vector2.ZERO
player.lastDirection = "down"
player.call("_play_idle_animation")
player.call("_update_world_sort_z")
func _extract_companion_data(data: Dictionary) -> Dictionary:
var companionVariant: Variant = data.get("companion", {})
if companionVariant is Dictionary:
return companionVariant
if data.has("service_point_id"):
return data
return {}
func _service_point_marker(servicePointId: String) -> Marker2D:
if servicePointId.is_empty():
return null
return $Markers/ServiceIdlePoints.get_node_or_null(servicePointId) as Marker2D
func _configure_hired_player_companion_target(companionData: Dictionary, servicePointId: String, personaName: String) -> void:
hiredPlayerCompanionTarget.servicePointId = servicePointId
hiredPlayerCompanionTarget.companionId = str(companionData.get("id", "")).strip_edges()
if hiredPlayerCompanionTarget.companionId.is_empty():
hiredPlayerCompanionTarget.companionId = str(companionData.get("companion_id", "")).strip_edges()
hiredPlayerCompanionTarget.companionType = str(companionData.get("companion_type", "hired_player")).strip_edges()
hiredPlayerCompanionTarget.personaName = personaName
hiredPlayerCompanionTarget.ownerUserId = str(companionData.get("owner_user_id", "")).strip_edges()
hiredPlayerCompanionTarget.employmentEndsAt = str(companionData.get("employment_ends_at", "")).strip_edges()
hiredPlayerCompanionTarget.selfTarget = true
hiredPlayerCompanionTarget.input_pickable = true
var shapeNode := hiredPlayerCompanionTarget.get_node_or_null("CollisionShape2D") as CollisionShape2D
if shapeNode != null:
shapeNode.disabled = false
func _disable_hired_player_companion_target() -> void:
hiredPlayerCompanionTarget.input_pickable = false
hiredPlayerCompanionTarget.selfTarget = false
var shapeNode := hiredPlayerCompanionTarget.get_node_or_null("CollisionShape2D") as CollisionShape2D
if shapeNode != null:
shapeNode.disabled = true
func _configure_hired_player_nameplate(personaName: String) -> void:
_configure_companion_nameplate(hiredPlayerNameplate, personaName, HIRED_PLAYER_NAMEPLATE_OFFSET_Y)
hiredPlayerNameplate.visible = true
func _configure_static_companion_nameplates() -> void:
_configure_companion_nameplate(cafeCompanionNameplate, cafeWhaleBaristaNpc.npcName, NPC_NAMEPLATE_OFFSET_Y)
cafeCompanionNameplate.visible = true
func _configure_companion_nameplate(label: Label, personaName: String, offsetY: float) -> void:
var displayName := personaName.strip_edges()
if displayName.is_empty():
displayName = "陪伴机器人"
var visualWidth := _companion_nameplate_visual_width(displayName)
var renderWidth := ceili(float(visualWidth) / COMPANION_NAMEPLATE_RENDER_SCALE)
var renderHeight := ceili(float(COMPANION_NAMEPLATE_VISUAL_HEIGHT) / COMPANION_NAMEPLATE_RENDER_SCALE)
label.theme = WORLD_TEXT_THEME
label.text = displayName
label.visible = true
label.z_index = 30
label.scale = Vector2.ONE * COMPANION_NAMEPLATE_RENDER_SCALE
label.position = Vector2(float(visualWidth) * -0.5, offsetY)
label.custom_minimum_size = Vector2(renderWidth, renderHeight)
label.size = label.custom_minimum_size
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.clip_text = true
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_color_override("font_color", Color(0.12, 0.20, 0.24, 1.0))
label.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
label.add_theme_constant_override("shadow_offset_x", 0)
label.add_theme_constant_override("shadow_offset_y", 1)
label.add_theme_font_size_override("font_size", COMPANION_NAMEPLATE_FONT_SIZE)
label.add_theme_stylebox_override("normal", _create_companion_nameplate_style())
func _companion_nameplate_visual_width(displayName: String) -> int:
var estimatedWidth := displayName.length() * COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH + 28
return clampi(estimatedWidth, COMPANION_NAMEPLATE_VISUAL_MIN_WIDTH, COMPANION_NAMEPLATE_VISUAL_MAX_WIDTH)
func _create_companion_nameplate_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(1.0, 0.988, 0.955, 0.96)
style.border_color = Color(0.18, 0.34, 0.38, 0.92)
style.border_width_left = 2
style.border_width_top = 2
style.border_width_right = 2
style.border_width_bottom = 2
style.corner_radius_top_left = 12
style.corner_radius_top_right = 12
style.corner_radius_bottom_left = 12
style.corner_radius_bottom_right = 12
style.content_margin_left = 12
style.content_margin_top = 4
style.content_margin_right = 12
style.content_margin_bottom = 4
style.shadow_color = Color(0.05, 0.08, 0.09, 0.18)
style.shadow_size = 4
style.shadow_offset = Vector2(0, 2)
return style

View File

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

View File

@@ -0,0 +1,539 @@
extends Node
# ============================================================================
# MapMultiplayerController.gd - 地图远端玩家同步
# ============================================================================
# 负责把本地玩家位置发送给聊天 WebSocket并把后端位置广播渲染为远端玩家。
# ============================================================================
const REMOTE_PLAYER_SCENE: PackedScene = preload("res://scenes/characters/remote_player.tscn")
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
const DEFAULT_MAP_ID: String = "whale_port"
const POSITION_SEND_INTERVAL: float = 0.12
const MIN_POSITION_DELTA: float = 4.0
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
const PRIVATE_CHAT_INTERACTION_DISTANCE: float = 150.0
const PRIVATE_CHAT_FORWARD_DOT_THRESHOLD: float = 0.25
@export var map_id: String = DEFAULT_MAP_ID
@export var local_player_path: NodePath = NodePath("../Characters/Players/Player")
@export var remote_players_root_path: NodePath = NodePath("../Characters/Players/RemotePlayers")
var _local_player: Node2D
var _remote_players_root: Node2D
var _remote_players: Dictionary = {}
var _pending_remote_players: Dictionary = {}
var _last_sent_position: Vector2 = Vector2.INF
var _last_sent_at_msec: int = 0
var _last_private_chat_interaction_msec: int = 0
var _last_friend_request_interaction_msec: int = 0
func _ready() -> void:
_local_player = get_node_or_null(local_player_path) as Node2D
_remote_players_root = get_node_or_null(remote_players_root_path) as Node2D
if _local_player == null:
push_warning("MapMultiplayerController: local player not found.")
if _remote_players_root == null:
push_warning("MapMultiplayerController: remote players root not found.")
var eventSystem := _get_event_system()
if eventSystem == null:
push_warning("MapMultiplayerController: EventSystem autoload is not available.")
return
eventSystem.call("connect_event", EventNames.PLAYER_MOVED, _on_local_player_moved, self)
eventSystem.call("connect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self)
eventSystem.call("connect_event", EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, _on_remote_players_snapshot_ready, self)
eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_POSITION_UPDATED, _on_remote_player_position_updated, self)
eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_JOINED, _on_remote_player_joined, self)
eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_LEFT, _on_remote_player_left, self)
eventSystem.call("connect_event", EventNames.REMOTE_SKIN_READY, _on_remote_skin_ready, self)
eventSystem.call("connect_event", EventNames.REMOTE_SKIN_FAILED, _on_remote_skin_failed, self)
eventSystem.call("connect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
eventSystem.call("connect_event", EventNames.INTERACT_PRESSED, _on_interact_pressed, self)
call_deferred("_send_world_ready")
func _process(_delta: float) -> void:
if Input.is_action_just_pressed("interact"):
_try_start_private_chat()
if Input.is_action_just_pressed("friend_request"):
_try_request_friend()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
if _try_start_private_chat():
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("friend_request") and _try_request_friend():
get_viewport().set_input_as_handled()
func _exit_tree() -> void:
var eventSystem := _get_event_system()
if eventSystem == null:
return
eventSystem.call("disconnect_event", EventNames.PLAYER_MOVED, _on_local_player_moved, self)
eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self)
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, _on_remote_players_snapshot_ready, self)
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_POSITION_UPDATED, _on_remote_player_position_updated, self)
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_JOINED, _on_remote_player_joined, self)
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_LEFT, _on_remote_player_left, self)
eventSystem.call("disconnect_event", EventNames.REMOTE_SKIN_READY, _on_remote_skin_ready, self)
eventSystem.call("disconnect_event", EventNames.REMOTE_SKIN_FAILED, _on_remote_skin_failed, self)
eventSystem.call("disconnect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
eventSystem.call("disconnect_event", EventNames.INTERACT_PRESSED, _on_interact_pressed, self)
func _get_event_system() -> Node:
return get_node_or_null("/root/EventSystem")
func _get_chat_manager() -> Node:
return get_node_or_null("/root/ChatManager")
func _get_settings_manager() -> Node:
return get_node_or_null("/root/SettingsManager")
func _find_chat_ui() -> Control:
var current: Node = self
while current != null:
var chat_ui := current.get_node_or_null("UILayer/ChatUI") as Control
if chat_ui != null:
return chat_ui
current = current.get_parent()
return null
func _is_text_input_focused() -> bool:
var focusOwner: Control = get_viewport().gui_get_focus_owner()
if focusOwner == null or not focusOwner.is_inside_tree() or not focusOwner.is_visible_in_tree():
return false
if focusOwner is LineEdit:
var lineEdit := focusOwner as LineEdit
return lineEdit.editable
if focusOwner is TextEdit:
var textEdit := focusOwner as TextEdit
return textEdit.editable
return false
func _on_interact_pressed(_data: Dictionary = {}) -> void:
_try_start_private_chat()
func _try_start_private_chat() -> bool:
if _is_text_input_focused():
return false
if not _settings_bool("allow_nearby_private", true):
return false
var now := Time.get_ticks_msec()
if _last_private_chat_interaction_msec > 0 and now - _last_private_chat_interaction_msec < 160:
return false
var target := _find_private_chat_target()
if target == null:
return false
var eventSystem := _get_event_system()
if eventSystem == null:
return false
_last_private_chat_interaction_msec = now
eventSystem.call("emit_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, {
"userId": str(target.get("userId")),
"username": str(target.get("username"))
})
return true
func _try_request_friend() -> bool:
if _is_text_input_focused():
return false
if not _settings_bool("allow_nearby_friend_requests", true):
return false
var now := Time.get_ticks_msec()
if _last_friend_request_interaction_msec > 0 and now - _last_friend_request_interaction_msec < 220:
return false
var target := _find_private_chat_target()
if target == null:
return false
var chat_ui := _find_chat_ui()
if chat_ui == null or not chat_ui.has_method("request_friend_with_target"):
return false
_last_friend_request_interaction_msec = now
return bool(chat_ui.call(
"request_friend_with_target",
str(target.get("userId")),
str(target.get("username"))
))
func _on_chat_login_success(_data: Dictionary) -> void:
_send_world_ready()
func _on_local_player_moved(data: Dictionary) -> void:
var position_variant: Variant = data.get("position", null)
if position_variant is Vector2:
_send_position(position_variant as Vector2)
func _send_world_ready() -> void:
if _local_player == null:
return
var chatManager := _get_chat_manager()
if chatManager == null or not chatManager.has_method("mark_world_ready"):
return
chatManager.call("mark_world_ready", _get_map_id(), _local_player.global_position)
func _send_position(position: Vector2, force: bool = false) -> void:
var now := Time.get_ticks_msec()
if not force and now - _last_sent_at_msec < int(POSITION_SEND_INTERVAL * 1000.0):
return
if not force and _last_sent_position != Vector2.INF and _last_sent_position.distance_to(position) < MIN_POSITION_DELTA:
return
var chatManager := _get_chat_manager()
if chatManager == null:
return
if chatManager.has_method("can_attempt_chat_connection") and not bool(chatManager.call("can_attempt_chat_connection")):
return
_last_sent_position = position
_last_sent_at_msec = now
chatManager.call("update_player_position", position.x, position.y, _get_map_id())
func _on_remote_player_joined(data: Dictionary) -> void:
if not _is_event_for_current_map(data):
return
var user_id := str(data.get("userId", "")).strip_edges()
if user_id.is_empty():
return
var remote_player := _ensure_remote_player(user_id, data)
var position_variant: Variant = data.get("position", null)
if remote_player != null and position_variant is Vector2 and remote_player.has_method("update_position"):
remote_player.call("update_position", position_variant as Vector2)
func _on_remote_players_snapshot_ready(data: Dictionary) -> void:
if not _is_event_for_current_map(data):
return
var seen_user_ids: Dictionary = {}
var players_variant: Variant = data.get("players", [])
if players_variant is Array:
for player_variant in players_variant:
if not (player_variant is Dictionary):
continue
var player_data: Dictionary = player_variant
var user_id := str(player_data.get("userId", "")).strip_edges()
if user_id.is_empty() or _is_current_user(user_id, str(player_data.get("username", ""))):
continue
seen_user_ids[user_id] = true
var remote_player := _ensure_remote_player(user_id, player_data)
var position_variant: Variant = player_data.get("position", null)
if remote_player != null and position_variant is Vector2 and remote_player.has_method("update_position"):
remote_player.call("update_position", position_variant as Vector2)
for user_id_variant in _remote_players.keys().duplicate():
var user_id := str(user_id_variant)
if seen_user_ids.has(user_id):
continue
var remote_player := _remote_players[user_id] as Node
_remote_players.erase(user_id)
if is_instance_valid(remote_player):
remote_player.queue_free()
for user_id_variant in _pending_remote_players.keys().duplicate():
var pendingUserId := str(user_id_variant)
if not seen_user_ids.has(pendingUserId):
_pending_remote_players.erase(pendingUserId)
func _on_remote_player_position_updated(data: Dictionary) -> void:
if not _is_event_for_current_map(data):
return
var user_id := str(data.get("userId", "")).strip_edges()
if user_id.is_empty():
return
if _is_current_user(user_id, str(data.get("username", ""))):
return
var position_variant: Variant = data.get("position", null)
if not (position_variant is Vector2):
return
var remote_player := _ensure_remote_player(user_id, data)
if remote_player != null and remote_player.has_method("update_position"):
remote_player.call("update_position", position_variant as Vector2)
func _on_remote_player_left(data: Dictionary) -> void:
if not _is_event_for_current_map(data):
return
var user_id := str(data.get("userId", "")).strip_edges()
if user_id.is_empty():
return
_pending_remote_players.erase(user_id)
if not _remote_players.has(user_id):
return
var remote_player := _remote_players[user_id] as Node
_remote_players.erase(user_id)
if is_instance_valid(remote_player):
remote_player.queue_free()
func _on_chat_message_received(data: Dictionary) -> void:
if not bool(data.get("show_bubble", false)):
return
if not _settings_bool("show_chat_bubbles", true):
return
var content := str(data.get("content", "")).strip_edges()
if content.is_empty():
return
var target := _resolve_chat_bubble_target(data)
if target == null:
return
_show_chat_bubble(target, content)
func _ensure_remote_player(user_id: String, data: Dictionary) -> Node2D:
if _is_current_user(user_id, str(data.get("username", ""))):
return null
var appearanceManager := get_node_or_null("/root/AppearanceManager")
var skinId := str(data.get("skin_id", data.get("skinId", ""))).strip_edges()
var skinAssetVariant: Variant = data.get("skin_asset", data.get("skinAsset", {}))
var skinAsset: Dictionary = skinAssetVariant if skinAssetVariant is Dictionary else {}
if appearanceManager != null and appearanceManager.has_method("is_remote_skin_ready"):
if not bool(appearanceManager.call("is_remote_skin_ready", skinId, skinAsset)):
_queue_pending_remote_player(user_id, data)
if _remote_players.has(user_id):
var loadingPlayer := _remote_players[user_id] as Node2D
if is_instance_valid(loadingPlayer):
loadingPlayer.hide()
if appearanceManager.has_method("request_remote_skin"):
appearanceManager.call("request_remote_skin", skinId, skinAsset)
return null
if _remote_players.has(user_id):
var existing_player := _remote_players[user_id] as Node2D
if is_instance_valid(existing_player):
_apply_remote_player_metadata(existing_player, data)
existing_player.show()
return existing_player
if _remote_players_root == null:
return null
var remote_player := REMOTE_PLAYER_SCENE.instantiate() as Node2D
if remote_player == null:
return null
remote_player.name = "RemotePlayer_%s" % user_id
remote_player.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_remote_players_root.add_child(remote_player)
_remote_players[user_id] = remote_player
if remote_player.has_method("setup"):
var setup_data := data.duplicate(true)
if not setup_data.has("position"):
setup_data["position"] = {"x": _local_player.global_position.x if _local_player != null else 0.0, "y": _local_player.global_position.y if _local_player != null else 0.0}
remote_player.call("setup", setup_data)
return remote_player
func _queue_pending_remote_player(userId: String, data: Dictionary) -> void:
var merged: Dictionary = (_pending_remote_players.get(userId, {}) as Dictionary).duplicate(true)
for key in data.keys():
merged[key] = data[key]
_pending_remote_players[userId] = merged
func _on_remote_skin_ready(data: Dictionary) -> void:
var skinId := str(data.get("skin_id", "")).strip_edges()
if skinId.is_empty():
return
for userIdVariant in _pending_remote_players.keys().duplicate():
var userId := str(userIdVariant)
var pendingData: Dictionary = _pending_remote_players.get(userId, {})
var pendingSkinId := str(pendingData.get("skin_id", pendingData.get("skinId", ""))).strip_edges()
if pendingSkinId != skinId:
continue
_pending_remote_players.erase(userId)
var remotePlayer := _ensure_remote_player(userId, pendingData)
var positionVariant: Variant = pendingData.get("position", null)
if remotePlayer != null and positionVariant is Vector2 and remotePlayer.has_method("update_position"):
remotePlayer.call("update_position", positionVariant as Vector2)
func _on_remote_skin_failed(data: Dictionary) -> void:
var skinId := str(data.get("skin_id", "")).strip_edges()
if skinId.is_empty():
return
for userIdVariant in _pending_remote_players.keys().duplicate():
var userId := str(userIdVariant)
var pendingData: Dictionary = _pending_remote_players.get(userId, {})
var pendingSkinId := str(pendingData.get("skin_id", pendingData.get("skinId", ""))).strip_edges()
if pendingSkinId != skinId:
continue
_pending_remote_players.erase(userId)
var fallbackData := pendingData.duplicate(true)
fallbackData["skin_id"] = ""
fallbackData["skinId"] = ""
fallbackData.erase("skin_asset")
fallbackData.erase("skinAsset")
var remotePlayer := _ensure_remote_player(userId, fallbackData)
var positionVariant: Variant = pendingData.get("position", null)
if remotePlayer != null and positionVariant is Vector2 and remotePlayer.has_method("update_position"):
remotePlayer.call("update_position", positionVariant as Vector2)
func _find_private_chat_target() -> Node2D:
if _local_player == null or _remote_players.is_empty():
return null
var local_position := _local_player.global_position
var facing_direction := _get_local_facing_direction()
var best_forward_target: Node2D = null
var best_forward_distance := INF
var best_any_target: Node2D = null
var best_any_distance := INF
for remote_player_variant in _remote_players.values():
var remote_player := remote_player_variant as Node2D
if not is_instance_valid(remote_player):
continue
var offset := remote_player.global_position - local_position
var distance := offset.length()
if distance > PRIVATE_CHAT_INTERACTION_DISTANCE:
continue
if distance < best_any_distance:
best_any_distance = distance
best_any_target = remote_player
if distance <= 0.01:
continue
var forward_dot := facing_direction.dot(offset.normalized())
if forward_dot >= PRIVATE_CHAT_FORWARD_DOT_THRESHOLD and distance < best_forward_distance:
best_forward_distance = distance
best_forward_target = remote_player
return best_forward_target if best_forward_target != null else best_any_target
func _get_local_facing_direction() -> Vector2:
if _local_player != null:
var last_direction := str(_local_player.get("lastDirection"))
match last_direction:
"up":
return Vector2.UP
"down":
return Vector2.DOWN
"left":
return Vector2.LEFT
"right":
return Vector2.RIGHT
_:
pass
return Vector2.DOWN
func _apply_remote_player_metadata(remote_player: Node2D, data: Dictionary) -> void:
var username := str(data.get("username", "")).strip_edges()
if not username.is_empty():
remote_player.set("username", username)
if data.has("skin_id") or data.has("skinId") or data.has("skin_asset") or data.has("skinAsset") or data.has("avatar_id") or data.has("avatarId") or data.has("cafe_companion") or data.has("cafeCompanion"):
if remote_player.has_method("setup"):
remote_player.call("setup", data)
func _resolve_chat_bubble_target(data: Dictionary) -> Node2D:
var from_user := str(data.get("from_user", data.get("username", ""))).strip_edges()
var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges()
var is_self := bool(data.get("is_self", false))
if is_self or (not user_id.is_empty() and _is_current_user(user_id, from_user)) or _is_current_username(from_user):
return _local_player
if not user_id.is_empty() and _remote_players.has(user_id):
var remote_by_id := _remote_players[user_id] as Node2D
if is_instance_valid(remote_by_id):
return remote_by_id
if from_user.is_empty():
return null
for remote_player_variant in _remote_players.values():
var remote_player := remote_player_variant as Node2D
if not is_instance_valid(remote_player):
continue
var remote_username := str(remote_player.get("username")).strip_edges()
if not remote_username.is_empty() and remote_username == from_user:
return remote_player
return null
func _show_chat_bubble(target: Node2D, text: String) -> void:
var bubble := CHAT_BUBBLE_SCENE.instantiate() as Control
if bubble == null:
return
var bubble_layer := _get_chat_bubble_layer()
bubble_layer.add_child(bubble)
if bubble.has_method("set_text"):
bubble.call("set_text", text, target, CHAT_BUBBLE_TARGET_OFFSET)
func _get_chat_bubble_layer() -> CanvasLayer:
return _get_or_create_canvas_layer(CHAT_BUBBLE_LAYER_NAME, 20)
func _get_or_create_canvas_layer(layerName: String, layerIndex: int) -> CanvasLayer:
var root := get_tree().root
var layer := root.get_node_or_null(layerName) as CanvasLayer
if layer != null:
return layer
layer = CanvasLayer.new()
layer.name = layerName
layer.layer = layerIndex
root.add_child(layer)
return layer
func _get_map_id() -> String:
var configured_map_id := map_id.strip_edges()
return configured_map_id if not configured_map_id.is_empty() else DEFAULT_MAP_ID
func _is_event_for_current_map(data: Dictionary) -> bool:
var event_map_id := str(data.get("mapId", data.get("map_id", ""))).strip_edges()
return event_map_id.is_empty() or event_map_id == _get_map_id()
func _is_current_user(user_id: String, username: String = "") -> bool:
var authManager := get_node_or_null("/root/AuthManager")
if authManager == null:
return false
var user: Dictionary = authManager.call("get_current_user")
var current_id := str(user.get("id", user.get("userId", user.get("user_id", "")))).strip_edges()
if not current_id.is_empty() and user_id == current_id:
return true
var current_username := str(user.get("username", "")).strip_edges()
return not current_username.is_empty() and not username.strip_edges().is_empty() and current_username == username.strip_edges()
func _is_current_username(username: String) -> bool:
var normalized_username := username.strip_edges()
if normalized_username.is_empty():
return false
var authManager := get_node_or_null("/root/AuthManager")
if authManager != null:
var user: Dictionary = authManager.call("get_current_user")
var current_username := str(user.get("username", "")).strip_edges()
if not current_username.is_empty() and current_username == normalized_username:
return true
var chatManager := _get_chat_manager()
if chatManager != null:
var chat_username := str(chatManager.get("_current_username")).strip_edges()
if not chat_username.is_empty() and chat_username == normalized_username:
return true
return false
func _settings_bool(key: String, defaultValue: bool) -> bool:
var settingsManager := _get_settings_manager()
if settingsManager != null and settingsManager.has_method("get_bool"):
return bool(settingsManager.call("get_bool", key))
return defaultValue

View File

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

View File

@@ -0,0 +1,808 @@
class_name PersonalSpace
extends Node2D
# ============================================================================
# PersonalSpace.gd - 个人空间场景控制器
# ============================================================================
# 独立于广场的私人房间空间。当前版本负责固定镜头、玩家出生点和
# 后续房间 manifest/装修系统的场景承载。
# ============================================================================
const CAMERA_ZOOM: Vector2 = Vector2(1.65, 1.65)
const CAMERA_LIMIT_LEFT: int = -538
const CAMERA_LIMIT_TOP: int = -358
const CAMERA_LIMIT_RIGHT: int = 538
const CAMERA_LIMIT_BOTTOM: int = 358
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
const TEXT_COLOR: Color = Color(0.102, 0.224, 0.376)
const PANEL_COLOR: Color = Color(0.970, 0.992, 1.0, 0.96)
const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922)
const MUTED_COLOR: Color = Color(0.400, 0.500, 0.620)
const DECOR_TEXTURE_FILTER: int = CanvasItem.TEXTURE_FILTER_LINEAR
const DECOR_COLLISION_LAYER: int = 1
const DECOR_COLLISION_MASK: int = 1
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
@onready var uiLayer: CanvasLayer = $UILayer
@onready var ySortWorld: Node2D = $YSortWorld
@onready var staticCollision: Node2D = $StaticCollision
var _decorLayer: Node2D
var _inventoryRequest: HTTPRequest
var _inventoryPanel: PanelContainer
var _inventoryGrid: GridContainer
var _dragSurface: Control
var _toastLabel: Label
var _selectedDecorId: String = ""
var _draggedDecor: Sprite2D
var _dragOffset: Vector2 = Vector2.ZERO
var _decorItems: Dictionary = {}
var _decorNodes: Dictionary = {}
var _decorCollisionBodies: Dictionary = {}
var _remoteDecorDefinitions: Dictionary = {}
var _inventoryRequestInFlight: bool = false
var _inventoryRequestReportsErrors: bool = false
func _ready() -> void:
_leave_multiplayer_world()
_apply_spawn_point()
_configure_camera()
_build_decor_layer()
_build_room_decor_requests()
_build_room_decor_ui()
_connect_room_decor_events()
_fetch_room_decor_inventory()
set_process(false)
func _leave_multiplayer_world() -> void:
var chatManager := get_node_or_null("/root/ChatManager")
if chatManager != null and chatManager.has_method("leave_world"):
chatManager.call("leave_world", "personal_space")
func _exit_tree() -> void:
var eventSystem := _get_event_system()
if eventSystem != null:
eventSystem.call("disconnect_event", EventNames.HUD_BACKPACK_TOGGLE, _on_backpack_toggle_requested, self)
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
if saveManager != null:
if saveManager.decor_save_succeeded.is_connected(_on_decor_save_succeeded):
saveManager.decor_save_succeeded.disconnect(_on_decor_save_succeeded)
if saveManager.decor_save_failed.is_connected(_on_decor_save_failed):
saveManager.decor_save_failed.disconnect(_on_decor_save_failed)
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
var mouseEvent := event as InputEventMouseButton
if mouseEvent.button_index != MOUSE_BUTTON_LEFT:
return
if _draggedDecor != null and not mouseEvent.pressed:
_finish_decor_drag()
get_viewport().set_input_as_handled()
elif _draggedDecor == null and mouseEvent.pressed and not _is_pointer_over_blocking_ui():
if _try_begin_decor_drag(get_global_mouse_position()):
get_viewport().set_input_as_handled()
elif event is InputEventMouseMotion and _draggedDecor != null:
_update_dragged_decor_position()
get_viewport().set_input_as_handled()
func _process(_delta: float) -> void:
if _draggedDecor != null:
_update_dragged_decor_position()
func _apply_spawn_point() -> void:
var spawnName: String = SceneManager.get_next_spawn_name()
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
var marker := $Markers.get_node_or_null(markerName) as Marker2D
if marker == null:
marker = $Markers/DefaultSpawn
player.global_position = marker.global_position
func _configure_camera() -> void:
playerCamera.zoom = CAMERA_ZOOM
playerCamera.position_smoothing_enabled = true
playerCamera.limit_left = CAMERA_LIMIT_LEFT
playerCamera.limit_top = CAMERA_LIMIT_TOP
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
playerCamera.limit_smoothed = true
func _build_decor_layer() -> void:
_decorLayer = Node2D.new()
_decorLayer.name = "RoomDecorLayer"
_decorLayer.y_sort_enabled = true
ySortWorld.add_child(_decorLayer)
ySortWorld.move_child(_decorLayer, 0)
func _build_room_decor_requests() -> void:
_inventoryRequest = HTTPRequest.new()
_inventoryRequest.name = "roomDecorInventoryRequest"
_inventoryRequest.timeout = 12.0
_inventoryRequest.request_completed.connect(_on_inventory_request_completed)
add_child(_inventoryRequest)
func _build_room_decor_ui() -> void:
_inventoryPanel = PanelContainer.new()
_inventoryPanel.visible = false
_inventoryPanel.custom_minimum_size = Vector2(560, 560)
_inventoryPanel.offset_left = 24
_inventoryPanel.offset_top = 116
_inventoryPanel.offset_right = 584
_inventoryPanel.offset_bottom = 676
_inventoryPanel.mouse_filter = Control.MOUSE_FILTER_STOP
_inventoryPanel.add_theme_stylebox_override("panel", _create_panel_style(PANEL_COLOR, 26, true))
uiLayer.add_child(_inventoryPanel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_top", 22)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_bottom", 22)
_inventoryPanel.add_child(margin)
var root := VBoxContainer.new()
root.add_theme_constant_override("separation", 14)
margin.add_child(root)
var header := HBoxContainer.new()
header.alignment = BoxContainer.ALIGNMENT_CENTER
header.add_theme_constant_override("separation", 12)
root.add_child(header)
var titleBox := VBoxContainer.new()
titleBox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
titleBox.add_theme_constant_override("separation", 2)
header.add_child(titleBox)
var title := Label.new()
title.text = "背包"
title.add_theme_font_size_override("font_size", 28)
title.add_theme_color_override("font_color", TEXT_COLOR)
titleBox.add_child(title)
var hint := Label.new()
hint.text = "按住装饰品,拖到房间里松手摆放。"
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.add_theme_font_size_override("font_size", 16)
hint.add_theme_color_override("font_color", MUTED_COLOR)
titleBox.add_child(hint)
var closeButton := Button.new()
closeButton.text = "×"
closeButton.custom_minimum_size = Vector2(48, 48)
closeButton.focus_mode = Control.FOCUS_NONE
closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
closeButton.add_theme_font_size_override("font_size", 30)
closeButton.add_theme_color_override("font_color", Color.WHITE)
closeButton.add_theme_stylebox_override("normal", _create_panel_style(ACCENT_COLOR, 18, false))
closeButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.150, 0.680, 1.0, 1.0), 18, false))
closeButton.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.060, 0.420, 0.780, 1.0), 18, false))
closeButton.pressed.connect(func() -> void:
_inventoryPanel.visible = false
)
header.add_child(closeButton)
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(0, 370)
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
root.add_child(scroll)
_inventoryGrid = GridContainer.new()
_inventoryGrid.columns = 2
_inventoryGrid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_inventoryGrid.add_theme_constant_override("h_separation", 14)
_inventoryGrid.add_theme_constant_override("v_separation", 14)
scroll.add_child(_inventoryGrid)
var removeButton := Button.new()
removeButton.text = "收回选中家具"
removeButton.custom_minimum_size = Vector2(0, 46)
removeButton.add_theme_font_size_override("font_size", 17)
removeButton.add_theme_color_override("font_color", TEXT_COLOR)
removeButton.add_theme_color_override("font_hover_color", ACCENT_COLOR)
removeButton.add_theme_color_override("font_pressed_color", Color(0.060, 0.270, 0.540, 1.0))
removeButton.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
removeButton.add_theme_stylebox_override("normal", _create_panel_style(Color(0.900, 0.960, 1.0, 0.92), 16, false))
removeButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.820, 0.925, 1.0, 1.0), 16, false))
removeButton.pressed.connect(_remove_selected_decor)
root.add_child(removeButton)
_toastLabel = Label.new()
_toastLabel.visible = false
_toastLabel.offset_left = 360
_toastLabel.offset_top = 100
_toastLabel.offset_right = 760
_toastLabel.offset_bottom = 148
_toastLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_toastLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_toastLabel.add_theme_font_size_override("font_size", 18)
_toastLabel.add_theme_color_override("font_color", Color.WHITE)
_toastLabel.add_theme_stylebox_override("normal", _create_panel_style(Color(0.028, 0.160, 0.310, 0.90), 18, false))
uiLayer.add_child(_toastLabel)
_dragSurface = Control.new()
_dragSurface.name = "RoomDecorDragSurface"
_dragSurface.visible = false
_dragSurface.set_anchors_preset(Control.PRESET_FULL_RECT)
_dragSurface.mouse_filter = Control.MOUSE_FILTER_STOP
_dragSurface.gui_input.connect(_on_drag_surface_gui_input)
uiLayer.add_child(_dragSurface)
func _connect_room_decor_events() -> void:
var eventSystem := _get_event_system()
if eventSystem == null:
push_warning("PersonalSpace: EventSystem autoload is not available.")
else:
eventSystem.call("connect_event", EventNames.HUD_BACKPACK_TOGGLE, _on_backpack_toggle_requested, self)
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
if saveManager != null:
if not saveManager.decor_save_succeeded.is_connected(_on_decor_save_succeeded):
saveManager.decor_save_succeeded.connect(_on_decor_save_succeeded)
if not saveManager.decor_save_failed.is_connected(_on_decor_save_failed):
saveManager.decor_save_failed.connect(_on_decor_save_failed)
func _on_backpack_toggle_requested(_data: Variant = null) -> void:
_toggle_inventory_panel()
func _toggle_inventory_panel() -> void:
_inventoryPanel.visible = not _inventoryPanel.visible
if _inventoryPanel.visible:
_fetch_room_decor_inventory(true)
func _fetch_room_decor_inventory(reportErrors: bool = false) -> void:
if _inventoryRequestInFlight:
_inventoryRequestReportsErrors = _inventoryRequestReportsErrors or reportErrors
return
if not _is_authenticated():
_apply_local_empty_inventory()
if reportErrors:
_show_toast("请先登录后使用家具背包")
return
_inventoryRequestInFlight = true
_inventoryRequestReportsErrors = reportErrors
var err := _inventoryRequest.request("%s/rooms/me/decor-placements" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_GET, "")
if err != OK:
_inventoryRequestInFlight = false
if _inventoryRequestReportsErrors:
_show_toast("家具背包请求发送失败")
_inventoryRequestReportsErrors = false
func _on_inventory_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_inventoryRequestInFlight = false
var reportErrors := _inventoryRequestReportsErrors
_inventoryRequestReportsErrors = false
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
if reportErrors:
_show_toast("家具背包读取失败")
return
var json := JSON.new()
if json.parse(body.get_string_from_utf8()) != OK:
if reportErrors:
_show_toast("家具背包响应解析失败")
return
var responseVariant: Variant = json.data
if not (responseVariant is Dictionary):
if reportErrors:
_show_toast("家具背包响应格式错误")
return
var response: Dictionary = responseVariant
if not bool(response.get("success", true)):
if reportErrors:
_show_toast(str(response.get("message", "家具背包读取失败")))
return
var dataVariant: Variant = response.get("data", {})
if dataVariant is Dictionary:
_apply_inventory_payload(dataVariant as Dictionary)
func _apply_inventory_payload(data: Dictionary) -> void:
_decorItems.clear()
_apply_remote_decor_definitions(data.get("definitions", []))
var itemsVariant: Variant = data.get("items", [])
if itemsVariant is Array:
for itemVariant in itemsVariant:
if itemVariant is Dictionary:
var item: Dictionary = (itemVariant as Dictionary).duplicate(true)
var decorId := str(item.get("decor_id", "")).strip_edges()
if not decorId.is_empty():
_decorItems[decorId] = _merge_decor_definition(item)
_render_inventory_list()
_render_placed_decors()
func _apply_local_empty_inventory() -> void:
_decorItems.clear()
_render_inventory_list()
_render_placed_decors()
func _merge_decor_definition(item: Dictionary) -> Dictionary:
var decorId := str(item.get("decor_id", "")).strip_edges()
var definition: Dictionary = _remoteDecorDefinitions.get(decorId, {})
for key in definition.keys():
if not item.has(key):
item[key] = definition[key]
return item
func _apply_remote_decor_definitions(definitionsVariant: Variant) -> void:
_remoteDecorDefinitions.clear()
if not (definitionsVariant is Array):
return
for definitionVariant in definitionsVariant:
if not (definitionVariant is Dictionary):
continue
var definition: Dictionary = (definitionVariant as Dictionary).duplicate(true)
var decorId := str(definition.get("decor_id", "")).strip_edges()
if decorId.is_empty():
continue
if definition.get("default_position", null) is Dictionary:
var position: Dictionary = definition.get("default_position")
definition["default_position"] = Vector2(
_to_float(position.get("x", 0.0), 0.0),
_to_float(position.get("y", 0.0), 0.0)
)
_remoteDecorDefinitions[decorId] = definition
func _render_inventory_list() -> void:
_clear_children(_inventoryGrid)
if _decorItems.is_empty():
var empty := _create_empty_inventory_card()
_inventoryGrid.add_child(empty)
return
for decorId in _decorItems.keys():
_inventoryGrid.add_child(_create_inventory_card(str(decorId)))
func _create_empty_inventory_card() -> Control:
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(500, 132)
card.add_theme_stylebox_override("panel", _create_panel_style(Color(1, 1, 1, 0.76), 20, false))
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 18)
margin.add_theme_constant_override("margin_top", 18)
margin.add_theme_constant_override("margin_right", 18)
margin.add_theme_constant_override("margin_bottom", 18)
card.add_child(margin)
var label := Label.new()
label.text = "背包里还没有装饰品。去商城的空间分类购买后会出现在这里。"
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 17)
label.add_theme_color_override("font_color", MUTED_COLOR)
margin.add_child(label)
return card
func _create_inventory_card(decorId: String) -> Button:
var item: Dictionary = _decorItems[decorId]
var card := Button.new()
card.custom_minimum_size = Vector2(240, 176)
card.focus_mode = Control.FOCUS_NONE
card.mouse_default_cursor_shape = Control.CURSOR_DRAG
card.add_theme_stylebox_override("normal", _create_inventory_card_style(false))
card.add_theme_stylebox_override("hover", _create_inventory_card_style(true))
card.add_theme_stylebox_override("pressed", _create_inventory_card_style(true))
card.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
card.button_down.connect(func() -> void:
_begin_inventory_decor_drag(decorId)
)
var margin := MarginContainer.new()
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_top", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_bottom", 12)
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_child(margin)
var content := VBoxContainer.new()
content.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_theme_constant_override("separation", 6)
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
margin.add_child(content)
var status := Label.new()
status.custom_minimum_size = Vector2(0, 22)
status.text = "已摆放" if bool(item.get("placed", false)) else "未摆放"
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
status.add_theme_font_size_override("font_size", 14)
status.add_theme_color_override("font_color", ACCENT_COLOR if bool(item.get("placed", false)) else MUTED_COLOR)
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_child(status)
var icon := TextureRect.new()
icon.texture = _load_texture(str(item.get("icon", "")))
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
icon.custom_minimum_size = Vector2(0, 82)
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_child(icon)
var nameLabel := Label.new()
nameLabel.text = str(item.get("name", decorId))
nameLabel.custom_minimum_size = Vector2(0, 36)
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
nameLabel.add_theme_font_size_override("font_size", 17)
nameLabel.add_theme_color_override("font_color", TEXT_COLOR)
nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_child(nameLabel)
return card
func _render_placed_decors() -> void:
for decorIdVariant in _decorNodes.keys().duplicate():
var decorId := str(decorIdVariant)
var item: Dictionary = _decorItems.get(decorId, {})
if item.is_empty() or not bool(item.get("placed", false)):
var node := _decorNodes[decorId] as Node
if is_instance_valid(node):
node.queue_free()
_decorNodes.erase(decorId)
_remove_decor_collision(decorId)
for decorId in _decorItems.keys():
var item: Dictionary = _decorItems[decorId]
if bool(item.get("placed", false)):
_ensure_decor_node(str(decorId), item)
func _begin_inventory_decor_drag(decorId: String) -> void:
if not _decorItems.has(decorId):
return
_selectedDecorId = decorId
var item := _ensure_decor_placed_for_drag(decorId)
var node := _ensure_decor_node(decorId, item)
if node == null:
return
_draggedDecor = node
_dragOffset = Vector2.ZERO
_set_decor_collision_disabled(decorId, true)
_draggedDecor.z_index = max(_draggedDecor.z_index, 50)
_inventoryPanel.visible = false
_enable_drag_surface()
_update_dragged_decor_position()
_pulse_node(_draggedDecor)
func _ensure_decor_placed_for_drag(decorId: String) -> Dictionary:
var item: Dictionary = _decorItems[decorId]
if not bool(item.get("placed", false)):
item["placed"] = true
var defaultPosition := _variant_to_vector2(item.get("default_position", Vector2.ZERO))
item["position_x"] = defaultPosition.x
item["position_y"] = defaultPosition.y
item["scale"] = _get_item_float(item, "default_scale", 1.0)
item["z_index"] = _get_item_int(item, "default_z_index", 0)
_decorItems[decorId] = item
return item
func _ensure_decor_node(decorId: String, item: Dictionary) -> Sprite2D:
var node := _decorNodes.get(decorId, null) as Sprite2D
if node == null or not is_instance_valid(node):
node = Sprite2D.new()
node.name = "Decor_%s" % decorId
node.texture_filter = DECOR_TEXTURE_FILTER
node.centered = true
node.set_meta("decor_id", decorId)
_decorLayer.add_child(node)
_decorNodes[decorId] = node
else:
node.texture_filter = DECOR_TEXTURE_FILTER
node.texture = _load_texture(str(item.get("texture", item.get("icon", ""))))
node.global_position = Vector2(_to_float(item.get("position_x", 0.0), 0.0), _to_float(item.get("position_y", 0.0), 0.0))
var itemScale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
node.scale = Vector2(itemScale, itemScale)
node.z_index = _get_item_int(item, "z_index", _get_item_int(item, "default_z_index", 0))
_sync_decor_collision(decorId, item)
return node
func _try_begin_decor_drag(worldPosition: Vector2) -> bool:
var bestNode: Sprite2D = null
var bestZ := -100000
for decorId in _decorNodes.keys():
var node := _decorNodes[decorId] as Sprite2D
if node == null or not is_instance_valid(node) or node.texture == null:
continue
if not _is_point_inside_sprite(node, worldPosition):
continue
if node.z_index >= bestZ:
bestNode = node
bestZ = node.z_index
if bestNode == null:
return false
_draggedDecor = bestNode
_selectedDecorId = str(bestNode.get_meta("decor_id", ""))
_dragOffset = bestNode.global_position - worldPosition
_set_decor_collision_disabled(_selectedDecorId, true)
_draggedDecor.z_index = max(_draggedDecor.z_index, 50)
_enable_drag_surface()
return true
func _finish_decor_drag() -> void:
if _draggedDecor == null:
_disable_drag_surface()
return
var decorId := str(_draggedDecor.get_meta("decor_id", ""))
if _decorItems.has(decorId):
var item: Dictionary = _decorItems[decorId]
item["position_x"] = _draggedDecor.global_position.x
item["position_y"] = _draggedDecor.global_position.y
item["placed"] = true
item["scale"] = _draggedDecor.scale.x
item["z_index"] = _get_item_int(item, "default_z_index", 0)
_decorItems[decorId] = item
_draggedDecor.z_index = _get_item_int(item, "z_index", 0)
_sync_decor_collision(decorId, item)
_set_decor_collision_disabled(decorId, false)
_save_decor_item(item)
_render_inventory_list()
_draggedDecor = null
_disable_drag_surface()
func _update_dragged_decor_position() -> void:
if _draggedDecor != null:
_draggedDecor.global_position = get_global_mouse_position() + _dragOffset
var decorId := str(_draggedDecor.get_meta("decor_id", ""))
if not decorId.is_empty() and _decorItems.has(decorId):
var item: Dictionary = _decorItems[decorId]
item["position_x"] = _draggedDecor.global_position.x
item["position_y"] = _draggedDecor.global_position.y
_sync_decor_collision(decorId, item)
func _enable_drag_surface() -> void:
if is_instance_valid(_dragSurface):
_dragSurface.visible = true
uiLayer.move_child(_dragSurface, uiLayer.get_child_count() - 1)
set_process(true)
func _disable_drag_surface() -> void:
if is_instance_valid(_dragSurface):
_dragSurface.visible = false
set_process(false)
func _on_drag_surface_gui_input(event: InputEvent) -> void:
if _draggedDecor == null:
_disable_drag_surface()
return
if event is InputEventMouseMotion:
_update_dragged_decor_position()
get_viewport().set_input_as_handled()
elif event is InputEventMouseButton:
var mouseEvent := event as InputEventMouseButton
if mouseEvent.button_index == MOUSE_BUTTON_LEFT and not mouseEvent.pressed:
_finish_decor_drag()
get_viewport().set_input_as_handled()
func _is_pointer_over_blocking_ui() -> bool:
var mousePosition := get_viewport().get_mouse_position()
for child in uiLayer.get_children():
if child == _dragSurface:
continue
if child is Control and _control_blocks_pointer(child as Control, mousePosition):
return true
return false
func _control_blocks_pointer(control: Control, mousePosition: Vector2) -> bool:
if not control.visible or not control.is_visible_in_tree():
return false
if control.mouse_filter != Control.MOUSE_FILTER_IGNORE and control.get_global_rect().has_point(mousePosition):
return true
for child in control.get_children():
if child is Control and _control_blocks_pointer(child as Control, mousePosition):
return true
return false
func _remove_selected_decor() -> void:
if _selectedDecorId.is_empty() or not _decorItems.has(_selectedDecorId):
_show_toast("请先选择一个家具")
return
var item: Dictionary = _decorItems[_selectedDecorId]
item["placed"] = false
_decorItems[_selectedDecorId] = item
if _decorNodes.has(_selectedDecorId):
var node := _decorNodes[_selectedDecorId] as Node
if is_instance_valid(node):
node.queue_free()
_decorNodes.erase(_selectedDecorId)
_remove_decor_collision(_selectedDecorId)
_save_decor_item(item)
_render_inventory_list()
_show_toast("已收回家具")
func _save_decor_item(item: Dictionary) -> void:
if not _is_authenticated():
_show_toast("请先登录后保存摆放")
return
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
if saveManager == null or not saveManager.has_method("enqueue_save"):
_show_toast("家具保存服务不可用")
return
saveManager.call("enqueue_save", item)
func _on_decor_save_succeeded(savedItem: Dictionary) -> void:
var item := _merge_decor_definition(savedItem.duplicate(true))
var decorId := str(item.get("decor_id", ""))
if not decorId.is_empty():
_decorItems[decorId] = item
func _on_decor_save_failed(_item: Dictionary, message: String) -> void:
_show_toast(message)
func _is_point_inside_sprite(node: Sprite2D, worldPosition: Vector2) -> bool:
if node.texture == null:
return false
var localPoint := node.to_local(worldPosition)
var size := node.texture.get_size()
var rect := Rect2(-size * 0.5, size)
return rect.has_point(localPoint)
func _sync_decor_collision(decorId: String, item: Dictionary) -> void:
var collisionSize := _variant_to_vector2(item.get("collision_size", Vector2.ZERO))
if collisionSize == Vector2.ZERO:
_remove_decor_collision(decorId)
return
var collisionOffset := _variant_to_vector2(item.get("collision_offset", Vector2.ZERO))
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
var shapeNode: CollisionShape2D
if body == null or not is_instance_valid(body):
body = StaticBody2D.new()
body.name = "DecorCollision_%s" % decorId
body.collision_layer = DECOR_COLLISION_LAYER
body.collision_mask = DECOR_COLLISION_MASK
shapeNode = CollisionShape2D.new()
shapeNode.name = "CollisionShape2D"
var rectShape := RectangleShape2D.new()
shapeNode.shape = rectShape
body.add_child(shapeNode)
staticCollision.add_child(body)
_decorCollisionBodies[decorId] = body
else:
shapeNode = body.get_node_or_null("CollisionShape2D") as CollisionShape2D
if shapeNode == null:
shapeNode = CollisionShape2D.new()
shapeNode.name = "CollisionShape2D"
shapeNode.shape = RectangleShape2D.new()
body.add_child(shapeNode)
var scale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
body.global_position = Vector2(_to_float(item.get("position_x", 0.0), 0.0), _to_float(item.get("position_y", 0.0), 0.0)) + collisionOffset * scale
shapeNode.position = Vector2.ZERO
var rectShape := shapeNode.shape as RectangleShape2D
if rectShape == null:
rectShape = RectangleShape2D.new()
shapeNode.shape = rectShape
rectShape.size = Vector2(max(8.0, collisionSize.x * scale), max(8.0, collisionSize.y * scale))
func _set_decor_collision_disabled(decorId: String, disabled: bool) -> void:
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
if body == null or not is_instance_valid(body):
return
var shapeNode := body.get_node_or_null("CollisionShape2D") as CollisionShape2D
if shapeNode != null:
shapeNode.disabled = disabled
func _remove_decor_collision(decorId: String) -> void:
var body := _decorCollisionBodies.get(decorId, null) as Node
if body != null and is_instance_valid(body):
body.queue_free()
_decorCollisionBodies.erase(decorId)
func _variant_to_vector2(value: Variant) -> Vector2:
if value is Vector2:
return value
if value is Dictionary:
var dict: Dictionary = value
return Vector2(_to_float(dict.get("x", 0.0), 0.0), _to_float(dict.get("y", 0.0), 0.0))
return Vector2.ZERO
func _to_float(value: Variant, fallback: float) -> float:
match typeof(value):
TYPE_FLOAT:
return value
TYPE_INT:
return value
TYPE_STRING:
var text := str(value).strip_edges()
return fallback if text.is_empty() else text.to_float()
TYPE_BOOL:
return 1.0 if bool(value) else 0.0
_:
return fallback
func _to_int(value: Variant, fallback: int) -> int:
match typeof(value):
TYPE_INT:
return value
TYPE_FLOAT:
return roundi(value)
TYPE_STRING:
var text := str(value).strip_edges()
return fallback if text.is_empty() else text.to_int()
TYPE_BOOL:
return 1 if bool(value) else 0
_:
return fallback
func _get_item_float(item: Dictionary, key: String, fallback: float) -> float:
return _to_float(item.get(key, fallback), fallback)
func _get_item_int(item: Dictionary, key: String, fallback: int) -> int:
return _to_int(item.get(key, fallback), fallback)
func _is_authenticated() -> bool:
var authManager := get_node_or_null("/root/AuthManager")
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated"))
func _auth_headers() -> PackedStringArray:
var authManager := get_node_or_null("/root/AuthManager")
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else ""
return PackedStringArray([
"Content-Type: application/json",
"Authorization: Bearer %s" % accessToken,
])
func _load_texture(path: String) -> Texture2D:
if path.is_empty():
return null
if FileAccess.file_exists("%s.import" % path):
var texture := load(path) as Texture2D
if texture != null:
return texture
var image := Image.load_from_file(ProjectSettings.globalize_path(path))
if image == null or image.is_empty():
return null
return ImageTexture.create_from_image(image)
func _get_event_system() -> Node:
return get_node_or_null("/root/EventSystem")
func _show_toast(message: String) -> void:
if not is_instance_valid(_toastLabel):
return
_toastLabel.text = message
_toastLabel.visible = true
var tween := create_tween()
tween.tween_interval(1.5)
tween.tween_callback(func() -> void:
if is_instance_valid(_toastLabel):
_toastLabel.visible = false
)
func _pulse_node(node: Node2D) -> void:
var originalScale := node.scale
var tween := create_tween()
tween.tween_property(node, "scale", originalScale * 1.08, 0.08)
tween.tween_property(node, "scale", originalScale, 0.10)
func _clear_children(node: Node) -> void:
for child in node.get_children():
child.queue_free()
func _create_panel_style(color: Color, radius: int, shadow: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.corner_radius_top_left = radius
style.corner_radius_top_right = radius
style.corner_radius_bottom_left = radius
style.corner_radius_bottom_right = radius
style.content_margin_left = 12
style.content_margin_top = 8
style.content_margin_right = 12
style.content_margin_bottom = 8
if shadow:
style.shadow_color = Color(0.082, 0.243, 0.380, 0.18)
style.shadow_size = 12
style.shadow_offset = Vector2(0, 4)
return style
func _create_inventory_card_style(hovered: bool) -> StyleBoxFlat:
var style := _create_panel_style(Color(0.982, 0.995, 1.0, 1.0), 18, true)
style.border_width_left = 2
style.border_width_top = 2
style.border_width_right = 2
style.border_width_bottom = 2
style.border_color = Color(0.650, 0.835, 0.965, 0.90) if not hovered else Color(0.086, 0.690, 0.960, 1.0)
style.shadow_color = Color(0.086, 0.314, 0.520, 0.14 if hovered else 0.08)
style.shadow_size = 14 if hovered else 8
style.content_margin_left = 0
style.content_margin_top = 0
style.content_margin_right = 0
style.content_margin_bottom = 0
return style

View File

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

View File

@@ -0,0 +1,32 @@
class_name ScenePortal
extends Area2D
# ============================================================================
# ScenePortal.gd - 场景入口传送组件
# ============================================================================
# 提供轻量级场景跳转能力,用于广场、小区和个人空间之间的入口。
# ============================================================================
@export var targetSceneName: String = ""
@export var targetSpawnName: String = ""
@export var targetPosition: Vector2 = Vector2.ZERO
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if not (body is PlayerController):
return
_change_scene()
func _change_scene() -> void:
if targetSceneName.is_empty():
push_warning("ScenePortal: targetSceneName is empty.")
return
if not targetSpawnName.is_empty():
SceneManager.set_next_spawn_name(targetSpawnName)
elif targetPosition != Vector2.ZERO:
SceneManager.set_next_scene_position(targetPosition)
SceneManager.change_scene(targetSceneName)

View File

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

42
scenes/Maps/Square.gd Normal file
View File

@@ -0,0 +1,42 @@
class_name Square
extends Node2D
# ============================================================================
# Square.gd - 广场场景控制器
# ============================================================================
# 负责广场玩家出生点和玩家相机边界配置,避免运行时看到地图外区域。
# ============================================================================
const CAMERA_ZOOM: Vector2 = Vector2(2.0, 2.0)
const CAMERA_LIMIT_LEFT: int = -1280
const CAMERA_LIMIT_TOP: int = -960
const CAMERA_LIMIT_RIGHT: int = 1280
const CAMERA_LIMIT_BOTTOM: int = 960
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
@onready var overviewCamera: Camera2D = $Camera2D
func _ready() -> void:
_apply_spawn_point()
_configure_camera()
func _apply_spawn_point() -> void:
var spawnName: String = SceneManager.get_next_spawn_name()
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
var marker := $Markers.get_node_or_null(markerName) as Marker2D
if marker == null:
marker = $Markers/DefaultSpawn
player.global_position = marker.global_position
func _configure_camera() -> void:
overviewCamera.enabled = false
playerCamera.enabled = true
playerCamera.make_current()
playerCamera.zoom = CAMERA_ZOOM
playerCamera.position_smoothing_enabled = true
playerCamera.limit_left = CAMERA_LIMIT_LEFT
playerCamera.limit_top = CAMERA_LIMIT_TOP
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
playerCamera.limit_smoothed = true

View File

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

176
scenes/Maps/WorkZone.gd Normal file
View File

@@ -0,0 +1,176 @@
class_name WorkZone
extends Node2D
# ============================================================================
# WorkZone.gd - 打工区场景控制器
# ============================================================================
# 承载赛博打工区概念地图,负责玩家出生点和相机边界配置。
# ============================================================================
const CAMERA_ZOOM: Vector2 = Vector2(2.0, 2.0)
const CAMERA_LIMIT_LEFT: int = -1280
const CAMERA_LIMIT_TOP: int = -960
const CAMERA_LIMIT_RIGHT: int = 1280
const CAMERA_LIMIT_BOTTOM: int = 960
const MALL_PANEL_SCENE: PackedScene = preload("res://scenes/ui/mall/MallPanel.tscn")
const COURSE_BOARD_PANEL_SCENE: PackedScene = preload("res://scenes/ui/CourseBoardPanel.tscn")
const MALL_ENTRANCE_POSITION: Vector2 = Vector2(-4, -418)
const MALL_ENTRANCE_SIZE: Vector2 = Vector2(112, 34)
const MALL_EXIT_POSITION: Vector2 = Vector2(-4, -310)
const MALL_EXIT_REOPEN_COOLDOWN: float = 0.45
const PLAYER_COLLISION_LAYER: int = 1
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
@onready var uiLayer: CanvasLayer = $UILayer
var _mallPanel: Control
var _courseBoardPanel: Control
var _mallEntranceArea: Area2D
var _isInsideMall: bool = false
var _mallCanEnter: bool = true
func _ready() -> void:
_apply_spawn_point()
_configure_camera()
_ensure_mall_panel()
_ensure_course_board_panel()
_ensure_mall_entrance_area()
EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
func _exit_tree() -> void:
EventSystem.disconnect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
EventSystem.disconnect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
func _apply_spawn_point() -> void:
var spawnName: String = SceneManager.get_next_spawn_name()
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
var marker := $Markers.get_node_or_null(markerName) as Marker2D
if marker == null:
marker = $Markers/DefaultSpawn
player.global_position = marker.global_position
func _configure_camera() -> void:
playerCamera.zoom = CAMERA_ZOOM
playerCamera.position_smoothing_enabled = true
playerCamera.limit_left = CAMERA_LIMIT_LEFT
playerCamera.limit_top = CAMERA_LIMIT_TOP
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
playerCamera.limit_smoothed = true
func _ensure_mall_panel() -> void:
var existingPanel := uiLayer.get_node_or_null("MallPanel") as Control
if existingPanel != null:
_mallPanel = existingPanel
return
_mallPanel = MALL_PANEL_SCENE.instantiate() as Control
_mallPanel.name = "MallPanel"
uiLayer.add_child(_mallPanel)
func _ensure_course_board_panel() -> void:
var existingPanel := uiLayer.get_node_or_null("CourseBoardPanel") as Control
if existingPanel != null:
_courseBoardPanel = existingPanel
return
_courseBoardPanel = COURSE_BOARD_PANEL_SCENE.instantiate() as Control
_courseBoardPanel.name = "CourseBoardPanel"
uiLayer.add_child(_courseBoardPanel)
func _ensure_mall_entrance_area() -> void:
var areas := get_node_or_null("InteractionAreas") as Node2D
if areas == null:
areas = Node2D.new()
areas.name = "InteractionAreas"
add_child(areas)
var entrance := areas.get_node_or_null("MallEntranceArea") as WorkZoneBuildingArea
if entrance == null:
entrance = WorkZoneBuildingArea.new()
entrance.name = "MallEntranceArea"
areas.add_child(entrance)
entrance.buildingId = "whale_super_mall"
entrance.buildingTitle = "鲸鱼商城"
entrance.buildingRole = "mall"
entrance.position = MALL_ENTRANCE_POSITION
var shape := entrance.get_node_or_null("CollisionShape2D") as CollisionShape2D
if shape == null:
shape = CollisionShape2D.new()
shape.name = "CollisionShape2D"
entrance.add_child(shape)
var rectangle := shape.shape as RectangleShape2D
if rectangle == null:
rectangle = RectangleShape2D.new()
shape.shape = rectangle
shape.position = Vector2.ZERO
rectangle.size = MALL_ENTRANCE_SIZE
_setup_mall_entrance_trigger(entrance)
func _setup_mall_entrance_trigger(entrance: Area2D) -> void:
_mallEntranceArea = entrance
_mallEntranceArea.collision_mask = PLAYER_COLLISION_LAYER
if not _mallEntranceArea.body_entered.is_connected(_on_mall_entrance_body_entered):
_mallEntranceArea.body_entered.connect(_on_mall_entrance_body_entered)
func _on_object_interacted(data: Dictionary) -> void:
var buildingId := str(data.get("buildingId", ""))
if buildingId.is_empty():
return
if buildingId == "whale_super_mall":
return
if buildingId == "virtual_whale_recruitment_board":
_open_course_board()
return
if not [
"whale_job_center",
"ai_service_station",
"whale_coin_exchange",
"virtual_whale_recruitment_board",
].has(buildingId):
return
var title := str(data.get("title", buildingId))
var role := str(data.get("role", ""))
print("WorkZone interaction ready: %s (%s)" % [title, role])
func _open_course_board() -> void:
if is_instance_valid(_courseBoardPanel) and _courseBoardPanel.has_method("show_panel"):
_courseBoardPanel.call("show_panel")
func _on_mall_entrance_body_entered(body: Node2D) -> void:
if body != player or not _mallCanEnter:
return
_enter_mall()
func _enter_mall() -> void:
if _isInsideMall:
return
_isInsideMall = true
_set_player_mall_transition_state(true)
if is_instance_valid(_mallPanel) and _mallPanel.has_method("show_panel"):
_mallPanel.call("show_panel")
func _on_mall_closed(_data: Dictionary = {}) -> void:
if not _isInsideMall:
return
_isInsideMall = false
_mallCanEnter = false
player.global_position = MALL_EXIT_POSITION
if player.has_method("_reset_movement_input_state"):
player.call("_reset_movement_input_state")
_set_player_mall_transition_state(false)
await get_tree().create_timer(MALL_EXIT_REOPEN_COOLDOWN).timeout
_mallCanEnter = true
func _set_player_mall_transition_state(isInsideMall: bool) -> void:
if not is_instance_valid(player):
return
player.visible = not isInsideMall
player.set_physics_process(not isInsideMall)
player.velocity = Vector2.ZERO
if player.has_method("_reset_movement_input_state"):
player.call("_reset_movement_input_state")

View File

@@ -0,0 +1 @@
uid://6de5gvosic2s

View File

@@ -0,0 +1,30 @@
class_name WorkZoneBuildingArea
extends Area2D
# ============================================================================
# WorkZoneBuildingArea.gd - 打工区建筑交互入口
# ============================================================================
# 为打工区建筑提供统一交互数据,玩家面对入口按交互键时由 PlayerController 调用。
# ============================================================================
const INTERACTION_COLLISION_LAYER: int = 2
@export var buildingId: String = ""
@export var buildingTitle: String = ""
@export var buildingRole: String = ""
func _ready() -> void:
collision_layer = INTERACTION_COLLISION_LAYER
collision_mask = 0
func interact() -> void:
var payload := {
"buildingId": buildingId,
"title": buildingTitle,
"role": buildingRole,
"nodePath": get_path(),
}
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem != null:
eventSystem.call("emit_event", EventNames.OBJECT_INTERACTED, payload)
print("WorkZone building interacted: %s - %s" % [buildingTitle, buildingRole])

View File

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

View File

@@ -0,0 +1,360 @@
[gd_scene format=3 uid="uid://c2flhkqk5icab"]
[ext_resource type="Script" uid="uid://ccesagjisqodl" path="res://scenes/Maps/CafeInterior.gd" id="1_script"]
[ext_resource type="Texture2D" path="res://output/novamailio-imagegen/cafe_interior_scene/whale_cafe_large_service_hall_base_v4.png" id="2_map"]
[ext_resource type="PackedScene" uid="uid://b2f8e24plwqgj" path="res://scenes/characters/player.tscn" id="3_player"]
[ext_resource type="PackedScene" path="res://scenes/ui/ChatUI.tscn" id="4_chatui"]
[ext_resource type="PackedScene" path="res://scenes/ui/PlayerHud.tscn" id="5_playerhud"]
[ext_resource type="PackedScene" path="res://scenes/ui/FriendListPanel.tscn" id="6_friendpanel"]
[ext_resource type="PackedScene" path="res://scenes/ui/SettingsPanel.tscn" id="7_settings"]
[ext_resource type="PackedScene" path="res://scenes/characters/cafe_whale_barista_npc.tscn" id="8_barista_npc"]
[ext_resource type="Script" uid="uid://d1sfqui0pqalf" path="res://scenes/characters/CafeCompanionTarget.gd" id="9_cafe_companion_target"]
[ext_resource type="PackedScene" path="res://scenes/ui/CafeCompanionPanel.tscn" id="10_cafe_companion_panel"]
[ext_resource type="PackedScene" path="res://scenes/ui/CafeCompanionRecruitmentPanel.tscn" id="11_cafe_recruitment_panel"]
[ext_resource type="Script" uid="uid://dxupavbgcw3tw" path="res://scenes/Maps/MapMultiplayerController.gd" id="12_multiplayer"]
[ext_resource type="Theme" uid="uid://brk6ca2npglqc" path="res://assets/ui/world_text_theme.tres" id="13_world_text_theme"]
[ext_resource type="PackedScene" path="res://scenes/ui/MapPanel.tscn" id="14_mappanel"]
[sub_resource type="StyleBoxFlat" id="Style_CafeCompanionNameplate"]
content_margin_left = 12.0
content_margin_top = 4.0
content_margin_right = 12.0
content_margin_bottom = 4.0
bg_color = Color(1, 0.988, 0.955, 0.96)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.18, 0.34, 0.38, 0.92)
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
shadow_color = Color(0.05, 0.08, 0.09, 0.18)
shadow_size = 4
shadow_offset = Vector2(0, 2)
[sub_resource type="RectangleShape2D" id="Shape_BaristaChatTarget"]
size = Vector2(112, 148)
[sub_resource type="RectangleShape2D" id="Shape_TopWall"]
size = Vector2(1580, 72)
[sub_resource type="RectangleShape2D" id="Shape_BottomWall"]
size = Vector2(1572, 72)
[sub_resource type="RectangleShape2D" id="Shape_LeftWall"]
size = Vector2(80, 1100)
[sub_resource type="RectangleShape2D" id="Shape_RightWall"]
size = Vector2(47, 721)
[sub_resource type="RectangleShape2D" id="Shape_FrontCounterBand"]
size = Vector2(1063, 64)
[sub_resource type="RectangleShape2D" id="Shape_BackEquipmentBand"]
size = Vector2(1300, 92)
[sub_resource type="RectangleShape2D" id="Shape_LeftServiceFixture"]
size = Vector2(208, 309)
[sub_resource type="RectangleShape2D" id="Shape_RightServiceFixture"]
size = Vector2(202, 329)
[sub_resource type="RectangleShape2D" id="Shape_ExitArea"]
size = Vector2(420, 96)
[sub_resource type="RectangleShape2D" id="Shape_CafeRecruitmentLogo"]
size = Vector2(135, 63)
[node name="CafeInterior" type="Node2D" unique_id=6364941]
script = ExtResource("1_script")
[node name="UILayer" type="CanvasLayer" parent="." unique_id=915800518]
layer = 10
[node name="ChatUI" parent="UILayer" unique_id=1173101039 instance=ExtResource("4_chatui")]
[node name="PlayerHud" parent="UILayer" unique_id=1564092911 instance=ExtResource("5_playerhud")]
[node name="MapPanel" parent="UILayer" instance=ExtResource("14_mappanel")]
[node name="FriendListPanel" parent="UILayer" unique_id=916180939 instance=ExtResource("6_friendpanel")]
[node name="SettingsPanel" parent="UILayer" unique_id=783599923 instance=ExtResource("7_settings")]
[node name="CafeCompanionPanel" parent="UILayer" unique_id=365240236 instance=ExtResource("10_cafe_companion_panel")]
[node name="CafeCompanionRecruitmentPanel" parent="UILayer" unique_id=1274369176 instance=ExtResource("11_cafe_recruitment_panel")]
[node name="StageBackdrop" type="ColorRect" parent="." unique_id=120901466]
z_index = -200
offset_left = -2000.0
offset_top = -1600.0
offset_right = 2000.0
offset_bottom = 1600.0
mouse_filter = 2
color = Color(0.065, 0.075, 0.09, 1)
[node name="CafeServiceHallBase" type="Sprite2D" parent="." unique_id=106640887]
z_index = -100
texture_filter = 1
texture = ExtResource("2_map")
[node name="YSortWorld" type="Node2D" parent="." unique_id=1472764725]
y_sort_enabled = true
[node name="Characters" type="Node2D" parent="YSortWorld" unique_id=2141328072]
y_sort_enabled = true
[node name="Players" type="Node2D" parent="YSortWorld/Characters" unique_id=1853391519]
y_sort_enabled = true
[node name="Player" parent="YSortWorld/Characters/Players" unique_id=616484753 instance=ExtResource("3_player")]
position = Vector2(0, 325)
[node name="CafeHiredCompanionNameplate" type="Label" parent="YSortWorld/Characters/Players/Player" unique_id=1594219674]
visible = false
z_index = 30
custom_minimum_size = Vector2(188, 44)
offset_left = -47.0
offset_top = -96.0
offset_right = -47.0
offset_bottom = -96.0
scale = Vector2(0.5, 0.5)
theme = ExtResource("13_world_text_theme")
theme_override_colors/font_color = Color(0.12, 0.2, 0.24, 1)
theme_override_colors/font_shadow_color = Color(1, 1, 1, 0.85)
theme_override_constants/shadow_offset_x = 0
theme_override_constants/shadow_offset_y = 1
theme_override_font_sizes/font_size = 24
theme_override_styles/normal = SubResource("Style_CafeCompanionNameplate")
text = "陪伴机器人"
horizontal_alignment = 1
vertical_alignment = 1
[node name="CafeHiredCompanionTarget" type="Area2D" parent="YSortWorld/Characters/Players/Player" unique_id=1604517960]
position = Vector2(0, -58)
collision_layer = 2
collision_mask = 0
script = ExtResource("9_cafe_companion_target")
[node name="CollisionShape2D" type="CollisionShape2D" parent="YSortWorld/Characters/Players/Player/CafeHiredCompanionTarget" unique_id=658877351]
shape = SubResource("Shape_BaristaChatTarget")
disabled = true
[node name="RemotePlayers" type="Node2D" parent="YSortWorld/Characters/Players" unique_id=2071839484]
y_sort_enabled = true
[node name="CafeWhaleBaristaNpc" parent="YSortWorld/Characters" unique_id=1188569491 instance=ExtResource("8_barista_npc")]
position = Vector2(-472, -291)
[node name="CafeCompanionNameplate" type="Label" parent="YSortWorld/Characters/CafeWhaleBaristaNpc" unique_id=885952340]
z_index = 30
custom_minimum_size = Vector2(152, 44)
offset_left = -38.0
offset_top = -136.0
offset_right = -38.0
offset_bottom = -136.0
scale = Vector2(0.5, 0.5)
theme = ExtResource("13_world_text_theme")
theme_override_colors/font_color = Color(0.12, 0.2, 0.25, 1)
theme_override_colors/font_shadow_color = Color(1, 1, 1, 0.85)
theme_override_constants/shadow_offset_x = 0
theme_override_constants/shadow_offset_y = 1
theme_override_font_sizes/font_size = 24
theme_override_styles/normal = SubResource("Style_CafeCompanionNameplate")
text = "海盐拿铁"
horizontal_alignment = 1
vertical_alignment = 1
[node name="CafeCompanionTarget" type="Area2D" parent="YSortWorld/Characters/CafeWhaleBaristaNpc" unique_id=1439390611]
position = Vector2(0, -58)
script = ExtResource("9_cafe_companion_target")
servicePointId = "ServiceIdlePoint01"
companionId = "cafe_companion_npc"
personaName = "海盐拿铁"
[node name="CollisionShape2D" type="CollisionShape2D" parent="YSortWorld/Characters/CafeWhaleBaristaNpc/CafeCompanionTarget" unique_id=292882204]
shape = SubResource("Shape_BaristaChatTarget")
[node name="MultiplayerController" type="Node" parent="YSortWorld" unique_id=1171758081]
script = ExtResource("12_multiplayer")
map_id = "whale_cafe"
[node name="StaticCollision" type="Node2D" parent="." unique_id=329016366]
[node name="CafeBoundary" type="StaticBody2D" parent="StaticCollision" unique_id=810981314]
[node name="TopWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=83767543]
position = Vector2(14, -501)
shape = SubResource("Shape_TopWall")
[node name="BottomWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=578059512]
position = Vector2(3, 472)
shape = SubResource("Shape_BottomWall")
[node name="LeftWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=1817681571]
position = Vector2(-782, -5)
shape = SubResource("Shape_LeftWall")
[node name="RightWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=19691881]
position = Vector2(761, 196)
shape = SubResource("Shape_RightWall")
[node name="BarCounterCollision" type="StaticBody2D" parent="StaticCollision" unique_id=2101575249]
[node name="FrontCounterBand" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=236568381]
position = Vector2(2.5, -123)
shape = SubResource("Shape_FrontCounterBand")
[node name="BackEquipmentBand" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=302153346]
position = Vector2(0, -435)
shape = SubResource("Shape_BackEquipmentBand")
[node name="LeftServiceFixture" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=1759728122]
position = Vector2(-641, -283.5)
shape = SubResource("Shape_LeftServiceFixture")
[node name="RightServiceFixture" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=1239925157]
position = Vector2(636, -294.5)
shape = SubResource("Shape_RightServiceFixture")
[node name="PropCollision" type="StaticBody2D" parent="StaticCollision" unique_id=729484024]
[node name="CafeReceptionDeskCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1834206100]
polygon = PackedVector2Array(-119, 60, -107, 43, 117, 43, 112, 74, 86, 116, -104, 105)
[node name="LeftSelfServiceKiosk01Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1994436927]
polygon = PackedVector2Array(-521, 164, -465, 160, -462, 240, -510, 243, -525, 219)
[node name="LeftSelfServiceKiosk02Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1428046087]
polygon = PackedVector2Array(-450, 153, -404, 166, -397, 242, -433, 240, -449, 224)
[node name="RightSelfServiceKiosk01Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1839282295]
polygon = PackedVector2Array(411, 160, 443, 166, 439, 232, 398, 242, 390, 205, 400, 193)
[node name="RightSelfServiceKiosk02Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1823238674]
polygon = PackedVector2Array(463, 161, 517, 162, 519, 226, 510, 243, 468, 235)
[node name="LeftCornerPlantSignCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=57617661]
polygon = PackedVector2Array(-747, 325, -750, 203, -679, 208, -637, 296, -602, 329, -594, 411, -645, 422, -739, 410)
[node name="RightCornerPlantSignCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1013557406]
polygon = PackedVector2Array(629, 291, 653, 247, 740, 193, 747, 325, 739, 410, 658, 421, 619, 392)
[node name="LeftQueueTopRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=443030549]
position = Vector2(8, -35)
polygon = PackedVector2Array(-382, 4, -282, 5, -282, 14, -382, 13)
[node name="LeftQueueLeftRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=782461653]
position = Vector2(-18, -2)
polygon = PackedVector2Array(-360, 18, -352, -25, -365, 205, -376, 207)
[node name="LeftQueueRightRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=785942935]
position = Vector2(-6, -37)
polygon = PackedVector2Array(-286, 8, -275, 8, -290, 217, -293, 248)
[node name="LeftQueueBottomRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=141845763]
position = Vector2(-330, -326)
polygon = PackedVector2Array(-377, 204, -370, 207, -352, 386, -409, 460, -426, 358, -406, 202)
[node name="RightQueueTopRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=332551755]
position = Vector2(2, -35)
polygon = PackedVector2Array(282, 5, 381, 5, 381, 14, 282, 14)
[node name="RightQueueLeftRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1268670145]
position = Vector2(-2, -40)
polygon = PackedVector2Array(276, 8, 287, 8, 308, 258, 294, 254, 291, 193)
[node name="RightQueueRightRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=350292682]
position = Vector2(13, 3)
polygon = PackedVector2Array(355, -27, 363, 18, 387, 205, 374, 203)
[node name="RightQueueBottomRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=2067709655]
position = Vector2(300, -242)
polygon = PackedVector2Array(383, 116, 433, 92, 483, 231, 433, 355, 391, 297, 377, 232, 382, 191)
[node name="InteractionAreas" type="Node2D" parent="." unique_id=1414044788]
[node name="ExitToWorkZoneArea" type="Area2D" parent="InteractionAreas" unique_id=1132514494]
position = Vector2(0, 455)
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionAreas/ExitToWorkZoneArea" unique_id=1808842525]
shape = SubResource("Shape_ExitArea")
[node name="CafeRecruitmentLogoArea" type="Area2D" parent="InteractionAreas" unique_id=1787759997]
position = Vector2(0, 90)
collision_layer = 2
collision_mask = 0
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionAreas/CafeRecruitmentLogoArea" unique_id=521534737]
position = Vector2(-5.5, -16.5)
shape = SubResource("Shape_CafeRecruitmentLogo")
[node name="Markers" type="Node2D" parent="." unique_id=388355020]
[node name="DefaultSpawn" type="Marker2D" parent="Markers" unique_id=1348428752]
position = Vector2(0, 392)
[node name="ServiceIdlePoints" type="Node2D" parent="Markers" unique_id=917409132]
[node name="ServiceIdlePoint01" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=579915137]
position = Vector2(-472, -291)
[node name="ServiceIdlePoint02" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=783535284]
position = Vector2(-376, -291)
[node name="ServiceIdlePoint03" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=904507716]
position = Vector2(-280, -291)
[node name="ServiceIdlePoint04" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1203682930]
position = Vector2(-184, -291)
[node name="ServiceIdlePoint05" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=705534018]
position = Vector2(-90, -291)
[node name="ServiceIdlePoint06" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=227830370]
position = Vector2(0, -291)
[node name="ServiceIdlePoint07" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1258667067]
position = Vector2(92, -291)
[node name="ServiceIdlePoint08" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=868571064]
position = Vector2(185, -291)
[node name="ServiceIdlePoint09" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=510403492]
position = Vector2(277, -291)
[node name="ServiceIdlePoint10" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=527786790]
position = Vector2(371, -291)
[node name="ServiceIdlePoint11" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=29178558]
position = Vector2(-484, -223)
[node name="ServiceIdlePoint12" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=61920657]
position = Vector2(-384, -223)
[node name="ServiceIdlePoint13" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=882725816]
position = Vector2(-287, -223)
[node name="ServiceIdlePoint14" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=748399169]
position = Vector2(-191, -223)
[node name="ServiceIdlePoint15" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=197622030]
position = Vector2(-96, -223)
[node name="ServiceIdlePoint16" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1362126027]
position = Vector2(-2, -223)
[node name="ServiceIdlePoint17" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=2008866192]
position = Vector2(92, -223)
[node name="ServiceIdlePoint18" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=69828930]
position = Vector2(188, -223)
[node name="ServiceIdlePoint19" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=294392180]
position = Vector2(281, -223)
[node name="ServiceIdlePoint20" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1006333228]
position = Vector2(378, -223)

View File

@@ -0,0 +1,97 @@
[gd_scene load_steps=12 format=4]
[ext_resource type="Script" path="res://scenes/Maps/PersonalSpace.gd" id="1_personal_space"]
[ext_resource type="Texture2D" path="res://assets/maps/personal_space/v1/base/personal_room_25d_sidewalls_wider_not_longer_v1.png" id="2_room_base"]
[ext_resource type="PackedScene" path="res://scenes/characters/player.tscn" id="3_player"]
[ext_resource type="Script" path="res://scenes/Maps/ScenePortal.gd" id="4_portal"]
[ext_resource type="PackedScene" path="res://scenes/ui/ChatUI.tscn" id="5_chatui"]
[ext_resource type="PackedScene" path="res://scenes/ui/PlayerHud.tscn" id="6_playerhud"]
[ext_resource type="PackedScene" path="res://scenes/ui/FriendListPanel.tscn" id="7_friendpanel"]
[ext_resource type="PackedScene" path="res://scenes/ui/SettingsPanel.tscn" id="8_settingspanel"]
[sub_resource type="RectangleShape2D" id="Shape_TopWall"]
size = Vector2(882, 76)
[sub_resource type="RectangleShape2D" id="Shape_LeftWall"]
size = Vector2(133, 518)
[sub_resource type="RectangleShape2D" id="Shape_RightWall"]
size = Vector2(133, 518)
[sub_resource type="RectangleShape2D" id="Shape_ExitPortal"]
size = Vector2(105, 57)
[node name="PersonalSpace" type="Node2D"]
script = ExtResource("1_personal_space")
[node name="UILayer" type="CanvasLayer" parent="."]
layer = 10
[node name="ChatUI" parent="UILayer" instance=ExtResource("5_chatui")]
[node name="PlayerHud" parent="UILayer" instance=ExtResource("6_playerhud")]
[node name="FriendListPanel" parent="UILayer" instance=ExtResource("7_friendpanel")]
[node name="SettingsPanel" parent="UILayer" instance=ExtResource("8_settingspanel")]
[node name="StageBackdrop" type="ColorRect" parent="."]
z_index = -200
offset_left = -2000.0
offset_top = -1400.0
offset_right = 2000.0
offset_bottom = 1400.0
color = Color(0.118, 0.149, 0.18, 1)
[node name="RoomBase" type="Sprite2D" parent="."]
z_index = -100
texture_filter = 2
scale = Vector2(0.7, 0.7)
texture = ExtResource("2_room_base")
[node name="YSortWorld" type="Node2D" parent="."]
y_sort_enabled = true
[node name="Characters" type="Node2D" parent="YSortWorld"]
y_sort_enabled = true
[node name="Players" type="Node2D" parent="YSortWorld/Characters"]
y_sort_enabled = true
[node name="Player" parent="YSortWorld/Characters/Players" instance=ExtResource("3_player")]
position = Vector2(0, 217)
[node name="StaticCollision" type="Node2D" parent="."]
[node name="RoomBoundary" type="StaticBody2D" parent="StaticCollision"]
[node name="TopWall" type="CollisionShape2D" parent="StaticCollision/RoomBoundary"]
position = Vector2(0, -210)
shape = SubResource("Shape_TopWall")
[node name="LeftWall" type="CollisionShape2D" parent="StaticCollision/RoomBoundary"]
position = Vector2(-483, 14)
shape = SubResource("Shape_LeftWall")
[node name="RightWall" type="CollisionShape2D" parent="StaticCollision/RoomBoundary"]
position = Vector2(483, 14)
shape = SubResource("Shape_RightWall")
[node name="InteractionAreas" type="Node2D" parent="."]
[node name="ExitToSquareArea" type="Area2D" parent="InteractionAreas"]
position = Vector2(0, 273)
script = ExtResource("4_portal")
targetSceneName = "square"
targetSpawnName = "NeighborhoodTeleport"
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionAreas/ExitToSquareArea"]
shape = SubResource("Shape_ExitPortal")
[node name="Markers" type="Node2D" parent="."]
[node name="DefaultSpawn" type="Marker2D" parent="Markers"]
position = Vector2(0, 200)
[node name="FromNeighborhood" type="Marker2D" parent="Markers"]
position = Vector2(0, 214)

1459
scenes/Maps/square.tscn Normal file

File diff suppressed because one or more lines are too long

424
scenes/Maps/work_zone.tscn Normal file

File diff suppressed because one or more lines are too long