367 lines
11 KiB
GDScript
367 lines
11 KiB
GDScript
class_name Enemy
|
|
extends CharacterBody3D
|
|
|
|
const PICKUP_SCENE := preload("res://scenes/Pickup.tscn")
|
|
const LEATHER_SCENE := preload("res://scenes/Leather.tscn")
|
|
const IRON_SCENE := preload("res://scenes/Iron.tscn")
|
|
const ESSENCE_SCENE := preload("res://scenes/Essence.tscn")
|
|
|
|
signal died(points: int)
|
|
signal merged(upgrade: bool)
|
|
|
|
enum State { CHASING, FLYING, STUNNED, DEAD, MERGING }
|
|
|
|
static var first_leather_spawned: bool = false
|
|
static var first_iron_spawned: bool = false
|
|
static var first_essence_spawned: bool = false
|
|
|
|
var kickable_type: String = ""
|
|
var kick_tier: int = 1
|
|
var toughness_tier: int = 1
|
|
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 damage_modifier: float = 0.75
|
|
var enemy_kick_timer: float = 0.0
|
|
var kickable_kick_timer: float = 0.0
|
|
const ENEMY_KICK_COOLDOWN := 1.2
|
|
const KICKABLE_KICK_COOLDOWN := 2.5
|
|
const ENEMY_KICK_RANGE := 2.2
|
|
|
|
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
|
|
kickable_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
|
|
kick_tier = enemy_level
|
|
toughness_tier = enemy_level
|
|
|
|
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)
|
|
enemy_kick_timer = max(0.0, enemy_kick_timer - delta)
|
|
kickable_kick_timer = max(0.0, kickable_kick_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 enemy_kick_timer <= 0.0:
|
|
_try_enemy_kick()
|
|
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 _try_enemy_kick() -> void:
|
|
if not is_instance_valid(target):
|
|
return
|
|
var player_pos := target.global_position
|
|
var to_player := player_pos - global_position
|
|
to_player.y = 0.0
|
|
var kick_dir := to_player.normalized() if to_player.length() > 0.01 else -global_transform.basis.z
|
|
|
|
# 1. Kick nearest kickable towards player
|
|
if kickable_kick_timer <= 0.0:
|
|
var nearest_kickable: Node3D = null
|
|
var nearest_dist := ENEMY_KICK_RANGE
|
|
for node in get_tree().get_nodes_in_group("kickable"):
|
|
var k := node as Node3D
|
|
if k == null or not is_instance_valid(k):
|
|
continue
|
|
var d := (k.global_position - global_position)
|
|
d.y = 0.0
|
|
if d.length() < nearest_dist:
|
|
nearest_dist = d.length()
|
|
nearest_kickable = k
|
|
if nearest_kickable != null:
|
|
nearest_kickable.call("receive_kick", kick_dir, 35.0 + kick_tier * 8.0)
|
|
kickable_kick_timer = KICKABLE_KICK_COOLDOWN
|
|
return
|
|
|
|
# 2. Kick lower-toughness enemy nearest to player direction
|
|
if kick_tier > 0:
|
|
var nearest_enemy: Node3D = null
|
|
var nearest_enemy_dist := ENEMY_KICK_RANGE
|
|
for node in get_tree().get_nodes_in_group("enemies"):
|
|
var en := node as Node3D
|
|
if en == null or en == self or not is_instance_valid(en):
|
|
continue
|
|
if (en.get("toughness_tier") if en.get("toughness_tier") != null else 0) >= kick_tier:
|
|
continue
|
|
var d := (en.global_position - global_position)
|
|
d.y = 0.0
|
|
if d.length() < nearest_enemy_dist:
|
|
nearest_enemy_dist = d.length()
|
|
nearest_enemy = en
|
|
if nearest_enemy != null:
|
|
nearest_enemy.call("receive_kick", kick_dir, 40.0 + kick_tier * 10.0)
|
|
enemy_kick_timer = ENEMY_KICK_COOLDOWN
|
|
return
|
|
|
|
# 3. Kick player directly if lower toughness and in range
|
|
#if kick_tier > 0 and to_player.length() < ENEMY_KICK_RANGE:
|
|
if to_player.length() < ENEMY_KICK_RANGE:
|
|
var player_toughness: int = target.get("toughness_tier") if target.get("toughness_tier") != null else 0
|
|
if player_toughness < kick_tier:
|
|
target.call("receive_kick", kick_dir, 35.0 + kick_tier * 8.0)
|
|
enemy_kick_timer = ENEMY_KICK_COOLDOWN
|
|
|
|
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 merged := KickSystem.resolve(self, col3d, fly_vel)
|
|
if not merged and is_instance_valid(col3d):
|
|
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("kickable"):
|
|
KickSystem.resolve(self, col3d, fly_vel)
|
|
var rock_dir := col3d.global_position - global_position
|
|
rock_dir.y = 0.0
|
|
if rock_dir.length() > 0.01:
|
|
col3d.call("receive_kick", rock_dir.normalized(), speed_now * 0.5)
|
|
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 can_merge_with(other: Node3D, collision_speed: float) -> bool:
|
|
return (collision_speed >= 18.0
|
|
and other.get("enemy_type") == enemy_type
|
|
and other.get("enemy_level") == enemy_level
|
|
and not is_upgrading
|
|
and not other.get("is_upgrading"))
|
|
|
|
func do_merge_with(other: Node3D) -> void:
|
|
_start_merge(other)
|
|
|
|
func apply_collision_damage(dmg: float) -> void:
|
|
_take_hit(int(dmg))
|
|
|
|
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:
|
|
if enemy_level == 1:
|
|
var drop_leather := not first_leather_spawned or randf() < 0.20
|
|
if drop_leather:
|
|
first_leather_spawned = true
|
|
var leather := LEATHER_SCENE.instantiate() as Node3D
|
|
get_parent().add_child(leather)
|
|
leather.global_position = global_position
|
|
|
|
if enemy_level == 2:
|
|
var drop_iron := not first_iron_spawned or randf() < 0.20
|
|
if drop_iron:
|
|
first_iron_spawned = true
|
|
var iron := IRON_SCENE.instantiate() as Node3D
|
|
get_parent().add_child(iron)
|
|
iron.global_position = global_position
|
|
|
|
if enemy_level == 3:
|
|
var drop_essence := not first_essence_spawned or randf() < 0.20
|
|
if drop_essence:
|
|
first_essence_spawned = true
|
|
var essence := ESSENCE_SCENE.instantiate() as Node3D
|
|
get_parent().add_child(essence)
|
|
essence.global_position = global_position
|
|
|
|
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)
|