forked from xiangwang25/whale-town-front-v2
Initial WhaleTown V2 frontend
This commit is contained in:
103
scenes/characters/CafeCompanionTarget.gd
Normal file
103
scenes/characters/CafeCompanionTarget.gd
Normal file
@@ -0,0 +1,103 @@
|
||||
extends Area2D
|
||||
class_name CafeCompanionTarget
|
||||
|
||||
# ============================================================================
|
||||
# CafeCompanionTarget.gd - 咖啡店陪伴机器人点击组件
|
||||
# ============================================================================
|
||||
# 挂在咖啡店 NPC 或被雇佣玩家节点下,负责把鼠标点击转成
|
||||
# “选择陪伴机器人”事件;真正聊天必须由付费时长购买后开启。
|
||||
# ============================================================================
|
||||
|
||||
@export var servicePointId: String = ""
|
||||
@export var companionId: String = ""
|
||||
@export var companionType: String = "npc"
|
||||
@export var personaName: String = ""
|
||||
@export var ownerUserId: String = ""
|
||||
@export var employmentEndsAt: String = ""
|
||||
@export var selfTarget: bool = false
|
||||
|
||||
var _lastClickMsec: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
input_pickable = true
|
||||
set_process_unhandled_input(true)
|
||||
|
||||
func _input_event(_viewport: Viewport, event: InputEvent, _shapeIdx: int) -> void:
|
||||
if not (event is InputEventMouseButton):
|
||||
return
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index != MOUSE_BUTTON_LEFT or not mouseEvent.pressed:
|
||||
return
|
||||
|
||||
get_viewport().set_input_as_handled()
|
||||
_try_emit_target_selected()
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not (event is InputEventMouseButton):
|
||||
return
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index != MOUSE_BUTTON_LEFT or not mouseEvent.pressed:
|
||||
return
|
||||
if not _contains_global_point(get_global_mouse_position()):
|
||||
return
|
||||
|
||||
get_viewport().set_input_as_handled()
|
||||
_try_emit_target_selected()
|
||||
|
||||
func _try_emit_target_selected() -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _lastClickMsec > 0 and now - _lastClickMsec < 120:
|
||||
return
|
||||
_lastClickMsec = now
|
||||
_emit_target_selected()
|
||||
|
||||
func _emit_target_selected() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem == null:
|
||||
return
|
||||
|
||||
var payload := {
|
||||
"cafe_id": "whale_cafe",
|
||||
"service_point_id": servicePointId,
|
||||
"companion_id": companionId,
|
||||
"companion_type": companionType,
|
||||
"persona_name": _resolved_persona_name(),
|
||||
"owner_user_id": ownerUserId,
|
||||
"employment_ends_at": employmentEndsAt,
|
||||
"target_path": str(get_parent().get_path()) if get_parent() != null else "",
|
||||
}
|
||||
eventSystem.call("emit_event", EventNames.CAFE_COMPANION_SELF_SELECTED if selfTarget else EventNames.CAFE_COMPANION_SELECTED, payload)
|
||||
|
||||
func _resolved_persona_name() -> String:
|
||||
var normalizedName := personaName.strip_edges()
|
||||
if not normalizedName.is_empty():
|
||||
return normalizedName
|
||||
var parent := get_parent()
|
||||
if parent != null and parent.get("npcName") != null:
|
||||
return str(parent.get("npcName")).strip_edges()
|
||||
return companionId
|
||||
|
||||
func _contains_global_point(globalPoint: Vector2) -> bool:
|
||||
for child in 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
|
||||
if shape is CapsuleShape2D:
|
||||
var capsule := shape as CapsuleShape2D
|
||||
var halfHeight: float = maxf(0.0, capsule.height * 0.5 - capsule.radius)
|
||||
var clampedY: float = clampf(localPoint.y, -halfHeight, halfHeight)
|
||||
return Vector2(localPoint.x, localPoint.y - clampedY).length() <= capsule.radius
|
||||
return false
|
||||
1
scenes/characters/CafeCompanionTarget.gd.uid
Normal file
1
scenes/characters/CafeCompanionTarget.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1sfqui0pqalf
|
||||
156
scenes/characters/NPCController.gd
Normal file
156
scenes/characters/NPCController.gd
Normal file
@@ -0,0 +1,156 @@
|
||||
extends CharacterBody2D
|
||||
class_name NPCController
|
||||
|
||||
# ============================================================================
|
||||
# 文件名: NPCController.gd
|
||||
# 作用: 通用 NPC 控制器,负责角色待机表现与交互对话
|
||||
#
|
||||
# 主要功能:
|
||||
# - 播放 NPC 待机动画
|
||||
# - 响应玩家射线交互
|
||||
# - 触发聊天气泡与 NPC 对话事件
|
||||
#
|
||||
# 依赖: EventSystem, EventNames, ChatBubble
|
||||
# 作者: Codex
|
||||
# 创建时间: 2026-03-10
|
||||
# ============================================================================
|
||||
|
||||
signal interaction_happened(text: String)
|
||||
|
||||
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
|
||||
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
|
||||
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
||||
const NPC_TALKED_EVENT: String = "npc_talked"
|
||||
const WORLD_SORT_Z_OFFSET: int = 2048
|
||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||
const NAMEPLATE_RENDER_SCALE: float = 0.5
|
||||
const NAMEPLATE_FONT_SIZE: int = 24
|
||||
const NAMEPLATE_VISUAL_HEIGHT: int = 22
|
||||
const NAMEPLATE_VISUAL_MIN_WIDTH: int = 76
|
||||
const NAMEPLATE_VISUAL_MAX_WIDTH: int = 118
|
||||
const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
|
||||
|
||||
@export var npcName: String = "NPC"
|
||||
@export_multiline var dialogue: String = "欢迎来到WhaleTown,我是镇长范鲸晶"
|
||||
@export var showNameplate: bool = false
|
||||
@export var nameplateOffsetY: float = -112.0
|
||||
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
|
||||
var _nameplate: Label
|
||||
|
||||
func _ready() -> void:
|
||||
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
|
||||
if animation_player.has_animation("idle"):
|
||||
animation_player.play("idle")
|
||||
_update_nameplate()
|
||||
_update_world_sort_z()
|
||||
|
||||
# 保持 NPC 可被玩家射线与角色碰撞识别。
|
||||
collision_layer = 3
|
||||
collision_mask = 3
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
_update_world_sort_z()
|
||||
|
||||
# 处理玩家交互,展示气泡并向全局事件系统广播。
|
||||
func interact() -> void:
|
||||
show_bubble(dialogue)
|
||||
var eventSystem: Node = get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", NPC_TALKED_EVENT, {
|
||||
"npc": self,
|
||||
"npc_name": npcName,
|
||||
"dialogue": dialogue
|
||||
})
|
||||
interaction_happened.emit(dialogue)
|
||||
|
||||
# 在 NPC 头顶生成一次性聊天气泡。
|
||||
#
|
||||
# 参数:
|
||||
# text: String - 要展示的对话内容
|
||||
func show_bubble(text: String) -> void:
|
||||
var bubble: Control = CHAT_BUBBLE_SCENE.instantiate() as Control
|
||||
if bubble == null:
|
||||
return
|
||||
var bubbleLayer: CanvasLayer = _get_chat_bubble_layer()
|
||||
bubbleLayer.add_child(bubble)
|
||||
if bubble.has_method("set_text"):
|
||||
bubble.call("set_text", text, self, CHAT_BUBBLE_TARGET_OFFSET)
|
||||
|
||||
func _get_chat_bubble_layer() -> CanvasLayer:
|
||||
var root: Window = get_tree().root
|
||||
var layer: CanvasLayer = root.get_node_or_null(CHAT_BUBBLE_LAYER_NAME) as CanvasLayer
|
||||
if layer != null:
|
||||
return layer
|
||||
|
||||
layer = CanvasLayer.new()
|
||||
layer.name = CHAT_BUBBLE_LAYER_NAME
|
||||
layer.layer = 20
|
||||
root.add_child(layer)
|
||||
return layer
|
||||
|
||||
func _update_world_sort_z() -> void:
|
||||
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
|
||||
|
||||
func _update_nameplate() -> void:
|
||||
if not showNameplate:
|
||||
if is_instance_valid(_nameplate):
|
||||
_nameplate.visible = false
|
||||
return
|
||||
if not is_instance_valid(_nameplate):
|
||||
_nameplate = Label.new()
|
||||
_nameplate.name = "Nameplate"
|
||||
add_child(_nameplate)
|
||||
|
||||
var displayName := npcName.strip_edges()
|
||||
if displayName.is_empty():
|
||||
displayName = "NPC"
|
||||
var visualWidth := _nameplate_visual_width(displayName)
|
||||
var renderWidth := ceili(float(visualWidth) / NAMEPLATE_RENDER_SCALE)
|
||||
var renderHeight := ceili(float(NAMEPLATE_VISUAL_HEIGHT) / NAMEPLATE_RENDER_SCALE)
|
||||
|
||||
_nameplate.theme = WORLD_TEXT_THEME
|
||||
_nameplate.text = displayName
|
||||
_nameplate.visible = true
|
||||
_nameplate.z_index = 30
|
||||
_nameplate.scale = Vector2.ONE * NAMEPLATE_RENDER_SCALE
|
||||
_nameplate.position = Vector2(float(visualWidth) * -0.5, nameplateOffsetY)
|
||||
_nameplate.custom_minimum_size = Vector2(renderWidth, renderHeight)
|
||||
_nameplate.size = _nameplate.custom_minimum_size
|
||||
_nameplate.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameplate.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_nameplate.clip_text = true
|
||||
_nameplate.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_nameplate.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nameplate.add_theme_color_override("font_color", Color(0.12, 0.20, 0.24, 1.0))
|
||||
_nameplate.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
|
||||
_nameplate.add_theme_constant_override("shadow_offset_x", 0)
|
||||
_nameplate.add_theme_constant_override("shadow_offset_y", 1)
|
||||
_nameplate.add_theme_font_size_override("font_size", NAMEPLATE_FONT_SIZE)
|
||||
_nameplate.add_theme_stylebox_override("normal", _create_nameplate_style())
|
||||
|
||||
func _nameplate_visual_width(displayName: String) -> int:
|
||||
var estimatedWidth := displayName.length() * NAMEPLATE_VISUAL_CHAR_WIDTH + 28
|
||||
return clampi(estimatedWidth, NAMEPLATE_VISUAL_MIN_WIDTH, NAMEPLATE_VISUAL_MAX_WIDTH)
|
||||
|
||||
func _create_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
|
||||
1
scenes/characters/NPCController.gd.uid
Normal file
1
scenes/characters/NPCController.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dqgblo33v4mfv
|
||||
283
scenes/characters/PlayerController.gd
Normal file
283
scenes/characters/PlayerController.gd
Normal file
@@ -0,0 +1,283 @@
|
||||
extends CharacterBody2D
|
||||
class_name PlayerController
|
||||
|
||||
# 信号定义
|
||||
signal player_moved(position: Vector2)
|
||||
|
||||
# 常量定义
|
||||
const MOVE_SPEED = 200.0
|
||||
const WORLD_SORT_Z_OFFSET = 2048
|
||||
const INTERACTION_COLLISION_MASK = 2
|
||||
const WALK_ANIMATION_LENGTH = 0.8
|
||||
const NAME_LABEL_OFFSET: Vector2 = Vector2(-70, -112)
|
||||
const DIRECTION_ROWS: Dictionary = {
|
||||
"down": 0,
|
||||
"up": 1,
|
||||
"right": 2,
|
||||
"left": 3,
|
||||
}
|
||||
|
||||
# 节点引用
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
@onready var ray_cast: RayCast2D = $RayCast2D
|
||||
|
||||
var lastDirection: String = "down"
|
||||
var _nameLabel: Label
|
||||
var _movementLocked: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_reset_movement_input_state()
|
||||
_apply_current_appearance()
|
||||
_subscribe_to_appearance_events()
|
||||
_create_name_label()
|
||||
_update_name_label()
|
||||
|
||||
# 检查是否有初始位置设置
|
||||
call_deferred("_check_spawn_position")
|
||||
|
||||
# 播放初始动画
|
||||
if animation_player.has_animation("idle_down"):
|
||||
animation_player.play("idle_down")
|
||||
|
||||
# Initialize RayCast
|
||||
ray_cast.add_exception(self) # Ignore local player
|
||||
ray_cast.enabled = true
|
||||
ray_cast.collide_with_areas = true
|
||||
ray_cast.collide_with_bodies = true
|
||||
ray_cast.collision_mask = INTERACTION_COLLISION_MASK
|
||||
ray_cast.target_position = Vector2(0, 60)
|
||||
_update_world_sort_z()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.APPEARANCE_SKIN_CHANGED, _on_appearance_skin_changed, self)
|
||||
eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_APPLICATION_FOCUS_IN:
|
||||
_reset_movement_input_state()
|
||||
|
||||
func _reset_movement_input_state() -> void:
|
||||
Input.flush_buffered_events()
|
||||
_release_movement_actions()
|
||||
|
||||
func _release_movement_actions() -> void:
|
||||
Input.action_release("move_left")
|
||||
Input.action_release("move_right")
|
||||
Input.action_release("move_up")
|
||||
Input.action_release("move_down")
|
||||
|
||||
func _check_spawn_position() -> void:
|
||||
var spawnPos: Variant = SceneManager.get_next_scene_position()
|
||||
if spawnPos != null:
|
||||
global_position = spawnPos
|
||||
_update_world_sort_z()
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_handle_movement(delta)
|
||||
_update_world_sort_z()
|
||||
_handle_interaction()
|
||||
|
||||
func _handle_interaction() -> void:
|
||||
if _is_text_input_focused():
|
||||
return
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
EventSystem.emit_event(EventNames.INTERACT_PRESSED, {
|
||||
"player": self,
|
||||
"position": global_position,
|
||||
"direction": lastDirection
|
||||
})
|
||||
if ray_cast.is_colliding():
|
||||
var collider := ray_cast.get_collider()
|
||||
if collider and collider.has_method("interact"):
|
||||
collider.interact()
|
||||
|
||||
func _handle_movement(_delta: float) -> void:
|
||||
if _movementLocked:
|
||||
_release_movement_actions()
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
# 输入框获得焦点时禁止移动,避免聊天/表单输入影响角色
|
||||
if _is_text_input_focused():
|
||||
_release_movement_actions()
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
# 获取移动向量 (参考 docs/02-开发规范/输入映射配置.md)
|
||||
var direction := Input.get_vector(
|
||||
"move_left", "move_right",
|
||||
"move_up", "move_down"
|
||||
)
|
||||
|
||||
# 应用移动
|
||||
if direction != Vector2.ZERO:
|
||||
velocity = direction * MOVE_SPEED
|
||||
_update_animation_state(direction)
|
||||
else:
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
|
||||
move_and_slide()
|
||||
|
||||
# 发送移动事件 (如果位置发生明显变化)
|
||||
if velocity.length() > 0:
|
||||
player_moved.emit(global_position)
|
||||
EventSystem.emit_event(EventNames.PLAYER_MOVED, {
|
||||
"position": global_position
|
||||
})
|
||||
|
||||
func _update_animation_state(direction: Vector2) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
# Determine primary direction
|
||||
if abs(direction.x) > abs(direction.y):
|
||||
if direction.x > 0:
|
||||
lastDirection = "right"
|
||||
ray_cast.target_position = Vector2(60, 0)
|
||||
else:
|
||||
lastDirection = "left"
|
||||
ray_cast.target_position = Vector2(-60, 0)
|
||||
else:
|
||||
if direction.y > 0:
|
||||
lastDirection = "down"
|
||||
ray_cast.target_position = Vector2(0, 60)
|
||||
else:
|
||||
lastDirection = "up"
|
||||
ray_cast.target_position = Vector2(0, -60)
|
||||
|
||||
animation_player.play("walk_" + lastDirection)
|
||||
|
||||
func _play_idle_animation() -> void:
|
||||
if animation_player:
|
||||
animation_player.play("idle_" + lastDirection)
|
||||
|
||||
func set_movement_locked(locked: bool) -> void:
|
||||
_movementLocked = locked
|
||||
if locked:
|
||||
_release_movement_actions()
|
||||
velocity = Vector2.ZERO
|
||||
_play_idle_animation()
|
||||
|
||||
func _update_world_sort_z() -> void:
|
||||
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
|
||||
|
||||
func _subscribe_to_appearance_events() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("connect_event", EventNames.APPEARANCE_SKIN_CHANGED, _on_appearance_skin_changed, self)
|
||||
eventSystem.call("connect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
|
||||
func _on_appearance_skin_changed(_data: Dictionary) -> void:
|
||||
_apply_current_appearance()
|
||||
|
||||
func _on_settings_changed(_data: Dictionary) -> void:
|
||||
_update_name_label()
|
||||
|
||||
func _apply_current_appearance() -> void:
|
||||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
||||
if appearanceManager != null and appearanceManager.has_method("apply_skin_to_sprite"):
|
||||
appearanceManager.call("apply_skin_to_sprite", sprite)
|
||||
_configure_directional_animations()
|
||||
|
||||
func _configure_directional_animations() -> void:
|
||||
if animation_player == null or sprite == null:
|
||||
return
|
||||
|
||||
var library := animation_player.get_animation_library("")
|
||||
if library == null:
|
||||
library = AnimationLibrary.new()
|
||||
animation_player.add_animation_library("", library)
|
||||
|
||||
var frameCount: int = max(1, sprite.hframes)
|
||||
for direction in DIRECTION_ROWS.keys():
|
||||
var row := int(DIRECTION_ROWS[direction])
|
||||
_set_frame_animation(library, "idle_%s" % direction, [row * frameCount], 0.1)
|
||||
var walkFrames: Array[int] = []
|
||||
for column in range(frameCount):
|
||||
walkFrames.append(row * frameCount + column)
|
||||
_set_frame_animation(library, "walk_%s" % direction, walkFrames, WALK_ANIMATION_LENGTH)
|
||||
|
||||
func _set_frame_animation(library: AnimationLibrary, animationName: String, frameValues: Array[int], length: float) -> void:
|
||||
if library.has_animation(animationName):
|
||||
library.remove_animation(animationName)
|
||||
|
||||
var animation := Animation.new()
|
||||
animation.resource_name = animationName
|
||||
animation.length = length
|
||||
animation.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
var trackIndex := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(trackIndex, NodePath("Sprite2D:frame"))
|
||||
animation.value_track_set_update_mode(trackIndex, Animation.UPDATE_DISCRETE)
|
||||
|
||||
var frameStep: float = length / float(max(1, frameValues.size()))
|
||||
for index in range(frameValues.size()):
|
||||
animation.track_insert_key(trackIndex, index * frameStep, frameValues[index])
|
||||
|
||||
library.add_animation(animationName, animation)
|
||||
|
||||
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 _create_name_label() -> void:
|
||||
if is_instance_valid(_nameLabel):
|
||||
return
|
||||
_nameLabel = Label.new()
|
||||
_nameLabel.name = "NameLabel"
|
||||
_nameLabel.position = NAME_LABEL_OFFSET
|
||||
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
||||
_nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
||||
add_child(_nameLabel)
|
||||
|
||||
func _update_name_label() -> void:
|
||||
if not is_instance_valid(_nameLabel):
|
||||
return
|
||||
_nameLabel.text = _current_username()
|
||||
_nameLabel.visible = _settings_bool("show_name_always", false)
|
||||
|
||||
func _current_username() -> String:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager != null and authManager.has_method("get_current_username"):
|
||||
var username := str(authManager.call("get_current_username")).strip_edges()
|
||||
if not username.is_empty():
|
||||
return username
|
||||
return "玩家"
|
||||
|
||||
func _settings_bool(key: String, defaultValue: bool) -> bool:
|
||||
var settingsManager := get_node_or_null("/root/SettingsManager")
|
||||
if settingsManager != null and settingsManager.has_method("get_bool"):
|
||||
return bool(settingsManager.call("get_bool", key))
|
||||
return defaultValue
|
||||
|
||||
func _create_name_label_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 1.0, 1.0, 0.74)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
1
scenes/characters/PlayerController.gd.uid
Normal file
1
scenes/characters/PlayerController.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://lwki8qk6adle
|
||||
375
scenes/characters/RemotePlayer.gd
Normal file
375
scenes/characters/RemotePlayer.gd
Normal file
@@ -0,0 +1,375 @@
|
||||
extends CharacterBody2D
|
||||
class_name RemotePlayer
|
||||
|
||||
# 远程玩家脚本
|
||||
# 负责处理位置同步和动画播放
|
||||
# 严格遵循 Visual Only 原则:无输入处理,无物理碰撞
|
||||
|
||||
var userId: String = ""
|
||||
var username: String = ""
|
||||
var skinId: String = ""
|
||||
var skinAsset: Dictionary = {}
|
||||
var avatarId: String = ""
|
||||
var targetPosition: Vector2 = Vector2.ZERO
|
||||
var cafeCompanionData: Dictionary = {}
|
||||
|
||||
# 内部状态
|
||||
var lastDirection: String = "down"
|
||||
const WORLD_SORT_Z_OFFSET = 2048
|
||||
const WALK_ANIMATION_LENGTH = 0.8
|
||||
const NAME_LABEL_OFFSET: Vector2 = Vector2(-70, -112)
|
||||
const CAFE_NAME_LABEL_RENDER_SCALE: float = 0.5
|
||||
const CAFE_NAME_LABEL_FONT_SIZE: int = 24
|
||||
const CAFE_NAME_LABEL_VISUAL_HEIGHT: int = 22
|
||||
const CAFE_NAME_LABEL_VISUAL_MIN_WIDTH: int = 76
|
||||
const CAFE_NAME_LABEL_VISUAL_MAX_WIDTH: int = 118
|
||||
const CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH: int = 12
|
||||
const CAFE_NAME_LABEL_OFFSET_Y: float = -96.0
|
||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||
const DIRECTION_ROWS: Dictionary = {
|
||||
"down": 0,
|
||||
"up": 1,
|
||||
"right": 2,
|
||||
"left": 3,
|
||||
}
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
var _nameLabel: Label
|
||||
var _cafeCompanionTarget: CafeCompanionTarget
|
||||
|
||||
func _ready() -> void:
|
||||
# 初始化时确保无物理处理
|
||||
set_physics_process(false)
|
||||
# 初始位置设为当前位置
|
||||
targetPosition = global_position
|
||||
_apply_appearance()
|
||||
_update_world_sort_z()
|
||||
_create_name_label()
|
||||
_update_name_label()
|
||||
_subscribe_to_settings_events()
|
||||
|
||||
# 确保禁用物理碰撞 (双重保险)
|
||||
if has_node("CollisionShape2D"):
|
||||
$CollisionShape2D.disabled = true
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# 1. 平滑移动插值
|
||||
var currentPos: Vector2 = global_position
|
||||
var distance: float = currentPos.distance_to(targetPosition)
|
||||
|
||||
if distance > 1.0:
|
||||
# 简单的线性插值,速度系数 10.0 可根据需要调整
|
||||
var newPos: Vector2 = currentPos.lerp(targetPosition, 10.0 * delta)
|
||||
|
||||
# 计算移动向量用于动画朝向
|
||||
var moveVec: Vector2 = newPos - currentPos
|
||||
_update_animation(moveVec)
|
||||
|
||||
global_position = newPos
|
||||
_update_world_sort_z()
|
||||
else:
|
||||
# 距离很近时直接吸附并播放待机动画
|
||||
global_position = targetPosition
|
||||
_update_world_sort_z()
|
||||
_play_idle_animation()
|
||||
|
||||
# 统一初始化方法
|
||||
# data: 包含 camelCase 字段的字典 (userId, username, position 等)
|
||||
func setup(data: Dictionary) -> void:
|
||||
if data.has("userId"):
|
||||
userId = data.userId
|
||||
if data.has("username"):
|
||||
username = str(data.username)
|
||||
if data.has("skin_id"):
|
||||
skinId = str(data.get("skin_id", ""))
|
||||
elif data.has("skinId"):
|
||||
skinId = str(data.get("skinId", ""))
|
||||
if data.has("skin_asset"):
|
||||
var skinAssetData: Variant = data.get("skin_asset", {})
|
||||
skinAsset = skinAssetData if skinAssetData is Dictionary else {}
|
||||
elif data.has("skinAsset"):
|
||||
var skinAssetPayload: Variant = data.get("skinAsset", {})
|
||||
skinAsset = skinAssetPayload if skinAssetPayload is Dictionary else {}
|
||||
if data.has("avatar_id"):
|
||||
avatarId = str(data.get("avatar_id", ""))
|
||||
elif data.has("avatarId"):
|
||||
avatarId = str(data.get("avatarId", ""))
|
||||
if data.has("cafe_companion") or data.has("cafeCompanion"):
|
||||
cafeCompanionData = _normalize_cafe_companion_data(data.get("cafe_companion", data.get("cafeCompanion", null)))
|
||||
_apply_appearance()
|
||||
_configure_cafe_companion_target()
|
||||
_update_name_label()
|
||||
|
||||
if data.has("position"):
|
||||
var positionData: Variant = data.position
|
||||
if positionData is Vector2:
|
||||
global_position = positionData
|
||||
targetPosition = positionData
|
||||
_update_world_sort_z()
|
||||
elif positionData.has("x") and positionData.has("y"):
|
||||
var newPos := Vector2(positionData.x, positionData.y)
|
||||
global_position = newPos
|
||||
targetPosition = newPos
|
||||
_update_world_sort_z()
|
||||
|
||||
# 更新目标位置
|
||||
func update_position(newPos: Vector2) -> void:
|
||||
targetPosition = newPos
|
||||
|
||||
func _update_world_sort_z() -> void:
|
||||
z_index = WORLD_SORT_Z_OFFSET + int(round(global_position.y))
|
||||
|
||||
func _update_animation(moveVec: Vector2) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
# 确定主方向
|
||||
if abs(moveVec.x) > abs(moveVec.y):
|
||||
if moveVec.x > 0:
|
||||
lastDirection = "right"
|
||||
else:
|
||||
lastDirection = "left"
|
||||
else:
|
||||
if moveVec.y > 0:
|
||||
lastDirection = "down"
|
||||
else:
|
||||
lastDirection = "up"
|
||||
|
||||
animation_player.play("walk_" + lastDirection)
|
||||
|
||||
func _play_idle_animation() -> void:
|
||||
if animation_player:
|
||||
animation_player.play("idle_" + lastDirection)
|
||||
|
||||
func _apply_appearance() -> void:
|
||||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
||||
if appearanceManager == null:
|
||||
return
|
||||
if not skinAsset.is_empty() and appearanceManager.has_method("apply_skin_asset_to_sprite"):
|
||||
appearanceManager.call("apply_skin_asset_to_sprite", sprite, skinId, skinAsset)
|
||||
elif appearanceManager.has_method("apply_skin_to_sprite"):
|
||||
appearanceManager.call("apply_skin_to_sprite", sprite, skinId)
|
||||
_configure_directional_animations()
|
||||
|
||||
func _configure_directional_animations() -> void:
|
||||
if animation_player == null or sprite == null:
|
||||
return
|
||||
|
||||
var library := animation_player.get_animation_library("")
|
||||
if library == null:
|
||||
library = AnimationLibrary.new()
|
||||
animation_player.add_animation_library("", library)
|
||||
|
||||
var frameCount: int = max(1, sprite.hframes)
|
||||
for direction in DIRECTION_ROWS.keys():
|
||||
var row := int(DIRECTION_ROWS[direction])
|
||||
_set_frame_animation(library, "idle_%s" % direction, [row * frameCount], 0.1)
|
||||
var walkFrames: Array[int] = []
|
||||
for column in range(frameCount):
|
||||
walkFrames.append(row * frameCount + column)
|
||||
_set_frame_animation(library, "walk_%s" % direction, walkFrames, WALK_ANIMATION_LENGTH)
|
||||
|
||||
func _set_frame_animation(library: AnimationLibrary, animationName: String, frameValues: Array[int], length: float) -> void:
|
||||
if library.has_animation(animationName):
|
||||
library.remove_animation(animationName)
|
||||
|
||||
var animation := Animation.new()
|
||||
animation.resource_name = animationName
|
||||
animation.length = length
|
||||
animation.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
var trackIndex := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(trackIndex, NodePath("Sprite2D:frame"))
|
||||
animation.value_track_set_update_mode(trackIndex, Animation.UPDATE_DISCRETE)
|
||||
|
||||
var frameStep: float = length / float(max(1, frameValues.size()))
|
||||
for index in range(frameValues.size()):
|
||||
animation.track_insert_key(trackIndex, index * frameStep, frameValues[index])
|
||||
|
||||
library.add_animation(animationName, animation)
|
||||
|
||||
func _subscribe_to_settings_events() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("connect_event", EventNames.SETTINGS_CHANGED, _on_settings_changed, self)
|
||||
|
||||
func _on_settings_changed(_data: Dictionary) -> void:
|
||||
_update_name_label()
|
||||
|
||||
func _create_name_label() -> void:
|
||||
if is_instance_valid(_nameLabel):
|
||||
return
|
||||
_nameLabel = Label.new()
|
||||
_nameLabel.name = "NameLabel"
|
||||
_nameLabel.position = NAME_LABEL_OFFSET
|
||||
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
||||
_nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
||||
add_child(_nameLabel)
|
||||
|
||||
func _update_name_label() -> void:
|
||||
if not is_instance_valid(_nameLabel):
|
||||
return
|
||||
var personaName := str(cafeCompanionData.get("persona_name", "")).strip_edges()
|
||||
if not personaName.is_empty():
|
||||
_configure_cafe_name_label(personaName)
|
||||
_nameLabel.visible = true
|
||||
return
|
||||
_configure_default_name_label()
|
||||
_nameLabel.text = username if not username.strip_edges().is_empty() else "玩家"
|
||||
_nameLabel.visible = _settings_bool("show_name_always", false)
|
||||
|
||||
func _configure_cafe_name_label(personaName: String) -> void:
|
||||
var displayName := personaName.strip_edges()
|
||||
if displayName.is_empty():
|
||||
displayName = "陪伴机器人"
|
||||
var visualWidth := _cafe_name_label_visual_width(displayName)
|
||||
var renderWidth := ceili(float(visualWidth) / CAFE_NAME_LABEL_RENDER_SCALE)
|
||||
var renderHeight := ceili(float(CAFE_NAME_LABEL_VISUAL_HEIGHT) / CAFE_NAME_LABEL_RENDER_SCALE)
|
||||
|
||||
_nameLabel.theme = WORLD_TEXT_THEME
|
||||
_nameLabel.text = displayName
|
||||
_nameLabel.z_index = 30
|
||||
_nameLabel.scale = Vector2.ONE * CAFE_NAME_LABEL_RENDER_SCALE
|
||||
_nameLabel.position = Vector2(float(visualWidth) * -0.5, CAFE_NAME_LABEL_OFFSET_Y)
|
||||
_nameLabel.custom_minimum_size = Vector2(renderWidth, renderHeight)
|
||||
_nameLabel.size = _nameLabel.custom_minimum_size
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_nameLabel.clip_text = true
|
||||
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.12, 0.20, 0.24, 1.0))
|
||||
_nameLabel.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
|
||||
_nameLabel.add_theme_constant_override("shadow_offset_x", 0)
|
||||
_nameLabel.add_theme_constant_override("shadow_offset_y", 1)
|
||||
_nameLabel.add_theme_font_size_override("font_size", CAFE_NAME_LABEL_FONT_SIZE)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_cafe_name_label_style())
|
||||
|
||||
func _configure_default_name_label() -> void:
|
||||
_nameLabel.theme = null
|
||||
_nameLabel.position = NAME_LABEL_OFFSET
|
||||
_nameLabel.scale = Vector2.ONE
|
||||
_nameLabel.custom_minimum_size = Vector2(140, 28)
|
||||
_nameLabel.size = _nameLabel.custom_minimum_size
|
||||
_nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
_nameLabel.clip_text = false
|
||||
_nameLabel.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
|
||||
_nameLabel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_nameLabel.add_theme_color_override("font_color", Color(0.188, 0.294, 0.424))
|
||||
_nameLabel.remove_theme_color_override("font_shadow_color")
|
||||
_nameLabel.remove_theme_constant_override("shadow_offset_x")
|
||||
_nameLabel.remove_theme_constant_override("shadow_offset_y")
|
||||
_nameLabel.add_theme_font_size_override("font_size", 14)
|
||||
_nameLabel.add_theme_stylebox_override("normal", _create_name_label_style())
|
||||
|
||||
func _cafe_name_label_visual_width(displayName: String) -> int:
|
||||
var estimatedWidth := displayName.length() * CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH + 28
|
||||
return clampi(estimatedWidth, CAFE_NAME_LABEL_VISUAL_MIN_WIDTH, CAFE_NAME_LABEL_VISUAL_MAX_WIDTH)
|
||||
|
||||
func _configure_cafe_companion_target() -> void:
|
||||
if cafeCompanionData.is_empty():
|
||||
_clear_cafe_companion_target()
|
||||
return
|
||||
|
||||
if not is_instance_valid(_cafeCompanionTarget):
|
||||
_cafeCompanionTarget = CafeCompanionTarget.new()
|
||||
_cafeCompanionTarget.name = "CafeCompanionTarget"
|
||||
_cafeCompanionTarget.position = Vector2(0, -58)
|
||||
_cafeCompanionTarget.collision_layer = 2
|
||||
_cafeCompanionTarget.collision_mask = 0
|
||||
add_child(_cafeCompanionTarget)
|
||||
|
||||
var shapeNode := CollisionShape2D.new()
|
||||
shapeNode.name = "CollisionShape2D"
|
||||
var shape := RectangleShape2D.new()
|
||||
shape.size = Vector2(112, 148)
|
||||
shapeNode.shape = shape
|
||||
_cafeCompanionTarget.add_child(shapeNode)
|
||||
|
||||
_cafeCompanionTarget.servicePointId = str(cafeCompanionData.get("service_point_id", "")).strip_edges()
|
||||
_cafeCompanionTarget.companionId = str(cafeCompanionData.get("companion_id", "")).strip_edges()
|
||||
_cafeCompanionTarget.companionType = str(cafeCompanionData.get("companion_type", "hired_player")).strip_edges()
|
||||
_cafeCompanionTarget.personaName = str(cafeCompanionData.get("persona_name", "")).strip_edges()
|
||||
_cafeCompanionTarget.ownerUserId = str(cafeCompanionData.get("owner_user_id", "")).strip_edges()
|
||||
_cafeCompanionTarget.employmentEndsAt = str(cafeCompanionData.get("employment_ends_at", "")).strip_edges()
|
||||
_cafeCompanionTarget.selfTarget = false
|
||||
_cafeCompanionTarget.input_pickable = true
|
||||
|
||||
var shapeNode := _cafeCompanionTarget.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode != null:
|
||||
shapeNode.disabled = false
|
||||
|
||||
func _clear_cafe_companion_target() -> void:
|
||||
if is_instance_valid(_cafeCompanionTarget):
|
||||
_cafeCompanionTarget.queue_free()
|
||||
_cafeCompanionTarget = null
|
||||
|
||||
func _normalize_cafe_companion_data(value: Variant) -> Dictionary:
|
||||
if not (value is Dictionary):
|
||||
return {}
|
||||
|
||||
var raw: Dictionary = value
|
||||
var servicePointId := str(raw.get("servicePointId", raw.get("service_point_id", ""))).strip_edges()
|
||||
var companionId := str(raw.get("companionId", raw.get("companion_id", ""))).strip_edges()
|
||||
var personaName := str(raw.get("personaName", raw.get("persona_name", ""))).strip_edges()
|
||||
if servicePointId.is_empty() or companionId.is_empty() or personaName.is_empty():
|
||||
return {}
|
||||
|
||||
return {
|
||||
"cafe_id": str(raw.get("cafeId", raw.get("cafe_id", "whale_cafe"))).strip_edges(),
|
||||
"service_point_id": servicePointId,
|
||||
"companion_id": companionId,
|
||||
"companion_type": str(raw.get("companionType", raw.get("companion_type", "hired_player"))).strip_edges(),
|
||||
"persona_name": personaName,
|
||||
"employment_ends_at": str(raw.get("employmentEndsAt", raw.get("employment_ends_at", ""))).strip_edges(),
|
||||
"owner_user_id": str(raw.get("ownerUserId", raw.get("owner_user_id", ""))).strip_edges(),
|
||||
}
|
||||
|
||||
func _settings_bool(key: String, defaultValue: bool) -> bool:
|
||||
var settingsManager := get_node_or_null("/root/SettingsManager")
|
||||
if settingsManager != null and settingsManager.has_method("get_bool"):
|
||||
return bool(settingsManager.call("get_bool", key))
|
||||
return defaultValue
|
||||
|
||||
func _create_name_label_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 1.0, 1.0, 0.74)
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
style.corner_radius_bottom_left = 10
|
||||
style.corner_radius_bottom_right = 10
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
|
||||
func _create_cafe_name_label_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
|
||||
1
scenes/characters/RemotePlayer.gd.uid
Normal file
1
scenes/characters/RemotePlayer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ciip727q52j4w
|
||||
68
scenes/characters/cafe_whale_barista_npc.tscn
Normal file
68
scenes/characters/cafe_whale_barista_npc.tscn
Normal file
@@ -0,0 +1,68 @@
|
||||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource type="Texture2D" path="res://assets/characters/cafe_whale_barista_npc_640.png" id="1_texture"]
|
||||
[ext_resource type="Script" path="res://scenes/characters/NPCController.gd" id="2_script"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_CafeWhaleNpc"]
|
||||
size = Vector2(48, 24)
|
||||
|
||||
[sub_resource type="Animation" id="Animation_Reset"]
|
||||
length = 0.001
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_Idle"]
|
||||
resource_name = "idle"
|
||||
length = 1.2
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0.0333333, 0.26666665, 0.4666667, 0.8, 1),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [2, 1, 0, 4, 5]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_CafeWhaleNpc"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_Reset"),
|
||||
&"idle": SubResource("Animation_Idle")
|
||||
}
|
||||
|
||||
[node name="CafeWhaleBaristaNpc" type="CharacterBody2D"]
|
||||
script = ExtResource("2_script")
|
||||
npcName = "海盐拿铁"
|
||||
dialogue = "欢迎来到鲸鱼咖啡馆!我是海盐拿铁,买好陪聊时间后可以和我慢慢聊。"
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture_filter = 2
|
||||
position = Vector2(0, -58)
|
||||
scale = Vector2(0.72, 0.72)
|
||||
texture = ExtResource("1_texture")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
light_mask = 5
|
||||
visibility_layer = 5
|
||||
shape = SubResource("Shape_CafeWhaleNpc")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_CafeWhaleNpc")
|
||||
}
|
||||
67
scenes/characters/crayfish_npc.tscn
Normal file
67
scenes/characters/crayfish_npc.tscn
Normal file
@@ -0,0 +1,67 @@
|
||||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource type="Texture2D" path="res://assets/characters/crayfish_npc_256_256.png" id="1_texture"]
|
||||
[ext_resource type="Script" path="res://scenes/characters/NPCController.gd" id="2_script"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="1_shape"]
|
||||
size = Vector2(44, 22)
|
||||
|
||||
[sub_resource type="Animation" id="2_reset"]
|
||||
length = 0.001
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="3_idle"]
|
||||
resource_name = "idle"
|
||||
length = 1.2
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0.0333333, 0.26666665, 0.4666667, 0.8, 1),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [2, 1, 0, 4, 5]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="4_library"]
|
||||
_data = {
|
||||
&"RESET": SubResource("2_reset"),
|
||||
&"idle": SubResource("3_idle")
|
||||
}
|
||||
|
||||
[node name="CrayfishNpc" type="CharacterBody2D"]
|
||||
script = ExtResource("2_script")
|
||||
npcName = "虾小满"
|
||||
dialogue = "欢迎来到 WhaleTown!我是虾小满,负责看着喷泉边的水路和码头消息。想找热闹的地方,顺着水边走就对啦。"
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture_filter = 2
|
||||
scale = Vector2(0.65, 0.65)
|
||||
texture = ExtResource("1_texture")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
light_mask = 5
|
||||
visibility_layer = 5
|
||||
shape = SubResource("1_shape")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("4_library")
|
||||
}
|
||||
66
scenes/characters/npc.tscn
Normal file
66
scenes/characters/npc.tscn
Normal file
@@ -0,0 +1,66 @@
|
||||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource type="Texture2D" path="res://assets/characters/npc_286_241.png" id="1_2r34a"]
|
||||
[ext_resource type="Script" path="res://scenes/characters/NPCController.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_npc"]
|
||||
size = Vector2(48, 24)
|
||||
|
||||
[sub_resource type="Animation" id="Animation_2r34a"]
|
||||
length = 0.001
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle"]
|
||||
resource_name = "idle"
|
||||
length = 1.2
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0.0333333, 0.26666665, 0.4666667, 0.8, 1),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [2, 1, 0, 4, 5]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_npc"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_2r34a"),
|
||||
&"idle": SubResource("Animation_idle")
|
||||
}
|
||||
|
||||
[node name="NPC" type="CharacterBody2D"]
|
||||
position = Vector2(-8, 0)
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture_filter = 2
|
||||
scale = Vector2(0.72, 0.72)
|
||||
texture = ExtResource("1_2r34a")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
light_mask = 5
|
||||
visibility_layer = 5
|
||||
shape = SubResource("RectangleShape2D_npc")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_npc")
|
||||
}
|
||||
181
scenes/characters/player.tscn
Normal file
181
scenes/characters/player.tscn
Normal file
@@ -0,0 +1,181 @@
|
||||
[gd_scene load_steps=13 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/characters/PlayerController.gd" id="1_script"]
|
||||
[ext_resource type="Texture2D" path="res://assets/characters/player_pixel_spritesheet.png" id="2_texture"]
|
||||
|
||||
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_1"]
|
||||
radius = 21.0
|
||||
height = 48.0
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_down"]
|
||||
resource_name = "idle_down"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_left"]
|
||||
resource_name = "idle_left"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [12]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_right"]
|
||||
resource_name = "idle_right"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [8]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_up"]
|
||||
resource_name = "idle_up"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [4]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_down"]
|
||||
resource_name = "walk_down"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [0, 1, 2, 3]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_left"]
|
||||
resource_name = "walk_left"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [12, 13, 14, 15]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_right"]
|
||||
resource_name = "walk_right"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [8, 9, 10, 11]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_up"]
|
||||
resource_name = "walk_up"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [4, 5, 6, 7]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_1"]
|
||||
_data = {
|
||||
&"idle_down": SubResource("Animation_idle_down"),
|
||||
&"idle_left": SubResource("Animation_idle_left"),
|
||||
&"idle_right": SubResource("Animation_idle_right"),
|
||||
&"idle_up": SubResource("Animation_idle_up"),
|
||||
&"walk_down": SubResource("Animation_walk_down"),
|
||||
&"walk_left": SubResource("Animation_walk_left"),
|
||||
&"walk_right": SubResource("Animation_walk_right"),
|
||||
&"walk_up": SubResource("Animation_walk_up")
|
||||
}
|
||||
|
||||
[node name="Player" type="CharacterBody2D"]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture_filter = 2
|
||||
position = Vector2(0, -31)
|
||||
scale = Vector2(0.5, 0.5)
|
||||
texture = ExtResource("2_texture")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(2, -24)
|
||||
shape = SubResource("CapsuleShape2D_1")
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_1")
|
||||
}
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
zoom = Vector2(2, 2)
|
||||
|
||||
[node name="RayCast2D" type="RayCast2D" parent="."]
|
||||
177
scenes/characters/remote_player.tscn
Normal file
177
scenes/characters/remote_player.tscn
Normal file
@@ -0,0 +1,177 @@
|
||||
[gd_scene load_steps=13 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/characters/RemotePlayer.gd" id="1_mu86i"]
|
||||
[ext_resource type="Texture2D" path="res://assets/characters/player_pixel_spritesheet.png" id="2_7oc7u"]
|
||||
|
||||
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_1"]
|
||||
radius = 21.0
|
||||
height = 48.0
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_down"]
|
||||
resource_name = "idle_down"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [0]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_left"]
|
||||
resource_name = "idle_left"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [12]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_right"]
|
||||
resource_name = "idle_right"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [8]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_idle_up"]
|
||||
resource_name = "idle_up"
|
||||
length = 0.1
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [4]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_down"]
|
||||
resource_name = "walk_down"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [0, 1, 2, 3]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_left"]
|
||||
resource_name = "walk_left"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [12, 13, 14, 15]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_right"]
|
||||
resource_name = "walk_right"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [8, 9, 10, 11]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_walk_up"]
|
||||
resource_name = "walk_up"
|
||||
length = 0.8
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.2, 0.4, 0.6),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [4, 5, 6, 7]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_1"]
|
||||
_data = {
|
||||
&"idle_down": SubResource("Animation_idle_down"),
|
||||
&"idle_left": SubResource("Animation_idle_left"),
|
||||
&"idle_right": SubResource("Animation_idle_right"),
|
||||
&"idle_up": SubResource("Animation_idle_up"),
|
||||
&"walk_down": SubResource("Animation_walk_down"),
|
||||
&"walk_left": SubResource("Animation_walk_left"),
|
||||
&"walk_right": SubResource("Animation_walk_right"),
|
||||
&"walk_up": SubResource("Animation_walk_up")
|
||||
}
|
||||
|
||||
[node name="RemotePlayer" type="CharacterBody2D"]
|
||||
script = ExtResource("1_mu86i")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
texture_filter = 2
|
||||
position = Vector2(0, -31)
|
||||
scale = Vector2(0.5, 0.5)
|
||||
texture = ExtResource("2_7oc7u")
|
||||
hframes = 4
|
||||
vframes = 4
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
position = Vector2(2, -24)
|
||||
shape = SubResource("CapsuleShape2D_1")
|
||||
disabled = true
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
libraries = {
|
||||
&"": SubResource("AnimationLibrary_1")
|
||||
}
|
||||
Reference in New Issue
Block a user