Files
sijcdusmode/scripts/player_controller.gd
T
sijcdu f67bd94605 Remove leftover key tile on the crate; damage keeps the player in place
The key object moved next to the spawn, so the key tile painted at cell (10,8) (on top of the crate tile) was left behind and rendered a second key. Cleared it by setting that record's source_id to -1, which is a same-length edit inside the base64 tile_map_data. Also hurt() no longer teleports to the spawn point, it only drains a heart and grants brief i-frames; only dying returns the player to the spawn.
2026-10-07 20:56:54 +08:00

405 lines
11 KiB
GDScript

extends CharacterBody2D
signal died
const KEY_SCENE := preload("res://scenes/prefabs/collectibles/key_gold.tscn")
const KEY_SCRIPT := preload("res://scripts/key.gd")
const KEY_ATLAS := preload("res://assets/game/tilesets/kenney_tiles_atlas.png")
const KEY_REGION := Rect2(126, 18, 18, 18)
@export_category("移动参数")
@export var move_speed: float = 75.0
@export var acceleration: float = 600.0
@export var deceleration: float = 800.0
@export var jump_velocity: float = -190.0
@export_category("生命值")
## 最大生命值(几颗心)
@export var max_health: int = 3
## 按 Z 用掉一个回血道具后,获得的无敌盾持续时间(秒)
@export var shield_time: float = 5.0
## 受伤后的无敌时间,避免站在尖刺上反复触发
@export var invulnerable_time: float = 1.0
## 受伤瞬间的染色
@export var hurt_tint: Color = Color(1.0, 0.35, 0.35)
@export var hurt_flash_time: float = 0.25
## 有护盾时的染色
@export var shield_tint: Color = Color(0.6, 0.95, 1.0, 1.0)
## 死亡后的染色
@export var dead_tint: Color = Color(0.85, 0.3, 0.3, 1.0)
@export_category("钥匙投掷")
## 投掷初速度大小
@export var throw_speed: float = 300.0
## 投掷仰角(度)
@export_range(0.0, 89.0, 1.0) var throw_angle_deg: float = 60.0
## 预测弧线颜色
@export var arc_color: Color = Color(1.0, 0.94, 0.45, 0.85)
@export var arc_width: float = 1.5
## 拿着钥匙时头顶显示一个小图标(不想显示就关掉)
@export var show_carried_key: bool = true
@export_category("测试用飞行")
## 开发期测试用:按 F 切换飞行,方便飞到各处查看碰撞效果。
## 只在编辑器 / 调试版里生效;导出的正式版里按键无效,不会影响平衡。
@export var debug_fly: bool = true
@export var fly_speed: float = 90.0
## 飞行时的角色染色,用来一眼看出飞行开着(不想染色就改成白色)
@export var fly_tint: Color = Color(0.65, 0.9, 1.0)
@onready var sprite: Sprite2D = $Sprite2D
@onready var collider: CollisionShape2D = $CollisionShape2D
var flying: bool = false
var facing: int = 1
var held_key: bool = false
var is_aiming: bool = false
var hurt_count: int = 0
## 当前生命值(几颗心)
var health: int = 3
## 身上的回血道具数量(捡到的小球)
var heal_items: int = 0
## 无敌盾剩余时间,给 HUD 显示用
var shield_left: float = 0.0
## 生命值归零后进入死亡状态:不能操作,交给死亡界面
var is_dead: bool = false
var _arc: Line2D = null
var _landing_marker: Polygon2D = null
var _carried_icon: Sprite2D = null
## 最近一次扔出去、还没被回收的钥匙
var _thrown_key: Node = null
## 角色原点到底部的距离,传送时用来对齐脚底
var _feet_offset: float = 10.5
## 出生点,受伤后回到这里
var _spawn_point := Vector2.ZERO
var _invulnerable: float = 0.0
var _hurt_flash: float = 0.0
func _ready() -> void:
add_to_group("player")
_build_throw_visuals()
_measure_feet_offset()
_spawn_point = global_position
health = max_health
func _physics_process(delta: float) -> void:
if _invulnerable > 0.0:
_invulnerable -= delta
if _hurt_flash > 0.0:
_hurt_flash -= delta
if shield_left > 0.0:
shield_left -= delta
if is_dead:
# 死亡后不接受任何操作,只让重力把人放下来
apply_gravity(delta)
velocity.x = move_toward(velocity.x, 0.0, deceleration * delta)
_update_tint()
move_and_slide()
return
# 正式版里不允许飞行;即使 toggle 开着也强制落回地面
if flying and not fly_available():
set_flying(false)
if flying:
handle_fly_movement()
else:
apply_gravity(delta)
handle_jump()
handle_horizontal_movement(delta)
update_sprite_direction()
_update_tint()
if is_aiming:
update_arc()
move_and_slide()
func _unhandled_input(event: InputEvent) -> void:
if is_dead:
return
if fly_available() and event.is_action_pressed("fly") and not event.is_echo():
set_flying(not flying)
return
if event.is_action_pressed("use_heal") and not event.is_echo():
use_heal_item()
return
if event.is_action_pressed("warp_to_key") and not event.is_echo():
warp_to_thrown_key()
return
if event.is_action_pressed("squat") and not event.is_echo():
if can_aim():
is_aiming = true
update_arc()
elif event.is_action_released("squat"):
if is_aiming:
release_throw()
# ---------------------------------------------------------------- 生命值
## 由尖刺之类的危险物调用
func hurt(_amount: int = 1) -> void:
if is_dead or _invulnerable > 0.0:
return
hurt_count += 1
health -= 1
_invulnerable = invulnerable_time
_hurt_flash = hurt_flash_time
if flying:
set_flying(false)
if is_aiming:
_cancel_aim()
# 扣血不回出生点,只在原地短暂无敌
if health <= 0:
health = 0
_die()
func _die() -> void:
is_dead = true
# 只有死亡复活才回到出生点
global_position = _spawn_point
velocity = Vector2.ZERO
if flying:
set_flying(false)
if is_aiming:
_cancel_aim()
died.emit()
## 由可收集物调用
func add_heal_item() -> void:
heal_items += 1
## 按 Z:消耗一个回血道具,回一颗心并获得无敌盾
func use_heal_item() -> bool:
if is_dead or heal_items <= 0:
return false
heal_items -= 1
health = mini(health + 1, max_health)
_invulnerable = maxf(_invulnerable, shield_time)
shield_left = shield_time
return true
func _update_tint() -> void:
if is_dead:
sprite.modulate = dead_tint
elif _hurt_flash > 0.0:
sprite.modulate = hurt_tint
elif shield_left > 0.0:
sprite.modulate = shield_tint
elif flying:
sprite.modulate = fly_tint
else:
sprite.modulate = Color.WHITE
# ---------------------------------------------------------------- 钥匙投掷
func can_aim() -> bool:
return held_key and not flying and not is_aiming
## 由钥匙对象在碰到玩家时调用
func collect_key() -> void:
held_key = true
_update_carried_icon()
func throw_velocity() -> Vector2:
var rad := deg_to_rad(throw_angle_deg)
return Vector2(facing * throw_speed * cos(rad), -throw_speed * sin(rad))
func update_arc() -> void:
if not is_aiming or _arc == null:
return
# 用玩家自己的 collision_mask 做预测,所以"弧线能落到的地方"就是"玩家能站的地方"
var result := KEY_SCRIPT.simulate_trajectory(
get_world_2d().direct_space_state,
global_position,
throw_velocity(),
collision_mask)
_arc.points = result["points"]
_arc.visible = _arc.points.size() > 1
var landed: bool = result["landed"]
if _landing_marker != null:
_landing_marker.visible = landed
if landed:
_landing_marker.global_position = result["landing"]
func release_throw() -> void:
is_aiming = false
_hide_throw_visuals()
if not held_key:
return
held_key = false
_update_carried_icon()
var key: Node = KEY_SCENE.instantiate()
# 这是玩家扔出去的钥匙,不要去擦关卡里的占位地砖
key.erase_placeholder_on_ready = false
# 让钥匙的落地判定和玩家完全一致
key.flight_mask = collision_mask
get_parent().add_child(key)
key.global_position = global_position
key.launch(throw_velocity())
_thrown_key = key
## 按 W:传送到扔出去的那把钥匙所在的位置,并顺手把它捡回来
func warp_to_thrown_key() -> void:
if _thrown_key == null or not is_instance_valid(_thrown_key):
return
var key_pos: Vector2 = _thrown_key.global_position
# 让玩家的脚底落在钥匙脚底所在的那个面上,避免直接嵌进地形
global_position = Vector2(key_pos.x, key_pos.y + KEY_SCRIPT.FOOT_OFFSET - _feet_offset)
velocity = Vector2.ZERO
if is_aiming:
_cancel_aim()
_thrown_key.queue_free()
_thrown_key = null
held_key = true
_update_carried_icon()
func _cancel_aim() -> void:
is_aiming = false
_hide_throw_visuals()
func _hide_throw_visuals() -> void:
if _arc != null:
_arc.visible = false
if _landing_marker != null:
_landing_marker.visible = false
func _update_carried_icon() -> void:
if _carried_icon != null:
_carried_icon.visible = held_key
func _measure_feet_offset() -> void:
if collider == null or collider.shape == null:
return
var shape := collider.shape
if shape is CapsuleShape2D:
_feet_offset = collider.position.y + shape.height * 0.5
elif shape is RectangleShape2D:
_feet_offset = collider.position.y + shape.size.y * 0.5
elif shape is CircleShape2D:
_feet_offset = collider.position.y + shape.radius
func _build_throw_visuals() -> void:
# top_level = true 让它们忽略角色自身那个 rotation/scale 翻转,直接用世界坐标
_arc = Line2D.new()
_arc.width = arc_width
_arc.default_color = arc_color
_arc.visible = false
add_child(_arc)
_arc.top_level = true
_landing_marker = Polygon2D.new()
var s := 4.0
_landing_marker.polygon = PackedVector2Array([
Vector2(0.0, -s), Vector2(s, 0.0), Vector2(0.0, s), Vector2(-s, 0.0)])
_landing_marker.color = arc_color
_landing_marker.visible = false
add_child(_landing_marker)
_landing_marker.top_level = true
if show_carried_key:
var tex := AtlasTexture.new()
tex.atlas = KEY_ATLAS
tex.region = KEY_REGION
_carried_icon = Sprite2D.new()
_carried_icon.texture = tex
_carried_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_carried_icon.position = Vector2(0.0, -15.0)
_carried_icon.visible = false
add_child(_carried_icon)
# ---------------------------------------------------------------- 测试用飞行
func fly_available() -> bool:
return debug_fly and OS.is_debug_build()
func set_flying(on: bool) -> void:
flying = on
velocity = Vector2.ZERO
if on:
_cancel_aim()
func handle_fly_movement() -> void:
# get_axis(负方向, 正方向),屏幕坐标里 y 向下为正,所以空格(跳跃)要放在负方向
var direction := Vector2(
Input.get_axis("move_left", "move_right"),
Input.get_axis("jump", "squat")
)
if direction == Vector2.ZERO:
velocity = Vector2.ZERO
else:
velocity = direction.normalized() * fly_speed
# ---------------------------------------------------------------- 正常移动
func apply_gravity(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
func handle_horizontal_movement(delta: float) -> void:
var direction := Input.get_axis("move_left", "move_right")
var target_speed := direction * move_speed
if direction != 0.0:
velocity.x = move_toward(
velocity.x,
target_speed,
acceleration * delta
)
else:
velocity.x = move_toward(
velocity.x,
0.0,
deceleration * delta
)
func handle_jump() -> void:
# 只有踩在地面上才能起跳 —— 空中不再给第二次机会,不做多段跳
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
func update_sprite_direction() -> void:
if velocity.x > 0.0:
facing = 1
sprite.flip_h = false
elif velocity.x < 0.0:
facing = -1
sprite.flip_h = true