xiandie/scene/character/main_player.gd

511 lines
16 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
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.7
@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:
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(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
@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)
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():
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)
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) -> 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 Engine.is_editor_hint() or (reenter_lock and reenter_lock.is_frozen()):
# 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)
func player_action(action_code: int, auto_quit: bool):
if current_animation_config.has(action_code):
# 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:
# reset status to idle or stay
velocity = Vector2.ZERO
if (
current_status == PlayerAnimationConfig.MOVEMENT_WALKING
or current_status == PlayerAnimationConfig.MOVEMENT_RUNNING
):
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
# 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:
player_action(action_code, auto_quit)
if reenter_lock:
reenter_lock.freeze(duration)
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 _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
# 保证每次 pop_os 后都会有一次 os_finished 信号
var os_finish_emit_lock := Mutex.new()
var os_finished_not_emitted := false
var os_pausing_timer: SceneTreeTimer
func pop_os(lines := [], auto_freeze := true, auto_release := true) -> void:
if os_tween:
os_tween.kill()
os_finish_emit_lock.lock()
if os_finished_not_emitted:
os_finished.emit()
os_finished_not_emitted = true
os_finish_emit_lock.unlock()
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_property(os_contaner, "modulate:a", 0.0, 0.2)
if auto_release:
os_tween.tween_callback(release_player)
os_tween.tween_callback(func():
os_finished.emit()
os_finish_emit_lock.lock()
os_finished_not_emitted = false
os_finish_emit_lock.unlock()
)
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_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:
freeze_player(0, 3, false)
if global_pos.x < global_position.x:
facing_direction.x = -1.0
elif global_pos.x > global_position.x:
facing_direction.x = 1.0
_check_status(facing_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:
velocity = Vector2.ZERO
current_status = PlayerAnimationConfig.MOVEMENT_IDLE
_play_animation()
release_player()