399 lines
12 KiB
GDScript
399 lines
12 KiB
GDScript
@tool
|
||
extends CharacterBody2D
|
||
|
||
class_name MainPlayer
|
||
|
||
signal position_updated(global_pos: Vector2)
|
||
# 保证每次 pop_os 后都会有一次 os_finished 信号
|
||
signal os_finished
|
||
signal animation_finished
|
||
|
||
@export var hide_sprite := false:
|
||
set(val):
|
||
hide_sprite = val
|
||
if is_node_ready():
|
||
sprite.visible = not hide_sprite
|
||
@export var enable_light := true:
|
||
set(val):
|
||
enable_light = val
|
||
if is_node_ready():
|
||
light.enabled = enable_light
|
||
@export var catty_light_energy := 0.3
|
||
@export var lock_move_left := false
|
||
@export var lock_move_right := false
|
||
@export var reenter_lock: PlayerReenterLock
|
||
@export var camera_marker: CameraFocusMarker
|
||
@export_enum("吕萍", "吕萍爬行", "吕萍带小猫", "吕萍推柜子", "小小蝶", "盒子猫", "小小小蝶") var character := "吕萍":
|
||
set(val):
|
||
character = val
|
||
if is_node_ready():
|
||
# 切换 inventory
|
||
_check_character_runtime_status()
|
||
# @export var shadow_color := Color(0.1, 0.1, 0.1, 0.7)
|
||
# var shadow_y := 0.0
|
||
@export var player_movement_rect := Rect2(50, -500, 1400, 1000)
|
||
@export var velocity_ratio := 1.0
|
||
@export var running_locked := false
|
||
@export_enum("idle", "walking", "running") var current_status := 0
|
||
@export var facing_direction := Vector2(1.0, -1.0)
|
||
|
||
@export var debug_freeze := 0:
|
||
set(val):
|
||
if not Engine.is_editor_hint():
|
||
return
|
||
debug_freeze = val
|
||
if is_node_ready():
|
||
_check_character_runtime_status()
|
||
if val > 3:
|
||
freeze_player(1, val, true)
|
||
|
||
# 使用 new,方便在 editor 中刷新新值
|
||
var current_animation_config: Dictionary
|
||
|
||
@onready var light = $PointLight2D as PointLight2D
|
||
@onready var catty_light = $CattyPointLight2D as PointLight2D
|
||
@onready var footstep_timer = %FootstepTimer as Timer
|
||
@onready var sprite = %AnimatedSprite2D as AnimatedSprite2D
|
||
@onready var os_pivot = %OSPivot as Control
|
||
|
||
|
||
# # animation -> {frame -> {shadow polygon}}
|
||
# var animation_shadow_polygons = {}
|
||
func _ready() -> void:
|
||
sprite.visible = not hide_sprite
|
||
light.enabled = enable_light
|
||
_check_character_runtime_status()
|
||
if not Engine.is_editor_hint():
|
||
os_pivot.os_finished.connect(os_finished.emit)
|
||
footstep_timer.timeout.connect(_on_footstep_timer_timeout)
|
||
footstep_timer.stop()
|
||
sprite.animation_finished.connect(animation_finished.emit)
|
||
if not reenter_lock:
|
||
push_error("reenter_lock is not set.")
|
||
reenter_lock.freeze_changed.connect(_on_freeze_changed)
|
||
|
||
|
||
func reparent_light(node: Node):
|
||
light.reparent(node)
|
||
catty_light.reparent(node)
|
||
|
||
|
||
# func _enter_tree() -> void:
|
||
# if is_node_ready() and not Engine.is_editor_hint():
|
||
# _check_character_runtime_status()
|
||
|
||
|
||
func _check_character_runtime_status():
|
||
# set up animated sprite
|
||
# 使用 new,方便在 editor 中刷新新值
|
||
current_animation_config = PlayerAnimationConfig.new().ANIMATION_CONFIG[character]
|
||
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
|
||
sprite.scale = current_animation_config["scale"]
|
||
# editor 中无需检查下列项目
|
||
if Engine.is_editor_hint() or not is_node_ready() or SceneManager.is_restarting():
|
||
_play_animation()
|
||
return
|
||
# 如果当前是 prop_小猫玩具完整 ,尝试点亮玩家的灯效;否则无需点亮
|
||
if SceneManager.get_current_prop(false) == "prop_小猫玩具完整":
|
||
set_catty_light(true)
|
||
else:
|
||
set_catty_light(false)
|
||
# 设置 inventory
|
||
if not ArchiveManager or not ArchiveManager.archive:
|
||
printerr("player _checkout_inventory failed. ArchiveManager or Archive is not ready.")
|
||
else:
|
||
# 检查角色锁定状态
|
||
running_locked = ArchiveManager.archive.player_running_locked
|
||
# 每个角色有对应 inverntory
|
||
SceneManager.checkout_prop_inventory(character)
|
||
_play_animation()
|
||
|
||
|
||
func _on_footstep_timer_timeout():
|
||
# ground node is sibling of the player node.
|
||
var ground = get_parent() as Ground2D
|
||
if ground:
|
||
ground.play_footstep_sound()
|
||
|
||
|
||
# return whether the player status or facing direction has changed.
|
||
func _check_status(direction) -> void:
|
||
var tmp_status = current_status
|
||
var new_facing_direction := facing_direction
|
||
if direction.x:
|
||
new_facing_direction.x = direction.x
|
||
tmp_status = PlayerAnimationConfig.MOVEMENT_WALKING
|
||
if (
|
||
not running_locked
|
||
and Input.is_action_pressed("run")
|
||
and current_animation_config["can_run"]
|
||
):
|
||
tmp_status = PlayerAnimationConfig.MOVEMENT_RUNNING
|
||
else:
|
||
tmp_status = PlayerAnimationConfig.MOVEMENT_IDLE
|
||
if new_facing_direction != facing_direction or tmp_status != current_status:
|
||
facing_direction = new_facing_direction
|
||
current_status = tmp_status
|
||
_play_animation()
|
||
|
||
|
||
func _play_animation() -> void:
|
||
if not sprite:
|
||
return
|
||
sprite.offset = current_animation_config["foot_offset"]
|
||
sprite.scale = current_animation_config["scale"]
|
||
# reset the os label position on animation changed.
|
||
_reset_os_and_light_position()
|
||
# 检查动画基础偏移
|
||
check_foot_offset()
|
||
# 进一步偏移+播放动画
|
||
var config = current_animation_config[current_status]
|
||
_sprite_play_with_auto_flip_h(config[0], config[1])
|
||
if facing_direction.x > 0.0:
|
||
if config.size() > 3:
|
||
sprite.offset += config[3]
|
||
else:
|
||
if config.size() > 2:
|
||
sprite.offset += config[2]
|
||
# 播放脚步音效
|
||
match current_status:
|
||
PlayerAnimationConfig.MOVEMENT_IDLE:
|
||
footstep_timer.stop()
|
||
PlayerAnimationConfig.MOVEMENT_WALKING:
|
||
footstep_timer.wait_time = current_animation_config["walk_footstep"]
|
||
footstep_timer.start()
|
||
PlayerAnimationConfig.MOVEMENT_RUNNING:
|
||
footstep_timer.wait_time = current_animation_config["run_footstep"]
|
||
footstep_timer.start()
|
||
# 在编辑器中不播放动画
|
||
if Engine.is_editor_hint():
|
||
footstep_timer.stop()
|
||
sprite.stop()
|
||
|
||
|
||
func _sprite_play_with_auto_flip_h(left_animation: String, right_animation: String) -> String:
|
||
if facing_direction.x > 0.0:
|
||
if right_animation:
|
||
sprite.flip_h = false
|
||
sprite.play(right_animation)
|
||
return right_animation
|
||
else:
|
||
sprite.flip_h = true
|
||
sprite.play(left_animation)
|
||
return left_animation
|
||
else:
|
||
if left_animation:
|
||
sprite.flip_h = false
|
||
sprite.play(left_animation)
|
||
return left_animation
|
||
else:
|
||
sprite.flip_h = true
|
||
sprite.play(right_animation)
|
||
return right_animation
|
||
|
||
|
||
func _get_speed(direction: Vector2) -> Vector2:
|
||
match current_status:
|
||
PlayerAnimationConfig.MOVEMENT_WALKING:
|
||
var speed_walking = current_animation_config["speed_walking"]
|
||
return Vector2(speed_walking * direction.x, 0.0)
|
||
PlayerAnimationConfig.MOVEMENT_RUNNING:
|
||
var speed_runnig = current_animation_config["speed_runnig"]
|
||
return Vector2(speed_runnig * direction.x, 0.0)
|
||
return Vector2(0, 0)
|
||
|
||
|
||
func _physics_process(_delta: float) -> void:
|
||
if Engine.is_editor_hint() or (reenter_lock and reenter_lock.is_frozen()):
|
||
velocity = Vector2.ZERO
|
||
return
|
||
var x_direction := Input.get_axis("left", "right")
|
||
var y_direction := Input.get_axis("up", "down")
|
||
var direction := Vector2(x_direction, y_direction)
|
||
# 限制左右移动
|
||
if direction.x > 0 and lock_move_right:
|
||
direction.x = 0
|
||
elif direction.x < 0 and lock_move_left:
|
||
direction.x = 0
|
||
_check_status(direction)
|
||
var speed := _get_speed(direction) as Vector2
|
||
velocity.x = move_toward(velocity.x, speed.x, 300.0) * velocity_ratio
|
||
velocity.y = move_toward(velocity.y, speed.y, 300.0) * velocity_ratio
|
||
# var x = global_position.x
|
||
move_and_slide()
|
||
global_position = global_position.clamp(player_movement_rect.position, player_movement_rect.end)
|
||
# var delta_x = global_position.x - x
|
||
# if delta_x:
|
||
# SceneManager.player_moved_delta_x(delta_x)
|
||
_tweak_camera_marker()
|
||
position_updated.emit(global_position)
|
||
|
||
|
||
# drag the camera marker against the player movement
|
||
# so there will be a better vision in front of the player.
|
||
func _tweak_camera_marker():
|
||
if camera_marker != null:
|
||
camera_marker.tweak_position(velocity, facing_direction)
|
||
|
||
|
||
func set_catty_light(enable := false):
|
||
# 如果没有允许光照,那么 catty 的光也不添加
|
||
if not enable_light:
|
||
return
|
||
var tween = create_tween()
|
||
if enable:
|
||
tween.tween_property(catty_light, "energy", catty_light_energy, 0.5)
|
||
else:
|
||
tween.tween_property(catty_light, "energy", 0.0, 0.5)
|
||
|
||
|
||
func player_action(action_code: int, auto_quit: bool):
|
||
if current_animation_config.has(action_code):
|
||
# 等价于播放另一种动画的 idle 状态
|
||
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
|
||
# animation_name, scale, offset
|
||
var config = current_animation_config[action_code]
|
||
var animation_l = config[0]
|
||
var animation_r = config[0]
|
||
sprite.scale = config[1]
|
||
sprite.offset = config[2]
|
||
if config.size() >= 5:
|
||
animation_l = config[3]
|
||
animation_r = config[4]
|
||
var playing_animation = _sprite_play_with_auto_flip_h(animation_l, animation_r)
|
||
if auto_quit:
|
||
# reset animation after one play
|
||
if sprite.sprite_frames.get_animation_loop(playing_animation):
|
||
sprite.animation_looped.connect(_play_animation, CONNECT_ONE_SHOT)
|
||
else:
|
||
sprite.animation_finished.connect(_play_animation, CONNECT_ONE_SHOT)
|
||
|
||
|
||
func _on_freeze_changed(count: int, is_add: bool):
|
||
if count == 1 and is_add:
|
||
_on_first_frozen()
|
||
|
||
|
||
# 非首次 freeze 不改变动画状态,因为在动画演出中可能多次 freeze 与 release
|
||
func _on_first_frozen() -> void:
|
||
if GlobalConfig.DEBUG:
|
||
print("player _on_first_frozen. current_status=", current_status)
|
||
# reset status to idle or stay
|
||
velocity = Vector2.ZERO
|
||
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
|
||
_play_animation()
|
||
|
||
|
||
# duration: the time to lock the player action. 0 means lock forever, thus the player will be locked until release_player is called.
|
||
func freeze_player(duration: float, action_code: int, auto_quit: bool) -> void:
|
||
if reenter_lock:
|
||
reenter_lock.freeze(duration)
|
||
else:
|
||
push_error("freeze_player reenter_lock is not set.")
|
||
# 先 freeze 再 action,否则会重置 action
|
||
player_action(action_code, auto_quit)
|
||
|
||
|
||
func release_player():
|
||
if reenter_lock:
|
||
reenter_lock.release()
|
||
# velocity_ratio = 1.0
|
||
# _play_animation()
|
||
|
||
|
||
# 强制播放特定动画
|
||
func force_play_animation(animation: String) -> void:
|
||
sprite.play(animation)
|
||
|
||
|
||
func set_facing_direction(direction: Vector2) -> void:
|
||
facing_direction = direction
|
||
_play_animation()
|
||
|
||
|
||
func _reset_os_and_light_position():
|
||
if sprite and sprite.animation:
|
||
# reset the os label position
|
||
# var texture = sprite.sprite_frames.get_frame_texture(sprite.animation, 0) as Texture2D
|
||
# var size = texture.get_size()
|
||
# os_pivot.position.y = -size.y * 0.5 * sprite.scale.x
|
||
os_pivot.position.y = -abs(current_animation_config["os_height"])
|
||
light.position.y = -abs(current_animation_config["light_height"])
|
||
catty_light.position.y = -abs(current_animation_config["light_height"])
|
||
# reset the shadow position
|
||
# shadow_y = size.y * 0.5
|
||
|
||
|
||
func pop_os(lines := []) -> void:
|
||
await os_pivot.pop_os(lines)
|
||
|
||
|
||
# animation -> offset_y
|
||
var foot_offset_dict = {}
|
||
|
||
|
||
func check_foot_offset():
|
||
var animation = sprite.animation
|
||
if (
|
||
current_animation_config.has("auto_foot_offset")
|
||
and current_animation_config["auto_foot_offset"]
|
||
):
|
||
sprite.offset = Vector2.ZERO
|
||
if not foot_offset_dict.has(animation):
|
||
var frame_y = sprite.sprite_frames.get_frame_texture(animation, 0).get_size().y
|
||
foot_offset_dict[animation] = frame_y * 0.5
|
||
# 从屏幕下边缘算起
|
||
global_position.y = 158.0 - foot_offset_dict[animation]
|
||
elif current_animation_config.has("foot_offset"):
|
||
sprite.offset = current_animation_config["foot_offset"]
|
||
|
||
|
||
# 从地面高度设置 y 坐标
|
||
func set_y_from_ground(player_y: float) -> void:
|
||
if (
|
||
current_animation_config.has("auto_foot_offset")
|
||
and current_animation_config["auto_foot_offset"]
|
||
):
|
||
# 无视地面 y,使用动画帧中的 y
|
||
if GlobalConfig.DEBUG:
|
||
print("set_y_from_ground: auto_foot_offset")
|
||
else:
|
||
global_position.y = player_y
|
||
|
||
|
||
func walk_to_x(global_pos_x: float) -> Tween:
|
||
return walk_to(Vector2(global_pos_x, global_position.y))
|
||
|
||
|
||
# auto freeze and release
|
||
func walk_to(global_pos: Vector2) -> Tween:
|
||
var tween = create_tween()
|
||
velocity = Vector2.ZERO
|
||
if GlobalConfig.DEBUG:
|
||
print("walk_to:", global_pos, " from:", global_position)
|
||
# 不同距离下,行走时长略做自适应
|
||
var time_cost = absf(global_pos.distance_to(global_position) / 50.0)
|
||
# 忽略过小的位移
|
||
if time_cost >= 0.05:
|
||
if GlobalConfig.DEBUG:
|
||
print("walk_to start. lock player. time_cost:", time_cost)
|
||
SceneManager.lock_player()
|
||
var direction = facing_direction
|
||
if global_pos.x < global_position.x:
|
||
direction.x = -1.0
|
||
elif global_pos.x > global_position.x:
|
||
direction.x = 1.0
|
||
_check_status(direction)
|
||
_play_animation()
|
||
tween.tween_property(self, "global_position", global_pos, time_cost)
|
||
tween.tween_callback(_after_walk_to)
|
||
return tween
|
||
|
||
|
||
func _after_walk_to() -> void:
|
||
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
|
||
_play_animation()
|
||
if GlobalConfig.DEBUG:
|
||
print("walk_to end. unlock player")
|
||
SceneManager.unlock_player()
|