Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot Genre Stealth

  • 131 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

Use godot-genre-stealth for development tasks

About

godot-genre-stealth: A skill for development. This provides functionality for development workflows.

  • godot-genre-stealth

Godot Genre Stealth by the numbers

  • 131 all-time installs (skills.sh)
  • +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
  • Ranked #2,707 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-stealth

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs131
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

What it does

Use godot-genre-stealth for development tasks

Files

SKILL.mdMarkdownGitHub ↗

Genre: Stealth

Player choice, systemic AI, and clear communication define stealth games.

NEVER Do (Expert Anti-Patterns)

Detection & Awareness

  • NEVER use binary "Seen/Not Seen" detection; strictly use a Gradual Detection Meter (0-100%) that builds based on distance, light level, and speed.
  • NEVER use standard RayCast3D nodes for massive amounts of vision checks; strictly use `PhysicsDirectSpaceState3D.intersect_ray()` to query the PhysicsServer instantly and nodelessly.
  • NEVER allow AI to see through solid geometry; strictly use raycasts between AI eyes and player sample points (Head/Torso/Feet).
  • NEVER use a single sample point for visibility; strictly sample at least 3 points (Head, Torso, Feet) to prevent detection bugs when partially in cover.
  • NEVER use static "Guard Paths"; strictly implement Dynamic Investigating where guards leave their route to check on suspicious sounds/activities.
  • NEVER trigger "Detection" immediately upon line-of-sight; strictly use a Detection Meter with a decay rate to provide a "forgiveness window" for the player to recover.
  • NEVER assume a random navmesh point is safe; strictly verify cover points by Raycasting toward the Threat to ensure geometry successfully breaks the line of sight.
  • NEVER forget to pass the guard's own RID into the raycast exclude array; if omitted, the ray will hit the guard's own body, causing false blocking.
  • NEVER run complex AI detection for off-screen guards; strictly use VisibleOnScreenNotifier3D to pause heavy logic for distant enemies.

Systemic & World Logic

  • NEVER use a simple distance_to() check for hearing; strictly calculate sound travel along the Navigation Path to determine if a wall blocks noise.
  • NEVER make combat as viable as stealth; strictly ensure "going loud" triggers intense reinforcements or high-lethality states to preserve the stealth loop.
  • NEVER hide the "Why" of detection; strictly provide immediate feedback via UI icons (?, !) or audio barks ("What was that?").
  • NEVER ignore the return value of intersect_ray(); strictly check is_empty() first to prevent runtime crashes.
  • NEVER assume a raycast won't hit the guard itself; strictly exclude the guard's RID from Query Parameters.

Optimization & Performance

  • NEVER tightly couple AI to player scripts; strictly use duck-typing (e.g., if body.has_method("get_detected")) so guards can spot decoys or dead bodies without brittle dependencies.
  • NEVER maintain hardcoded arrays to trigger base-wide alarms; strictly add guards to a "guards" group and use get_tree().call_group() for dynamic notification.
  • NEVER use standard Strings for AI state; strictly use StringName (&"alert") for O(1) pointer-level comparisons in high-frequency loops.
  • NEVER bake massive NavigationMeshes synchronously; strictly use use_async_iterations to prevent main thread stalls during runtime bakes.
  • NEVER rely on Node.find_child() during gameplay; strictly use Groups or exported references for O(1) player tracking.
  • NEVER leave CollisionShapes enabled on incapacitated bodies; strictly disable them or move them to a "corpse" layer to prevent pathing interference.

---

🛠 Expert Components (scripts/)

Original Expert Patterns

  • stealth_ai_controller.gd - Professional-grade NPC controller with composite vision, sound paths, and alert state logic.

Modular Components

  • stealth_patterns.gd - Collection of patterns for PhysicsServer raycasting, noise bus routing, and avoidance masking.

---

Design Principles

From industry experts (Splinter Cell, Dishonored, Hitman developers):

1. Player Choice: Multiple valid approaches to every scenario 2. Systemic Design: Rules-based AI that players can learn and exploit 3. Clear Communication: Player always understands game state and threats 4. Fair Detection: No "gotcha" moments - threats visible before dangerous

---

AI Detection System

Vision Cone Implementation

Based on Splinter Cell Blacklist GDC talk - realistic vision uses composite shapes:

class_name EnemyVision
extends Node3D

@export var forward_vision_range := 20.0    # Main vision cone
@export var peripheral_range := 10.0        # Side vision
@export var forward_fov := 60.0             # Degrees
@export var peripheral_fov := 120.0          # Degrees
@export var detection_speed := 1.0          # How fast detection builds

var detection_level := 0.0  # 0-100
var target: Node3D = null

