extends CharacterBody3D signal attacked_player(damage: float) @export var max_health: float = 20.0 @export var damage: float = 5.0 @export var detection_radius: float = 6.0 @export var attack_radius: float = 1.5 @export var move_speed: float = 1.0 enum State { WANDER, CHASE, ATTACK } var health: float var _state: State = State.WANDER var _time: float = 0.0 var _wander_target: Vector3 = Vector3.ZERO var _wander_timer: float = 0.0 var _attack_cooldown: float = 0.0 var _player: CharacterBody3D = null var _base_scale: Vector3 = Vector3.ONE # Visual nodes var _dome: MeshInstance3D var _tentacles: Array[MeshInstance3D] = [] func _ready() -> void: health = max_health _build_visuals() _find_player() _base_scale = scale _pick_wander_target() func _find_player() -> void: var players := get_tree().get_nodes_in_group("player") if players.size() > 0: _player = players[0] as CharacterBody3D func _build_visuals() -> void: var mat := StandardMaterial3D.new() mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.albedo_color = Color(0.9, 0.6, 0.9, 0.5) mat.emission_enabled = true mat.emission = Color(0.5, 0.1, 0.7) mat.emission_energy_multiplier = 0.8 mat.cull_mode = BaseMaterial3D.CULL_DISABLED # Dome (sphere scaled to look like a bell) _dome = MeshInstance3D.new() var sphere := SphereMesh.new() sphere.radius = 0.5 sphere.height = 0.7 _dome.mesh = sphere _dome.material_override = mat _dome.scale = Vector3(1.0, 0.7, 1.0) add_child(_dome) # 4 tentacles var tent_mat := StandardMaterial3D.new() tent_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA tent_mat.albedo_color = Color(0.85, 0.5, 0.85, 0.35) tent_mat.emission_enabled = true tent_mat.emission = Color(0.4, 0.0, 0.6) tent_mat.emission_energy_multiplier = 0.5 tent_mat.cull_mode = BaseMaterial3D.CULL_DISABLED var offsets: Array[Vector2] = [ Vector2(0.2, 0.0), Vector2(-0.2, 0.0), Vector2(0.0, 0.2), Vector2(0.0, -0.2) ] for off: Vector2 in offsets: var tent := MeshInstance3D.new() var cap := CapsuleMesh.new() cap.radius = 0.04 cap.height = 0.6 tent.mesh = cap tent.material_override = tent_mat tent.position = Vector3(off.x, -0.55, off.y) add_child(tent) _tentacles.append(tent) # Collision shape var col := CollisionShape3D.new() var cshape := SphereShape3D.new() cshape.radius = 0.55 col.shape = cshape add_child(col) func _process(delta: float) -> void: _time += delta # Pulse animation var pulse: float = 1.0 + sin(_time * 3.0) * 0.08 _dome.scale.y = 0.7 * pulse func _physics_process(delta: float) -> void: if _player == null: _find_player() _attack_cooldown = max(0.0, _attack_cooldown - delta) _wander_timer = max(0.0, _wander_timer - delta) var player_dist: float = INF if is_instance_valid(_player): player_dist = global_position.distance_to(_player.global_position) match _state: State.WANDER: _do_wander(delta) if player_dist < detection_radius: _state = State.CHASE State.CHASE: if is_instance_valid(_player): var dir: Vector3 = (_player.global_position - global_position).normalized() velocity = dir * move_speed if player_dist < attack_radius: _state = State.ATTACK if player_dist > detection_radius * 1.5: _state = State.WANDER State.ATTACK: if is_instance_valid(_player) and _attack_cooldown <= 0.0: attacked_player.emit(damage) _attack_cooldown = 1.5 if player_dist > attack_radius * 1.5: _state = State.CHASE # Smooth rotation toward movement if velocity.length_squared() > 0.01: var target_basis: Basis = Basis.looking_at(velocity.normalized(), Vector3.UP) transform.basis = transform.basis.slerp(target_basis, delta * 3.0) move_and_slide() func _do_wander(delta: float) -> void: if _wander_timer <= 0.0: _pick_wander_target() var dir: Vector3 = (_wander_target - global_position) if dir.length() < 0.5: _pick_wander_target() else: velocity = dir.normalized() * move_speed * 0.5 func _pick_wander_target() -> void: _wander_target = global_position + Vector3( randf_range(-5.0, 5.0), randf_range(-2.0, 2.0), randf_range(-5.0, 5.0) ) _wander_timer = randf_range(3.0, 6.0) func take_damage(dmg: float) -> void: health -= dmg if health <= 0.0: queue_free()