Files
KickSurvivors/scripts/Enemy.gd
T
2026-04-22 17:24:11 +03:00

248 lines
6.9 KiB
GDScript

extends CharacterBody3D
const PICKUP_SCENE := preload("res://scenes/Pickup.tscn")
signal died(points: int)
signal merged(upgrade: bool)
enum State { CHASING, FLYING, STUNNED, DEAD, MERGING }
var move_speed: float = 3.0
var health: int = 30
var damage_to_player: int = 8
var score_value: int = 10
var enemy_level: int = 1
var enemy_type: String = "slime"
var wall_damage_mult: float = 1.8
var chain_factor: float = 0.65
var stun_time: float = 0.5
var base_scale: float = 1.0
var wave_num: int = 1
var state: State = State.CHASING
var fly_vel: Vector3 = Vector3.ZERO
var stun_timer: float = 0.0
var contact_timer: float = 0.0
var target: Node3D = null
var merge_partner: Node = null
var is_upgrading: bool = false
@onready var mesh_node: MeshInstance3D = $BodyMesh
var mat: StandardMaterial3D
var COLOR_CHASE = Color(1.0, 0.28, 0.18)
var COLOR_FLY = Color(1.0, 0.85, 0.1)
var COLOR_STUN = Color(0.55, 0.55, 0.65)
const CONTACT_CD = 0.7
const AIR_FRICTION = 0.86
func _ready() -> void:
add_to_group("enemies")
mat = mesh_node.material_override.duplicate() as StandardMaterial3D
mesh_node.material_override = mat
COLOR_CHASE = mat.albedo_color
func setup(type: String, wave: int) -> void:
enemy_type = type
wave_num = wave
match type:
"slime":
move_speed = 2.8 + wave * 0.12
health = 28 + wave * 4
score_value = 10
damage_to_player = 8
enemy_level = 1
"bat":
move_speed = 5.5 + wave * 0.15
health = 14 + wave * 2
score_value = 15
damage_to_player = 6
base_scale = 0.7
mesh_node.scale = Vector3(0.7, 0.7, 0.7)
COLOR_CHASE = Color(0.6, 0.2, 0.8)
mat.albedo_color = COLOR_CHASE
enemy_level = 2
"ogre":
move_speed = 1.8 + wave * 0.08
health = 80 + wave * 12
score_value = 25
damage_to_player = 18
base_scale = 1.5
mesh_node.scale = Vector3(1.5, 1.5, 1.5)
COLOR_CHASE = Color(0.3, 0.7, 0.3)
mat.albedo_color = COLOR_CHASE
enemy_level = 3
func _physics_process(delta: float) -> void:
match state:
State.CHASING: _chase(delta)
State.FLYING: _fly(delta)
State.STUNNED: _stun_tick(delta)
State.DEAD: pass
func _chase(delta: float) -> void:
if not is_instance_valid(target):
return
contact_timer = max(0.0, contact_timer - delta)
var diff := target.global_position - global_position
diff.y = 0.0
var dist := diff.length()
if dist < 1.0 and contact_timer <= 0.0:
contact_timer = CONTACT_CD
if target.has_method("take_damage"):
target.take_damage(damage_to_player)
if dist > 0.05:
var dir := diff.normalized()
velocity.x = dir.x * move_speed
velocity.z = dir.z * move_speed
rotation.y = lerp_angle(rotation.y, atan2(dir.x, dir.z), 8.0 * delta)
velocity.y = 0.0
move_and_slide()
func _fly(delta: float) -> void:
var speed_now := Vector2(fly_vel.x, fly_vel.z).length()
velocity = fly_vel
velocity.y = 0.0
move_and_slide()
var hit_wall := false
for i in get_slide_collision_count():
var col := get_slide_collision(i)
var col3d := col.get_collider() as Node3D
if col3d == null:
continue
if col3d.has_meta("is_wall"):
_take_hit(int(speed_now * wall_damage_mult))
_wall_impact_effect()
fly_vel = Vector3.ZERO
velocity = Vector3.ZERO
_enter_stun()
hit_wall = true
break
elif col3d.is_in_group("enemies") and col3d != self:
var other: Node = col3d
if speed_now >= 3.0 and other.get("enemy_level") == enemy_level and other.get("is_upgrading") == false and is_upgrading == false:
_start_merge(other)
else:
var chain_dir := col3d.global_position - global_position
chain_dir.y = 0.0
if chain_dir.length() > 0.01:
col3d.call("receive_kick", chain_dir.normalized(), speed_now * chain_factor)
elif col3d.is_in_group("player"):
col3d.call("take_damage", int(speed_now * 0.6))
if not hit_wall:
fly_vel = velocity
fly_vel.y = 0.0
fly_vel *= pow(AIR_FRICTION, delta * 60.0)
if Vector2(fly_vel.x, fly_vel.z).length() < 0.4:
_enter_chase()
mesh_node.rotation.y += delta * 10.0
func _stun_tick(delta: float) -> void:
velocity = Vector3.ZERO
stun_timer -= delta
if stun_timer <= 0.0:
_enter_chase()
func receive_kick(direction: Vector3, force: float) -> void:
if state == State.DEAD:
return
fly_vel = direction * force
fly_vel.y = 0.0
state = State.FLYING
mat.albedo_color = COLOR_FLY
func _start_merge(other: Node) -> void:
is_upgrading = true
other.is_upgrading = true
other.merge_partner = self
merge_partner = other
state = State.MERGING
other.state = State.MERGING
fly_vel = Vector3.ZERO
velocity = Vector3.ZERO
other.fly_vel = Vector3.ZERO
other.velocity = Vector3.ZERO
mat.albedo_color = Color(1.0, 1.0, 0.5)
other.mat.albedo_color = Color(1.0, 1.0, 0.5)
var tw := create_tween().set_parallel(true)
tw.tween_property(self, "global_position", other.global_position, 0.2)
tw.tween_property(other, "global_position", global_position, 0.2)
tw.tween_callback(_on_merge_complete)
func _on_merge_complete() -> void:
var merge_pos := global_position
var merge_type := enemy_type
var new_level: int = enemy_level + 1
var new_wave: int = wave_num
if is_instance_valid(merge_partner):
merge_pos = (global_position + merge_partner.global_position) / 2.0
merge_partner.queue_free()
queue_free()
emit_signal("merged", true)
var tree := get_tree()
if tree != null and tree.has_group("main"):
var main := tree.get_nodes_in_group("main")[0] as Node
if main != null and main.has_method("_spawn_upgraded_enemy"):
var new_enemy := main.call("_spawn_upgraded_enemy", merge_pos, merge_type, new_level, new_wave) as CharacterBody3D
if new_enemy != null:
new_enemy.state = State.FLYING
new_enemy.fly_vel = Vector3.ZERO
func _enter_stun() -> void:
state = State.STUNNED
stun_timer = stun_time
mat.albedo_color = COLOR_STUN
func _enter_chase() -> void:
state = State.CHASING
mat.albedo_color = COLOR_CHASE
func _take_hit(dmg: int) -> void:
if state == State.DEAD:
return
health -= dmg
if health <= 0:
_die()
func _wall_impact_effect() -> void:
var tw := create_tween()
tw.tween_property(mat, "albedo_color", Color.WHITE, 0.04)
tw.tween_property(mat, "albedo_color", COLOR_STUN, 0.12)
func _try_drop_pickup() -> void:
var roll := randf()
var p_type := ""
var p_heal := 0
match enemy_type:
"slime":
if roll < 0.30: p_type = "health"; p_heal = 15
elif roll < 0.40: p_type = "score"
"bat":
if roll < 0.20: p_type = "health"; p_heal = 12
elif roll < 0.25: p_type = "score"
"ogre":
if roll < 0.55: p_type = "health"; p_heal = 35
elif roll < 0.80: p_type = "score"
if p_type == "":
return
var pickup := PICKUP_SCENE.instantiate()
pickup.setup(p_type, p_heal)
get_parent().add_child(pickup)
pickup.global_position = global_position
func _die() -> void:
if state == State.DEAD:
return
state = State.DEAD
set_physics_process(false)
emit_signal("died", score_value)
_try_drop_pickup()
var tw := create_tween()
tw.tween_property(self, "scale", Vector3(2.0, 0.05, 2.0), 0.18)
tw.tween_property(self, "scale", Vector3(0.0, 0.0, 0.0), 0.1)
tw.tween_callback(queue_free)