Files
KickSurvivors/scripts/Player.gd
T
2026-04-23 14:57:14 +03:00

337 lines
11 KiB
GDScript

extends CharacterBody3D
signal died
signal health_changed(current: int, maximum: int)
@export var move_speed: float = 7.0
@export var kick_range: float = 3.5
@export var kick_force: float = 60.0
@export var kick_cooldown: float = 0.6
@export var kick_angle: float = 60.0
@export var max_health: int = 100
var health: int = max_health
var kick_tier: int = 0
var toughness_tier: int = 0
var has_stick_armor: bool = false
var has_leather_armor: bool = false
var has_plate_armor: bool = false
var has_wooden_shield: bool = false
var has_iron_shield: bool = false
var shield_tier: int = 0
var is_shielding: bool = false
var kick_timer: float = 0.0
var invincible_timer: float = 0.0
var is_alive: bool = true
var last_move_dir: Vector3 = Vector3.FORWARD
var _aim_yaw: float = 0.0
var _is_aiming: bool = false
@onready var mesh_node: MeshInstance3D = $player_man
@onready var indicator_node: MeshInstance3D = $KickIndicator
@onready var anim_player: AnimationPlayer = $AnimationPlayer2
var player_mat: StandardMaterial3D
var indicator_mat: StandardMaterial3D
const IFRAMES_DURATION := 0.6
const BASE_COLOR := Color(0.2, 0.55, 1.0)
func _ready() -> void:
add_to_group("player")
player_mat = mesh_node.get_surface_override_material(0).duplicate() as StandardMaterial3D
mesh_node.set_surface_override_material(0, player_mat)
_setup_indicator()
func _setup_indicator() -> void:
indicator_node.position.y = 0.02
indicator_node.mesh = _make_kick_arc_mesh()
indicator_mat = StandardMaterial3D.new()
indicator_mat.albedo_color = Color(1.0, 0.85, 0.1, 0.2)
indicator_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
indicator_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
indicator_mat.no_depth_test = true
indicator_mat.cull_mode = BaseMaterial3D.CULL_DISABLED
indicator_node.material_override = indicator_mat
func _make_kick_arc_mesh() -> ArrayMesh:
var verts := PackedVector3Array()
var half_rad: float = deg_to_rad(kick_angle * 0.5)
const SEGS := 24
verts.append(Vector3(0.0, 0.0, 0.0))
for i in range(SEGS + 1):
var t: float = float(i) / float(SEGS)
var a: float = lerpf(-half_rad, half_rad, t)
verts.append(Vector3(sin(a) * kick_range, 0.0, -cos(a) * kick_range))
var indices := PackedInt32Array()
for i in range(SEGS):
indices.append(0)
indices.append(i + 1)
indices.append(i + 2)
var arrays: Array = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = verts
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_E:
_try_interact()
func _physics_process(delta: float) -> void:
if not is_alive:
return
is_shielding = shield_tier > 0 and Input.is_key_pressed(KEY_SHIFT)
_handle_movement(delta)
_handle_kick(delta)
_handle_iframes(delta)
if Input.is_action_just_pressed("ui_accept") and kick_timer <= 0.0:
_do_kick()
func _handle_movement(delta: float) -> void:
var effective_speed := move_speed * (0.2 if is_shielding else 1.0)
var input_x: float = (
float(Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT)) -
float(Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT))
)
var input_z: float = (
float(Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN)) -
float(Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP))
)
var cam := get_viewport().get_camera_3d()
if (abs(input_x) > 0.0 or abs(input_z) > 0.0) and cam != null:
var cam_fwd := -cam.global_transform.basis.z
cam_fwd.y = 0.0
cam_fwd = cam_fwd.normalized() if cam_fwd.length() > 0.01 else Vector3(0.0, 0.0, -1.0)
var cam_right := cam.global_transform.basis.x
cam_right.y = 0.0
cam_right = cam_right.normalized() if cam_right.length() > 0.01 else Vector3(1.0, 0.0, 0.0)
var move := cam_fwd * (-input_z) + cam_right * input_x
if move.length() > 0.01:
move = move.normalized()
velocity.x = move.x * effective_speed
velocity.z = move.z * effective_speed
last_move_dir = move
if not _is_aiming:
var target_y: float = atan2(-move.x, -move.z)
rotation.y = lerp_angle(rotation.y, target_y, 16.0 * delta)
else:
velocity.x = move_toward(velocity.x, 0.0, effective_speed * 12.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, effective_speed * 12.0 * delta)
if _is_aiming:
rotation.y = lerp_angle(rotation.y, _aim_yaw, 14.0 * delta)
_is_aiming = false
velocity.y = 0.0
move_and_slide()
func _handle_kick(delta: float) -> void:
kick_timer = max(0.0, kick_timer - delta)
var t: float = clamp(1.0 - kick_timer / kick_cooldown, 0.0, 1.0)
indicator_mat.albedo_color.a = 0.05 + t * 0.3
func _handle_iframes(delta: float) -> void:
if invincible_timer > 0.0:
invincible_timer -= delta
if invincible_timer > 0.0:
mesh_node.visible = fmod(invincible_timer * 10.0, 1.0) > 0.5
else:
mesh_node.visible = true
func _do_kick() -> void:
kick_timer = kick_cooldown
_play_kick_blend()
var forward := -global_transform.basis.z
forward.y = 0.0
forward = forward.normalized() if forward.length() > 0.01 else Vector3(0.0, 0.0, -1.0)
var half_cos: float = cos(deg_to_rad(kick_angle * 0.5))
var candidates: Array[Node3D] = []
var candidate_dists: Array[float] = []
for e in get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("kickable"):
if not is_instance_valid(e):
continue
var en := e as Node3D
if en == null:
continue
var diff := en.global_position - global_position
diff.y = 0.0
var dist := diff.length()
if dist < 0.01 or dist > kick_range:
continue
if dist < 1.0 or (diff / dist).dot(forward) >= half_cos:
candidates.append(en)
candidate_dists.append(dist)
if candidates.is_empty():
return
var nearest_idx := 0
for i in range(1, candidates.size()):
if candidate_dists[i] < candidate_dists[nearest_idx]:
nearest_idx = i
var best := candidates[nearest_idx]
var best_diff := best.global_position - global_position
best_diff.y = 0.0
var best_dir := best_diff.normalized()
var obj_toughness: int = best.get("toughness_tier") if best.get("toughness_tier") != null else 0
var diff_tier := kick_tier - obj_toughness
var force: float
if diff_tier < 0:
force = 15.0
elif diff_tier == 0:
force = 50.0
elif diff_tier == 1:
force = 70.0
else:
force = 80.0
best.call("receive_kick", best_dir, force)
FX.hit_spark(best.global_position + Vector3(0, 0.4, 0), get_parent())
_squish_effect()
func _try_interact() -> void:
var best: Node3D = null
var best_dist := 2.5
for node in get_tree().get_nodes_in_group("interactable"):
var n := node as Node3D
if n == null:
continue
var d := global_position.distance_to(n.global_position)
if d < best_dist:
best_dist = d
best = n
if best != null:
best.call("interact", self)
func set_aim_direction(yaw_rad: float) -> void:
_aim_yaw = yaw_rad
_is_aiming = true
func _play_kick_blend() -> void:
var tw := create_tween()
tw.tween_method(func(v: float): mesh_node.set_blend_shape_value(0, v), 0.0, 1.0, 0.12)
tw.tween_method(func(v: float): mesh_node.set_blend_shape_value(0, v), 1.0, 0.0, 0.38)
func _squish_effect() -> void:
var tw := create_tween()
tw.tween_property(mesh_node, "scale", Vector3(1.3, 0.55, 1.3), 0.07)
tw.tween_property(mesh_node, "scale", Vector3(1.0, 1.0, 1.0), 0.18)
func receive_kick(direction: Vector3, force: float) -> void:
if not is_alive or invincible_timer > 0.0:
return
velocity.x = direction.x * force
velocity.z = direction.z * force
invincible_timer = IFRAMES_DURATION * 0.5
_squish_effect()
func take_damage(amount: int, attacker_toughness: int = 0) -> void:
if not is_alive or invincible_timer > 0.0:
return
invincible_timer = IFRAMES_DURATION
if is_shielding and shield_tier > 0:
var diff := shield_tier - attacker_toughness
var mod: float = 0.15 if diff >= 2 else (0.30 if diff == 1 else 0.50)
amount = int(amount * mod)
_squish_effect()
func heal(amount: int) -> void:
if not is_alive:
return
health = min(health + amount, max_health)
emit_signal("health_changed", health, max_health)
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.1, 1.0, 0.35), 0.08)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.3)
func _die() -> void:
is_alive = false
emit_signal("died")
var tw := create_tween()
tw.tween_property(self, "scale", Vector3(2.0, 0.0, 2.0), 0.35)
func apply_upgrade(id: String) -> void:
match id:
"kick_force": kick_force += 6.0
"kick_range":
kick_range += 0.7
indicator_node.mesh = _make_kick_arc_mesh()
"kick_cooldown": kick_cooldown = max(0.12, kick_cooldown - 0.09)
"move_speed": move_speed += 1.2
"max_health":
max_health += 30
health = min(health + 30, max_health)
emit_signal("health_changed", health, max_health)
func apply_upgrade_boots(speed_bonus: float, _tier: int) -> void:
kick_tier += 1
move_speed += speed_bonus
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(1.0, 0.85, 0.2), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.4)
func apply_upgrade_armor() -> void:
kick_tier += 1
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.7, 0.8, 1.0), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.5)
func apply_upgrade_enchant() -> void:
kick_tier += 1
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.8, 0.2, 1.0), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.6)
func apply_stick_armor() -> bool:
if has_stick_armor:
return false
has_stick_armor = true
toughness_tier += 1
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.7, 0.5, 0.2), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.4)
return true
func apply_leather_armor() -> bool:
if has_leather_armor:
return false
has_leather_armor = true
toughness_tier += 1
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.8, 0.5, 0.2), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.4)
return true
func apply_plate_armor() -> bool:
if has_plate_armor:
return false
has_plate_armor = true
toughness_tier += 1
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.6, 0.7, 1.0), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.5)
return true
func apply_wooden_shield() -> bool:
if has_wooden_shield:
return false
has_wooden_shield = true
shield_tier = max(shield_tier, 1)
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.55, 0.38, 0.18), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.4)
return true
func apply_iron_shield() -> bool:
if has_iron_shield:
return false
has_iron_shield = true
shield_tier = max(shield_tier, 2)
var tw := create_tween()
tw.tween_property(player_mat, "albedo_color", Color(0.55, 0.58, 0.62), 0.1)
tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.5)
return true