4a6f4862de
Space is bound to jump again, exactly as before: same action name, same physical_keycode 32, same jump_velocity -190 and gravity 980, so the measured jump height is 20.07 px, identical to the reading taken before the binding was removed. Single jump only, no double jump. The debug flight goes back to Space for up and S for down like it used to, with the arrow keys kept as an extra. No gameplay content touched.
424 lines
11 KiB
GDScript
424 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
|
|
## 已收集的钻石数量
|
|
var diamonds: int = 0
|
|
## 关卡里钻石总数(开场自动数一遍)
|
|
var diamond_total: 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
|
|
# 等一帧再数钻石,确保关卡里所有钻石节点都已经 _ready
|
|
_count_diamond_total.call_deferred()
|
|
|
|
|
|
func _count_diamond_total() -> void:
|
|
diamond_total = get_tree().get_nodes_in_group("diamond").size()
|
|
|
|
|
|
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
|
|
|
|
|
|
## 由钻石调用
|
|
func add_diamond() -> void:
|
|
diamonds += 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
|
|
|
|
|
|
func set_flying(on: bool) -> void:
|
|
flying = on
|
|
velocity = Vector2.ZERO
|
|
if on:
|
|
_cancel_aim()
|
|
|
|
|
|
func handle_fly_movement() -> void:
|
|
# 和以前一样:空格(跳跃键)上升、S 下降。方向键上下也顺便能用
|
|
var vertical := Input.get_axis("jump", "squat")
|
|
if is_zero_approx(vertical):
|
|
vertical = Input.get_axis("ui_up", "ui_down")
|
|
var direction := Vector2(
|
|
Input.get_axis("move_left", "move_right"),
|
|
vertical
|
|
)
|
|
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
|