200 lines
6.5 KiB
GDScript
200 lines
6.5 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_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 = $BodyMesh
|
|
@onready var indicator_node: MeshInstance3D = $KickIndicator
|
|
|
|
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.material_override.duplicate() as StandardMaterial3D
|
|
mesh_node.material_override = 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 _physics_process(delta: float) -> void:
|
|
if not is_alive:
|
|
return
|
|
_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 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 * move_speed
|
|
velocity.z = move.z * move_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, move_speed * 12.0 * delta)
|
|
velocity.z = move_toward(velocity.z, 0.0, move_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
|
|
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 enemies := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("rocks")
|
|
var kicked_any := false
|
|
for e in enemies:
|
|
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.1 or dist > kick_range:
|
|
continue
|
|
if (diff / dist).dot(forward) >= half_cos:
|
|
en.call("receive_kick", diff / dist, kick_force)
|
|
kicked_any = true
|
|
if kicked_any:
|
|
_squish_effect()
|
|
|
|
func set_aim_direction(yaw_rad: float) -> void:
|
|
_aim_yaw = yaw_rad
|
|
_is_aiming = true
|
|
|
|
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 take_damage(amount: int) -> void:
|
|
if not is_alive or invincible_timer > 0.0:
|
|
return
|
|
invincible_timer = IFRAMES_DURATION
|
|
#health = max(0, health - amount)
|
|
#emit_signal("health_changed", health, max_health)
|
|
#var tw := create_tween()
|
|
#tw.tween_property(player_mat, "albedo_color", Color.RED, 0.08)
|
|
#tw.tween_property(player_mat, "albedo_color", BASE_COLOR, 0.25)
|
|
#if health <= 0:
|
|
#_die()
|
|
|
|
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)
|