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 tier: int = 0 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 _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 _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 targets := get_tree().get_nodes_in_group("enemies") + get_tree().get_nodes_in_group("kickable") var kicked_any := false for e in targets: 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: var obj_tier: int = en.get("tier") if en.get("tier") != null else 0 var diff_tier := tier - obj_tier 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 en.call("receive_kick", diff / dist, force) kicked_any = true if kicked_any: _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 _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) func apply_upgrade_boots(speed_bonus: float, _tier: int) -> void: tier += _tier 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: 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: 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)