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