xiandie/scene/player/main_player.gd

507 lines
16 KiB
GDScript
Raw Permalink 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
signal position_updated(global_pos: Vector2)
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.7
@export var lock_move_left := false
@export var lock_move_right := false
@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
# 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()
@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(0, val, true)
release_player()
# 使用 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
@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:
sprite.visible = not hide_sprite
light.enabled = enable_light
os_contaner.modulate.a = 0.0
_check_character_runtime_status()
_play_animation()
if not Engine.is_editor_hint():
footstep_timer.timeout.connect(_on_footstep_timer_timeout)
footstep_timer.stop()
sprite.animation_finished.connect(animation_finished.emit)
# 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():
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:
# 每个角色有对应 inverntory
var archive = ArchiveManager.archive
# 检查角色锁定状态
running_locked = archive.player_running_locked
match character:
"小小蝶":
SceneManager.checkout_prop_inventory(archive.prop_inventory_xxdie)
"小小小蝶":
SceneManager.checkout_prop_inventory(archive.prop_inventory_xxxdie)
_:
SceneManager.checkout_prop_inventory(archive.prop_inventory)
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 _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()
# 显示 os 效果
os_contaner.modulate.a = 1.0
os_label.text = "os 测试文本"
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 action_locked or action_freezed or Engine.is_editor_hint() or not is_visible_in_tree():
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)
var lock_mutex = Mutex.new()
var release_timer: SceneTreeTimer
# 自动解除对应 animation 的 loop 状态
# 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()
action_freezed = true
if current_animation_config.has(action_animation):
# animation_name, scale, offset
var config = current_animation_config[action_animation]
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)
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 force_play_animation(animation: String) -> void:
sprite.play(animation)
func set_facing_direction(direction: Vector2) -> void:
facing_direction = direction
_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_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
var os_tween: Tween
var os_pausing_timer: SceneTreeTimer
func pop_os(lines := [], auto_freeze := true, auto_release := true) -> Tween:
if os_tween:
os_tween.kill()
if auto_freeze:
freeze_player(0, 3, false)
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.2
# var duration = max(min(4.0, line.text.length() * 0.2), 2.0) - 0.4
os_tween.tween_callback(_os_load_line.bind(line, duration))
os_tween.tween_interval(0.1)
os_tween.tween_callback(os_finished.emit)
os_tween.tween_property(os_contaner, "modulate:a", 0.0, 0.2)
if auto_release:
os_tween.tween_callback(release_player)
return os_tween
func _os_load_line(line, duration):
os_label.dialogue_line = line
os_label.type_out()
os_tween.pause()
if os_pausing_timer and os_pausing_timer.timeout.is_connected(os_tween.play):
os_pausing_timer.timeout.disconnect(os_tween.play)
os_pausing_timer = get_tree().create_timer(duration)
os_pausing_timer.timeout.connect(_on_os_line_timeout, CONNECT_ONE_SHOT)
func _on_os_line_timeout(naturally := true):
if not naturally:
if os_label.is_typing:
os_label.skip_typing()
return
if os_pausing_timer.timeout.is_connected(_on_os_line_timeout):
os_pausing_timer.timeout.disconnect(_on_os_line_timeout)
if os_tween.is_valid():
# os_label.text = ""
os_tween.play()
os_pausing_timer = null
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
if os_pausing_timer and os_pausing_timer.time_left > 0:
_on_os_line_timeout(false)
get_viewport().set_input_as_handled()
# 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()