extends CharacterBody2D @export var patrol_points: PackedVector2Array = PackedVector2Array() @export var move_speed: float = 60.0 @export var wait_time: float = 1.0 @export var detection_fill_rate: float = 1.7 @export var detection_decay_rate: float = 0.5 @onready var animation_player: AnimationPlayer = $Cat/AnimationPlayer @onready var sprite: Sprite2D = $Cat/Sprite2D @onready var vision_cone: Node2D = $VisionCone2D @onready var vision_area: Area2D = $VisionCone2D/VisionConeArea @onready var vision_renderer: Polygon2D = $VisionCone2D/VisionConeRenderer const NORMAL_COLOR := Color(0.95, 0.78, 0.22, 0.45) const ALERT_COLOR := Color(0.9, 0.12, 0.12, 0.55) var _points: Array[Vector2] = [] var _target_index: int = 0 var _wait_timer: float = 0.0 var _facing := Vector2.DOWN var _detection: float = 0.0 var _sees_ghost: bool = false var _ghost_in_cone: Ghost = null func _ready() -> void: animation_player.play("idle_sit") if patrol_points.is_empty(): _points = [global_position] else: for p in patrol_points: _points.append(global_position + p) vision_area.body_entered.connect(_on_vision_body_entered) vision_area.body_exited.connect(_on_vision_body_exited) _update_cone_rotation() func _physics_process(delta: float) -> void: if GameManager.game_over: velocity = Vector2.ZERO return _patrol(delta) _update_detection(delta) func _patrol(delta: float) -> void: if _points.size() <= 1: velocity = Vector2.ZERO return var target: Vector2 = _points[_target_index] var to_target := target - global_position if to_target.length() < 4.0: velocity = Vector2.ZERO if animation_player.current_animation != "idle_sit": animation_player.play("idle_sit") _wait_timer += delta if _wait_timer >= wait_time: _wait_timer = 0.0 _target_index = (_target_index + 1) % _points.size() else: var dir := to_target.normalized() velocity = dir * move_speed if animation_player.current_animation != "walk": animation_player.play("walk") _set_facing(dir) move_and_slide() func _set_facing(dir: Vector2) -> void: if dir.length() < 0.01: return _facing = dir if absf(dir.x) > 0.05: sprite.flip_h = dir.x > 0 _update_cone_rotation() func _update_cone_rotation() -> void: vision_cone.global_rotation = _facing.angle() - PI / 2.0 func _update_detection(delta: float) -> void: if _ghost_in_cone != null and _ghost_in_cone.held_item != null: _detection += detection_fill_rate * delta else: _detection -= detection_decay_rate * delta _detection = clampf(_detection, 0.0, 1.0) vision_renderer.color = NORMAL_COLOR.lerp(ALERT_COLOR, _detection) GameManager.report_npc_alert(get_instance_id(), _detection) if _detection >= 1.0: GameManager.report_caught() func _on_vision_body_entered(body: Node2D) -> void: if body is Ghost: _ghost_in_cone = body func _on_vision_body_exited(body: Node2D) -> void: if body is Ghost: _ghost_in_cone = null