xiandie/scene/player/main_player.gd

392 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@tool
extends CharacterBody2D
class_name MainPlayer
@export_enum("吕萍", "吕萍爬行", "吕萍带小猫", "小小蝶") var character := "吕萍":
set(val):
character = val
# 使用 new方便在 editor 中刷新新值
current_animation_config = PlayerAnimationConfig.new().ANIMATION_CONFIG[val]
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
if is_node_ready():
sprite.scale = current_animation_config["scale"]
# @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
# action_locked 用于设置界面等强制锁定action_freezed 用于查看物品等锁定
# action_locked 优先级高于 action_freezed
# action_locked 对应 lock 与 unlock 方法
@export var action_locked := false:
set(val):
action_locked = val
_process_action_lock()
# action_freezed 对应 freeze 与 release 方法
@export var action_freezed := false:
set(val):
action_freezed = val
_process_action_lock()
@export_enum("idle", "walking", "running") var current_status := 0:
set(val):
current_status = val
_play_animation()
@export var facing_direction := Vector2(1.0, -1.0):
set(val):
facing_direction = val
_play_animation()
# 使用 new方便在 editor 中刷新新值
var current_animation_config := (
PlayerAnimationConfig.new().ANIMATION_CONFIG[character] as Dictionary
)
@onready var footstep_timer = %FootstepTimer as Timer
@onready var sprite = %AnimatedSprite2D as AnimatedSprite2D
@onready var os_pivot = %OSPivot as Control
@onready var os_contaner = %PanelContainer as PanelContainer
@onready var os_label = %OSLabel as DialogueLabel
# # animation -> {frame -> {shadow polygon}}
# var animation_shadow_polygons = {}
func _ready() -> void:
os_contaner.modulate.a = 0.0
# set up animated sprite
sprite.scale = current_animation_config["scale"]
_play_animation()
if Engine.is_editor_hint():
return
footstep_timer.timeout.connect(_on_footstep_timer_timeout)
footstep_timer.stop()
# SceneManager.focus_player(self)
_check_character_status()
func _enter_tree() -> void:
if is_node_ready():
_check_character_status()
func _check_character_status():
# 检查角色锁定状态
running_locked = ArchiveManager.archive.player_running_locked
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()
func _process_action_lock() -> void:
# reset status to idle or stay
if action_locked or action_freezed:
velocity = Vector2.ZERO
if (
current_status == PlayerAnimationConfig.MOVEMENT_WALKING
or current_status == PlayerAnimationConfig.MOVEMENT_RUNNING
):
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
# return whether the player status or facing direction has changed.
func _check_status(direction) -> bool:
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
return true
return false
func set_facing_direction(direction: Vector2) -> void:
facing_direction = direction
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_shadow_position()
# 检查动画基础偏移
check_foot_offset()
# 进一步偏移+播放动画
var config = current_animation_config[current_status]
if facing_direction.x > 0.0:
sprite.play(config[1])
if config.size() > 2:
sprite.offset += config[3]
else:
sprite.play(config[0])
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()
# 显示 os 效果
os_contaner.modulate.a = 1.0
os_label.text = "os 测试文本"
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 action_locked or action_freezed or Engine.is_editor_hint():
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)
_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()
# drag the camera marker against the player movement
# so there will be a better vision in front of the player.
func _tweak_camera_marker():
SceneManager.get_camera_marker().tweak_position(velocity, facing_direction)
var lock_mutex = Mutex.new()
var release_timer: SceneTreeTimer
# lock_time: 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(lock_time: float, action_animation: int, auto_quit: bool) -> void:
lock_mutex.lock()
if not action_freezed:
action_freezed = true
if current_animation_config.has(action_animation):
# animation_name, scale, offset
var config = current_animation_config[action_animation]
var animation = config[0]
sprite.scale = config[1]
sprite.offset = config[2]
if not animation and config.size() >= 5:
animation = config[4] if facing_direction.x > 0.0 else config[3]
if animation and sprite.sprite_frames.has_animation(animation):
sprite.sprite_frames.set_animation_loop(animation, false)
sprite.play(animation)
if auto_quit:
# reset animation after one play
sprite.animation_finished.connect(_play_animation, CONNECT_ONE_SHOT)
if lock_time:
if release_timer and release_timer.time_left > 0:
release_timer.time_left = max(lock_time, release_timer.time_left)
else:
release_timer = get_tree().create_timer(lock_time)
release_timer.timeout.connect(release_player)
lock_mutex.unlock()
func freeze_and_play(lock_time: float, animation: String, auto_quit: bool) -> void:
lock_mutex.lock()
if not action_freezed:
action_freezed = true
if animation and sprite.sprite_frames.has_animation(animation):
sprite.sprite_frames.set_animation_loop(animation, false)
sprite.play(animation)
if auto_quit:
# reset animation after one play
sprite.animation_finished.connect(_play_animation, CONNECT_ONE_SHOT)
if lock_time:
if release_timer and release_timer.time_left > 0:
release_timer.time_left = max(lock_time, release_timer.time_left)
else:
release_timer = get_tree().create_timer(lock_time)
release_timer.timeout.connect(release_player)
lock_mutex.unlock()
func release_player():
release_timer = null
lock_mutex.lock()
action_freezed = false
# velocity_ratio = 1.0
lock_mutex.unlock()
# _play_animation()
# func _draw() -> void:
# # 绘制阴影,暂不启用
# var animation = sprite.animation
# if not animation:
# return
# if not animation_shadow_polygons.has(animation):
# _build_shadow_polygons(animation)
# var animation_polygons = animation_shadow_polygons[animation]
# if animation_polygons.has(sprite.frame):
# draw_polygon(animation_polygons[sprite.frame], [shadow_color])
# else:
# printerr("No shadow polygon found for frame %d" % sprite.frame)
# func _build_shadow_polygons(animation):
# var frames = sprite.sprite_frames
# var coords_dict = {}
# for i in frames.get_frame_count(animation):
# var texture = frames.get_frame_texture(animation, i) as Texture2D
# if not texture:
# continue
# var image = texture.get_image()
# var x_min = 10000
# var x_max = -1
# for y in range(texture.get_height()):
# for x in range(texture.get_width()):
# var color = image.get_pixel(x, y)
# if color.a > 0.0:
# x_min = min(x_min, x)
# x_max = max(x_max, x)
# if x_min >= x_max:
# continue
# var oval_ab: Vector2
# oval_ab.x = (x_max - x_min) * 0.5
# oval_ab.y = max(3.0, oval_ab.x * 0.12)
# var x_offset = (x_max - x_min) * 0.5 + x_min - texture.get_width() * 0.5
# var coords: PackedVector2Array
# # build shadow oval shape with segments.
# var segments = 16
# for j in range(segments):
# var angle = PI * 2 / segments * j
# var x = cos(angle) * oval_ab.x + x_offset
# var y = sin(angle) * oval_ab.y + shadow_y
# coords.append(Vector2(x, y))
# coords_dict[i] = coords
# animation_shadow_polygons[animation] = coords_dict
func _reset_os_and_shadow_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"])
# reset the shadow position
# shadow_y = size.y * 0.5
var os_tween: Tween
func pop_os(lines := []):
if os_tween:
os_tween.kill()
os_tween = create_tween()
os_label.text = ""
os_tween.tween_property(os_contaner, "modulate:a", 1.0, 0.2)
for line in lines:
var duration = max(min(4.0, line.text.length() * 0.2), 2.0) - 0.4
os_tween.tween_callback(_os_load_line.bind(line))
os_tween.tween_callback(os_label.type_out)
os_tween.tween_interval(duration)
os_tween.tween_property(os_contaner, "modulate:a", 0.0, 0.2)
func _os_load_line(line):
os_label.dialogue_line = line
# 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(pos: Vector2, duration: float) -> void:
var tween = create_tween()
velocity = Vector2.ZERO
if pos.x < global_position.x:
facing_direction.x = -1.0
elif pos.x > global_position.x:
facing_direction.x = 1.0
_check_status(facing_direction)
_play_animation()
if GlobalConfig.DEBUG:
print("walk_to:", pos, duration, " from:", global_position)
# 不同距离下,行走时长略做自适应
var time_cost = min(duration, global_position.distance_to(pos) / 50.0)
if time_cost < duration:
tween.tween_interval(duration - time_cost)
tween.tween_property(self, "global_position", pos, time_cost)
tween.tween_callback(_after_walk_to)
func _after_walk_to():
velocity = Vector2.ZERO
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
_play_animation()