extends CharacterBody3D signal died(points: int) enum State { CHASING, FLYING, STUNNED, DEAD } var move_speed: float = 3.0 var health: int = 30 var damage_to_player: int = 8 var score_value: int = 10 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 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 @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: match type: "slime": move_speed = 2.8 + wave * 0.12 health = 28 + wave * 4 score_value = 10 damage_to_player = 8 "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 "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 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 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 var tw := create_tween() tw.tween_property(mesh_node, "scale:y", base_scale * 0.35, 0.06) tw.tween_property(mesh_node, "scale:y", base_scale, 0.18) func _enter_stun() -> void: state = State.STUNNED stun_timer = stun_time mat.albedo_color = COLOR_STUN var bs := base_scale var tw := create_tween() tw.tween_property(mesh_node, "scale", Vector3(bs * 1.6, bs * 0.25, bs * 1.6), 0.07) tw.tween_property(mesh_node, "scale", Vector3(bs, bs, bs), 0.22) 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 _die() -> void: if state == State.DEAD: return state = State.DEAD set_physics_process(false) emit_signal("died", score_value) 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)