@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 camera_marker: CameraFocusMarker @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() @export var debug_freeze := 0: set(val): if not Engine.is_editor_hint(): return debug_freeze = val current_animation_config = PlayerAnimationConfig.new().ANIMATION_CONFIG[character] if val > 3: freeze_player(0, val, true) release_player() # 使用 new,方便在 editor 中刷新新值 var current_animation_config := ( PlayerAnimationConfig.new().ANIMATION_CONFIG[character] as 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 # 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() sprite.animation_finished.connect(animation_finished.emit) _check_character_status() # 如果当前是 prop_小猫玩具完整 ,尝试点亮玩家的灯效;否则无需点亮 if SceneManager.get_current_prop(false) == "prop_小猫玩具完整": set_catty_light(true) func _enter_tree() -> void: if is_node_ready() and not Engine.is_editor_hint(): _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 _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] _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) _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() 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_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_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 var os_pausing_timer: SceneTreeTimer func pop_os(lines := [], auto_freeze := true, auto_release := true): 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) 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_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()