func _physics_process(delta: float) -> void:
    var player := get_player_if_visible()
    if player:
        # Detection rate varies by:
        # - Distance (closer = faster)
        # - Lighting on player
        # - Player movement (moving = more visible)
        # - In peripheral vs direct vision
        var rate := calculate_detection_rate(player)
        detection_level = min(100, detection_level + rate * delta)
    else:
        detection_level = max(0, detection_level - detection_speed * 0.5 * delta)

func get_player_if_visible() -> Player:
    var player := get_tree().get_first_node_in_group("player")
    if not player:
        return null
    
    var to_player := player.global_position - global_position
    var distance := to_player.length()
    var angle := rad_to_deg(global_basis.z.angle_to(-to_player.normalized()))
    
    # Check forward cone
    if angle < forward_fov / 2.0 and distance < forward_vision_range:
        if has_line_of_sight(player):
            return player
    
    # Check peripheral (less effective)
    elif angle < peripheral_fov / 2.0 and distance < peripheral_range:
        if has_line_of_sight(player):
            return player
    
    return null

func calculate_detection_rate(player: Player) -> float:
    var distance := global_position.distance_to(player.global_position)
    var distance_factor := 1.0 - (distance / forward_vision_range)
    
    var light_factor := player.get_light_level()  # 0.0 = dark, 1.0 = lit
    var movement_factor := 1.0 if player.velocity.length() > 0.5 else 0.3
    
    return detection_speed * distance_factor * light_factor * movement_factor * 50.0

Sound Detection System

Based on Thief/Hitman implementation - sounds propagate along navigation paths:

class_name SoundPropagation
extends Node

# Sound travels through connected navigation points, not through walls
func propagate_sound(origin: Vector3, loudness: float, sound_type: String) -> void:
    for enemy in get_tree().get_nodes_in_group("enemies"):
        var path := NavigationServer3D.map_get_path(
            get_world_3d().navigation_map,
            origin,
            enemy.global_position,
            true
        )
        
        if path.is_empty():
            continue  # No path = sound blocked
        
        var path_distance := calculate_path_length(path)
        var heard_loudness := loudness - (path_distance * 0.5)  # Falloff
        
        if heard_loudness > enemy.hearing_threshold:
            enemy.hear_sound(origin, sound_type, heard_loudness)

func calculate_path_length(path: PackedVector3Array) -> float:
    var length := 0.0
    for i in range(1, path.size()):
        length += path[i].distance_to(path[i - 1])
    return length

Player Light Level

class_name LightDetector
extends Node3D

@export var sample_points: Array[Marker3D]  # Multiple points on player body

func get_light_level() -> float:
    var total := 0.0
    var space := get_world_3d().direct_space_state
    
    for point in sample_points:
        for light in get_tree().get_nodes_in_group("lights"):
            var dir := light.global_position - point.global_position
            var query := PhysicsRayQueryParameters3D.create(
                point.global_position,
                light.global_position
            )
            var result := space.intersect_ray(query)
            
            if result.is_empty():  # Not blocked
                total += light.light_energy / dir.length_squared()
    
    return clamp(total / sample_points.size(), 0.0, 1.0)

---

AI Alert States

Three-phase system (industry standard):

enum AlertState { IDLE, SUSPICIOUS, ALERTED, COMBAT }

class_name EnemyAI
extends CharacterBody3D

var alert_state := AlertState.IDLE
var suspicion_point: Vector3
var search_timer := 0.0

signal alert_state_changed(new_state: AlertState)

func transition_to(new_state: AlertState) -> void:
    alert_state = new_state
    alert_state_changed.emit(new_state)
    
    match new_state:
        AlertState.SUSPICIOUS:
            play_animation("suspicious")
            speak_dialogue("what_was_that")
        AlertState.ALERTED:
            speak_dialogue("who_goes_there")
            # Other guards in range hear and become suspicious
            alert_nearby_guards()
        AlertState.COMBAT:
            speak_dialogue("intruder")
            trigger_alarm()

Visual Feedback (Critical!)

class_name AlertIndicator
extends Node3D

@export var idle_icon: Texture2D
@export var suspicious_icon: Texture2D  # "?" 
@export var alerted_icon: Texture2D     # "!"
@export var detection_meter: ProgressBar  # Shows filling detection

func update_indicator(state: AlertState, detection: float) -> void:
    detection_meter.value = detection
    
    match state:
        AlertState.IDLE:
            icon.texture = idle_icon
            detection_meter.visible = false
        AlertState.SUSPICIOUS:
            icon.texture = suspicious_icon
            detection_meter.visible = true
        AlertState.ALERTED:
            icon.texture = alerted_icon
            detection_meter.visible = false

---

Player Abilities

Five categories of stealth tools (per Mark Brown's analysis):

1. Movement Alteration

# Crouch, crawl, run (noisy vs quiet)
func calculate_noise_level() -> float:
    if is_crouching:
        return 0.2
    elif is_running:
        return 1.0
    else:
        return 0.5

2. Information Gathering

# Peek, scout, mark enemies
func activate_detective_vision() -> void:
    for enemy in get_tree().get_nodes_in_group("enemies"):
        enemy.show_outline()
        enemy.show_vision_cone()

3. AI Manipulation

# Throw distractions
func throw_distraction(target_position: Vector3) -> void:
    var rock := distraction_scene.instantiate()
    rock.global_position = target_position
    add_child(rock)
    SoundPropagation.propagate_sound(target_position, 30.0, "impact")

4. Space Control

# Shoot out lights, create hiding spots
func shoot_light(light: Light3D) -> void:
    light.visible = false
    # Update light level for area

5. Enemy Elimination

func perform_takedown(enemy: EnemyAI, lethal: bool) -> void:
    if enemy.alert_state == AlertState.COMBAT:
        return  # Can't stealth kill alert enemy
    
    if lethal:
        enemy.die()
    else:
        enemy.knockout()
    
    # Body becomes interactable
    spawn_body(enemy)

---

Level Design

Outpost Design (Open Areas)

                      [Safe perimeter for observation]
                               |
           [Sparse guards at edges - isolatable]
                               |
                [Dense center with objective]
                               |
              [Multiple entry points/routes]

Limited Encounter Design (Corridors)

  • Enemies visible 8+ meters before engagement
  • Multiple paths through
  • Cover objects and hiding spots
  • Emergency escape routes

---

UI Communication

Based on Thief's "light gem" innovation:

class_name StealthHUD
extends Control

@onready var visibility_meter: TextureProgressBar
@onready var sound_meter: TextureProgressBar
@onready var minimap: Control

func _process(_delta: float) -> void:
    visibility_meter.value = player.get_light_level() * 100
    sound_meter.value = player.current_noise_level * 100

4. Reaction-Delay Window (Meter)

Decoupled detection tracking with a forgiveness period to prevent instant "gotcha" moments.

# detection_meter.gd
func process_vision(is_seeing: bool, delta: float):
    if is_seeing:
        current_awareness += delta
        if current_awareness >= threshold:
            player_detected.emit()
    else:
        # Gradually decay awareness when out of sight
        current_awareness = max(0, current_awareness - delta * 0.5)

5. Cover-Point Finder (Raycast)

Find and verify valid cover points using Navigation and Physics queries.

# cover_finder.gd
func find_cover(threat_pos: Vector3):
    var map = get_world_3d().get_navigation_map()
    for i in 10:
        # Get a random point on the navmesh
        var p = NavigationServer3D.map_get_random_point(map, 1, false)
        # Verify LOS is broken from the point to the threat
        var query = PhysicsRayQueryParameters3D.create(p + Vector3.UP, threat_pos + Vector3.UP)
        var hit = get_world_3d().direct_space_state.intersect_ray(query)
        if hit: 
            return p # Hit environmental geometry, point is valid cover
    return global_position

6. Detection Cone Shader (Post-Process)

Highlight detection areas using a screen-space shader for clear player feedback.

// vision.gdshader
shader_type canvas_item;
uniform sampler2D screen : hint_screen_texture;
uniform vec2 enemy_pos; // Normalized screen coordinates (0.0-1.0)

void fragment() {
    vec4 base = texture(screen, SCREEN_UV);
    float dist = distance(SCREEN_UV, enemy_pos);
    if (dist < 0.2) {
        // Tint detected area red
        COLOR = mix(base, vec4(1, 0, 0, 1), 0.3);
    } else {
        COLOR = base;
    }
}

---

## Common Pitfalls

| Pitfall | Solution |
|---------|----------|
| Instant detection | Use gradual detection with clear feedback |
| Guards see through walls | Raycast-based vision with proper collision |
| Unfair patrol patterns | Make patterns learnable, with tells |
| Two games (stealth + combat) | Either commit to stealth or make combat risky |
| Unclear detection | Always show WHY player was detected |

---

## Godot-Specific Tips

1. **Raycasts for vision**: Use `PhysicsRayQueryParameters3D` with collision masks
2. **NavigationAgent3D**: For patrol routes and pathfinding
3. **Area3D**: For sound propagation zones and trigger areas
4. **AnimationTree**: Blend between alert state animations


## Reference
- Master Skill: [godot-master](../godot-master/SKILL.md)

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.