references/architecture-overview.md
# Architecture Overview
### 1. Wave Manager
Handles the timing and godot-composition of enemy waves.
```gdscript
# wave_manager.gd
extends Node
signal wave_started(wave_index: int)
signal wave_cleared
signal enemy_spawned(enemy: Node2D)
@export var waves: Array[Resource] # Array of WaveDefinition resources
var current_wave_index: int = 0
var active_enemies: int = 0
func start_next_wave() -> void:
if current_wave_index >= waves.size():
print("All waves cleared!")
return
var wave_data = waves[current_wave_index]
wave_started.emit(current_wave_index)
_spawn_wave(wave_data)
current_wave_index += 1
func _spawn_wave(wave: WaveResource) -> void:
for group in wave.groups:
await get_tree().create_timer(group.delay).timeout
for i in group.count:
var enemy = group.enemy_scene.instantiate()
add_child(enemy)
active_enemies += 1
enemy.tree_exiting.connect(_on_enemy_died)
await get_tree().create_timer(group.interval).timeout
func _on_enemy_died() -> void:
active_enemies -= 1
if active_enemies <= 0:
wave_cleared.emit()
```
### 2. Tower Logic (State Machine)
Towers act as autonomous agents.
* **States**: `Idle`, `AcquireTarget`, `Attack`, `Cooldown`.
* **Targeting Priority**: `First`, `Last`, `Strongest`, `Weakest`, `Closest`.
```gdscript
# tower.gd
extends Node2D
var targets_in_range: Array[Node2D] = []
var current_target: Node2D
func _physics_process(delta: float) -> void:
if current_target == null or not is_instance_valid(current_target):
_acquire_target()
if current_target:
_rotate_turret(current_target.global_position)
if can_fire():
fire_projectile()
func _acquire_target() -> void:
# Example: Target closest to end of path
var max_progress = -1.0
for enemy in targets_in_range:
if enemy.progress > max_progress:
current_target = enemy
max_progress = enemy.progress
```
### 3. Pathfinding Variants
#### A. Fixed Path (Kingdom Rush style)
Enemies follow a pre-defined `Path2D`.
* **Implementation**: `PathFollow2D` as parent of Enemy.
* **Pros**: Deterministic, easy to balance, optimized.
* **Cons**: Less player agency in shaping the path.
#### B. Mazing (Fieldrunners style)
Players build towers to block/reroute enemies.
* **Implementation**: `NavigationAgent2D` on enemies. Towers update `NavigationRegion2D` (bake on separate thread).
* **Pros**: High strategic depth.
* **Cons**: Computationally expensive recalculation, needs anti-blocking logic (don't let player seal the exit).
references/common-pitfalls-extended.md
# Common Pitfalls
1. **Death Spirals**: If a player leaks one enemy, they lose money/lives, making the next wave harder, leading to inevitable failure. **Fix**: Catch-up mechanics or discrete wave difficulty.
2. **Useless Towers**: Every tower type must have a distinct niche (AoE, Slow, Armor Pierce, Anti-Air).
3. **Path Blocking**: In mazing games, ensure players cannot completely block the path to the exit. Use `NavigationServer2D.map_get_path` to validate placement before building.
references/elite-technical-patterns.md
# 🚀 Elite Technical Implementations (Batch 09)
### 1. Navigation-Path-Validation (Maze Sealing Prevention)
In mazing TD games, players must not be able to block the exit. Use `AStarGrid2D` to simulate building placement and verify that a valid path still exists from spawn to core.
```gdscript
class_name GridPathValidator extends Node
var _astar_grid: AStarGrid2D
@export var spawn_point: Vector2i = Vector2i(0, 0)
@export var core_point: Vector2i = Vector2i(20, 20)
func _ready() -> void:
_astar_grid = AStarGrid2D.new()
_astar_grid.region = Rect2i(0, 0, 40, 40)
_astar_grid.cell_size = Vector2(64, 64)
_astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
_astar_grid.update()
---
# Simulates placing a tower. Returns true if the path remains valid.
func can_build_tower_at(cell_coords: Vector2i) -> bool:
if _astar_grid.is_point_solid(cell_coords):
return false
# 1. Temporarily mark the cell as solid
_astar_grid.set_point_solid(cell_coords, true)
# 2. Query path from start to finish
var test_path: Array[Vector2i] = _astar_grid.get_id_path(spawn_point, core_point)
# 3. If empty, maze is sealed. Revert and deny.
if test_path.is_empty():
_astar_grid.set_point_solid(cell_coords, false)
return false
return true
```
### 2. Burst-Searching (Frame-Sliced Targeting)
Towers scanning for enemies every frame create CPU spikes. Use `Engine.get_process_frames()` with a random offset to distribute targeting logic across multiple frames.
```gdscript
class_name BurstSearchTower extends Node2D
@export var search_interval_frames: int = 10
@export var attack_range: float = 250.0
var _frame_offset: int = 0
var _current_target: Node2D = null
func _ready() -> void:
# Stagger search frame per tower
_frame_offset = randi() % search_interval_frames
func _physics_process(_delta: float) -> void:
# Execute expensive logic only once every N frames
if (Engine.get_process_frames() + _frame_offset) % search_interval_frames == 0:
_burst_search_for_target()
func _burst_search_for_target() -> void:
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")
# ... distance squared logic to pick closest target ...
```
### 3. Bezier-Path-Follow (Organic Movement)
Smooth, curved enemy movement is achieved using `Path2D` and `PathFollow2D`. Increase the `progress` property to move the enemy along the spline.
```gdscript
class_name OrganicEnemyMovement extends PathFollow2D
@export var move_speed: float = 150.0
func _physics_process(delta: float) -> void:
# Use 'progress' (Godot 4) to advance along the Curve2D
progress += move_speed * delta
if progress_ratio >= 1.0:
# Reached the core
queue_free()
```
- Master Skill: [godot-master](../godot-master/SKILL.md)
- Related: Prove wave/economy bands with [godot-monte-carlo-balancer](../godot-monte-carlo-balancer/SKILL.md) (see also lane-defense example ref).
references/godot-tips.md
# Godot-Specific Tips
* **Physics Layers**: Put enemies on a specific layer (e.g., Layer 2) and tower "range" Areas on a different mask to avoid towers detecting each other or walls.
* **Area2D Performance**: For massive numbers of enemies, avoid `monitorable/monitoring` on every frame if possible. Use `PhysicsServer2D` queries for optimization if enemy count > 500.
* **Object Pooling**: Essential for projectiles and enemies to avoid garbage collection stutters during intense waves.
---
## 🚀 Elite Technical Implementations (Batch 09)
### 1. Navigation-Path-Validation (Maze Sealing Prevention)
In mazing TD games, players must not be able to block the exit. Use `AStarGrid2D` to simulate building placement and verify that a valid path still exists from spawn to core.
```gdscript
class_name GridPathValidator extends Node
var _astar_grid: AStarGrid2D
@export var spawn_point: Vector2i = Vector2i(0, 0)
@export var core_point: Vector2i = Vector2i(20, 20)
func _ready() -> void:
_astar_grid = AStarGrid2D.new()
_astar_grid.region = Rect2i(0, 0, 40, 40)
_astar_grid.cell_size = Vector2(64, 64)
_astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
_astar_grid.update()
## Simulates placing a tower. Returns true if the path remains valid.
func can_build_tower_at(cell_coords: Vector2i) -> bool:
if _astar_grid.is_point_solid(cell_coords):
return false
# 1. Temporarily mark the cell as solid
_astar_grid.set_point_solid(cell_coords, true)
# 2. Query path from start to finish
var test_path: Array[Vector2i] = _astar_grid.get_id_path(spawn_point, core_point)
# 3. If empty, maze is sealed. Revert and deny.
if test_path.is_empty():
_astar_grid.set_point_solid(cell_coords, false)
return false
return true
```
### 2. Burst-Searching (Frame-Sliced Targeting)
Towers scanning for enemies every frame create CPU spikes. Use `Engine.get_process_frames()` with a random offset to distribute targeting logic across multiple frames.
```gdscript
class_name BurstSearchTower extends Node2D
@export var search_interval_frames: int = 10
@export var attack_range: float = 250.0
var _frame_offset: int = 0
var _current_target: Node2D = null
func _ready() -> void:
# Stagger search frame per tower
_frame_offset = randi() % search_interval_frames
func _physics_process(_delta: float) -> void:
# Execute expensive logic only once every N frames
if (Engine.get_process_frames() + _frame_offset) % search_interval_frames == 0:
_burst_search_for_target()
func _burst_search_for_target() -> void:
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")
# ... distance squared logic to pick closest target ...
```
### 3. Bezier-Path-Follow (Organic Movement)
Smooth, curved enemy movement is achieved using `Path2D` and `PathFollow2D`. Increase the `progress` property to move the enemy along the spline.
```gdscript
class_name OrganicEnemyMovement extends PathFollow2D
@export var move_speed: float = 150.0
func _physics_process(delta: float) -> void:
# Use 'progress' (Godot 4) to advance along the Curve2D
progress += move_speed * delta
if progress_ratio >= 1.0:
# Reached the core
queue_free()
```
- Master Skill: [godot-master](../godot-master/SKILL.md)
- Related: Prove wave/economy bands with [godot-monte-carlo-balancer](../godot-monte-carlo-balancer/SKILL.md) (see also lane-defense example ref).
references/key-mechanics.md
# Key Mechanics Implementation
### Targeting Math (Projectile Prediction)
To hit a moving target, you must predict where it will be.
```gdscript
func get_predicted_position(target: Node2D, projectile_speed: float) -> Vector2:
var to_target = target.global_position - global_position
var time_to_hit = to_target.length() / projectile_speed
return target.global_position + (target.velocity * time_to_hit)
```
### Economy
Money management is the secondary core loop.
* **Kill Rewards**: Direct feedback for success.
* **Interest/Income**: Rewarding saved money (risk/reward).
* **Early Calling**: Bonus money for starting the next wave early.
references/migration-notes.md
# Migration notes: godot-genre-tower-defense
Incremental upgrade for topics this skill covers. Apply **one hop**, stabilize/test, then next. Never skip hops.
If the project is **< 4.0**, follow [godot-version-migration](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-version-migration/SKILL.md) era bridges (legacy → 3→4) until 4.0, then these hops. Official 3→4: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html).
## 4.0 → 4.1
Official: [Upgrading to Godot 4.1](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.1.html)
- `NavigationAgent2D/3D.set_velocity` → `velocity` property.
- `time_horizon` split into `time_horizon_agents` and `time_horizon_obstacles`.
- `NavigationAgent3D.agent_height_offset` → `path_height_offset`; `ignore_y` removed.
- `NavigationObstacle*.estimate_radius` removed; `get_rid` → `get_agent_rid`.
- `NavigationServer*.agent_set_callback` → `agent_set_avoidance_callback`; target velocity / time_horizon server APIs split/removed.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
- `TileMap.cell_quadrant_size` → `rendering_quadrant_size`.
## 4.2 → 4.3
Official: [Upgrading to Godot 4.3](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.3.html)
- If the genre uses TileMap, migrate to TileMapLayer nodes before relying on layer APIs.
- If the genre ships multiplayer, upgrade all peers to 4.3 together (SceneMultiplayer protocol).
- `AStar2D/3D/Grid2D.get_*_path` gain `allow_partial_path`.
- `NavigationRegion2D` experimental avoidance props (`avoidance_layers`, `constrain_avoidance`, …) removed with no replacement.
- **TileMap layers moved to `TileMapLayer` nodes** — migrate scenes/scripts to per-layer nodes.
- `TileData.get_navigation_polygon` / `get_occluder` gain flip/transpose optionals.
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- `NavigationServer2D/3D.query_path` gains optional `callback`.
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
- Nav regions update asynchronously by default (`navigation/world/region_use_async_iterations`) — expect sync delay.
- Navmesh merge order changed; edge merge errors may surface — tune `merge_rasterizer_cell_scale` / fix overlapping navmeshes.
- `TileMapLayer.get_coords_for_body_rid` less precise with physics chunking — set `physics_quadrant_size = 1` for old precision.
## 4.5 → 4.6
Official: [Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html)
- Retune Environment glow/fog if the genre leans on bloom-heavy looks.
- `AStar*.get_point_path` / `get_id_path` return empty path when `from_id` is disabled/solid.
## 4.6 → 4.7
Official: [Upgrading to Godot 4.7](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html)
- Confirm project stretch mode and AudioStreamPlayer area_mask after opening in 4.7.
scripts/burst_search_tower.gd
class_name BurstSearchTower extends Node2D
@export var search_interval_frames: int = 10
@export var attack_range: float = 250.0
var _frame_offset: int = 0
var _current_target: Node2D = null
func _ready() -> void:
# Stagger search frame per tower
_frame_offset = randi() % search_interval_frames
func _physics_process(_delta: float) -> void:
# Execute expensive logic only once every N frames
if (Engine.get_process_frames() + _frame_offset) % search_interval_frames == 0:
_burst_search_for_target()
func _burst_search_for_target() -> void:
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")
# ... distance squared logic to pick closest target ...
scripts/grid_path_validator.gd
class_name GridPathValidator extends Node
var _astar_grid: AStarGrid2D
@export var spawn_point: Vector2i = Vector2i(0, 0)
@export var core_point: Vector2i = Vector2i(20, 20)
func _ready() -> void:
_astar_grid = AStarGrid2D.new()
_astar_grid.region = Rect2i(0, 0, 40, 40)
_astar_grid.cell_size = Vector2(64, 64)
_astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
_astar_grid.update()
## Simulates placing a tower. Returns true if the path remains valid.
func can_build_tower_at(cell_coords: Vector2i) -> bool:
if _astar_grid.is_point_solid(cell_coords):
return false
# 1. Temporarily mark the cell as solid
_astar_grid.set_point_solid(cell_coords, true)
# 2. Query path from start to finish
var test_path: Array[Vector2i] = _astar_grid.get_id_path(spawn_point, core_point)
# 3. If empty, maze is sealed. Revert and deny.
if test_path.is_empty():
_astar_grid.set_point_solid(cell_coords, false)
return false
return true
scripts/homing_projectile_3d.gd
extends Area3D
class_name HomingProjectile3D
## Expert Homing Projectile (Godot 4.7).
## Uses Quaternion slerp for smooth tracking and handles 'Target Lost' gracefully.
@export var speed: float = 15.0
@export var turn_speed: float = 5.0
var target: Node3D = null
func _physics_process(delta: float) -> void:
# 1. Aim Logic (Rotation)
if is_instance_valid(target):
var target_pos = target.global_position
var dir = global_position.direction_to(target_pos)
# Expert Pattern: Smoothly rotate basis using Quaternions
var target_basis = Basis.looking_at(dir)
var current_quat = global_transform.basis.get_rotation_quaternion()
var target_quat = target_basis.get_rotation_quaternion()
global_transform.basis = Basis(current_quat.slerp(target_quat, turn_speed * delta))
# 2. Movement Logic (Local Forward)
global_position += -global_transform.basis.z * speed * delta
## [SKILL NOTICE]: Use 'is_instance_valid(target)' to check if the enemy
## was freed/killed before impact to prevent null pointer crashes.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_quaternion.html
# - https://docs.godotengine.org/en/stable/classes/class_basis.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md — projectile lifetime and target-lost handling
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — Area3D hit detection on impact
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — pool homing missiles under wave spikes
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-tower-defense/SKILL.md
# =============================================================================
scripts/organic_enemy_movement.gd
class_name OrganicEnemyMovement extends PathFollow2D
@export var move_speed: float = 150.0
func _physics_process(delta: float) -> void:
# Use 'progress' (Godot 4) to advance along the Curve2D
progress += move_speed * delta
if progress_ratio >= 1.0:
# Reached the core
queue_free()
scripts/tower_defense_patterns.gd
# tower_defense_patterns.gd
extends Node
# 1. Finding Primary Target (Furthest/First)
# EXPERT NOTE: Use functional reduction to efficiently find the enemy with the most path progress.
func get_first_target(enemies: Array[Node3D]) -> Node3D:
if enemies.is_empty(): return null
return enemies.reduce(func(max_e, e): return e if e.get(&"progress") > max_e.get(&"progress") else max_e)
# 2. Bypassing Nodes for Projectiles (PhysicsServer3D)
# EXPERT NOTE: Create bullets directly in the physics server for massive performance in bullet-hell scenarios.
func spawn_fast_bullet(space: RID, transform: Transform3D) -> RID:
var body := PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_KINEMATIC)
PhysicsServer3D.body_set_space(body, space)
PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, transform)
return body
# 3. ShapeCast for AoE Splash Damage
# EXPERT NOTE: Instantly grab all enemies in an explosion radius using the direct physics space state.
func apply_aoe_damage(pos: Transform3D, shape_rid: RID, damage: float) -> void:
var space := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.transform = pos
query.shape_rid = shape_rid
for result in space.intersect_shape(query):
if result.collider.has_method(&"take_damage"):
result.collider.call(&"take_damage", damage)
# 4. Spawning PathFollowers for Wave Minions
# EXPERT NOTE: Standard pattern for moving enemies along a predefined track with automatic orientation.
func spawn_minion(path: Path3D, scene: PackedScene) -> PathFollow3D:
var follower := PathFollow3D.new()
path.add_child(follower)
follower.add_child(scene.instantiate())
return follower
# 5. Deferred Collision Disabling for Corpses
# EXPERT NOTE: Safely remove collisions from dead enemies without causing physics server crashes.
func handle_death(collider: CollisionShape3D) -> void:
collider.set_deferred(&"disabled", true)
# 6. Optimized Enemy Type ID (StringName)
# EXPERT NOTE: Use StringName for significantly faster hash comparisons in high-frequency wave logic.
func check_enemy_type(type: StringName) -> void:
if type == &"armored_orc":
pass
# 7. Authoritative Economy Validation
# EXPERT NOTE: Always validate tower purchases on the server to prevent cheating in co-op.
@rpc("any_peer", "call_local", "reliable")
func request_tower_purchase(id: StringName, pos: Vector3) -> void:
if multiplayer.is_server():
# validate_funds(multiplayer.get_remote_sender_id())
print("Server validated purchase: ", id)
# 8. Unreliable Wave State Syncing
# EXPERT NOTE: Sync many moving minions via UDP (unreliable) to save bandwidth.
func sync_minion_positions(data: PackedByteArray) -> void:
multiplayer.send_bytes(data, 0, MultiplayerPeer.TRANSFER_MODE_UNRELIABLE)
# 9. Strict Vector3i Grid Placements
# EXPERT NOTE: Use integer vectors for grid-based tower placement to ensure mathematical precision.
var tower_grid: Dictionary[Vector3i, Node3D] = {}
# 10. Awaiting Timers for Wave Spawning
# EXPERT NOTE: Cleanest way to handle fixed-interval spawning without complex timer nodes.
func start_wave_sequence(count: int, delay: float) -> void:
for i in count:
# spawn_enemy()
await get_tree().create_timer(delay).timeout
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/performance/using_servers.html
# - https://docs.godotengine.org/en/stable/classes/class_physicsserver3d.html
# - https://docs.godotengine.org/en/stable/classes/class_physicsshapequeryparameters3d.html
# - https://docs.godotengine.org/en/stable/classes/class_astargrid2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — PhysicsServer bullets and deferred corpse disable
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md — authoritative purchase RPC and unreliable sync
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-navigation-pathfinding/SKILL.md — grid/path validation before placement
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-tower-defense/SKILL.md
# =============================================================================
scripts/tower_targeting_system.gd
extends Node3D
class_name TowerTargetingSystem
## Tower Targeting Expert Pattern
## Signal-cached targets + frame-sliced acquire + First/Last/Strongest/Weakest.
enum Priority { FIRST, LAST, STRONGEST, WEAKEST }
@export var target_priority: Priority = Priority.FIRST
@export var range: float = 10.0
@export var projectile_speed: float = 20.0
@export var acquire_interval_frames: int = 8
@export var range_area: Area3D # body_entered / body_exited cache — NEVER get_overlapping every frame
var _current_target: Node3D = null
var _targets_in_range: Array[Node3D] = []
var _frame_offset: int = 0
func _ready() -> void:
_frame_offset = randi() % max(1, acquire_interval_frames)
if range_area:
range_area.body_entered.connect(_on_body_entered)
range_area.body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node3D) -> void:
if body.is_in_group("enemies") and body not in _targets_in_range:
_targets_in_range.append(body)
func _on_body_exited(body: Node3D) -> void:
_targets_in_range.erase(body)
if _current_target == body:
_current_target = null
func _physics_process(_delta: float) -> void:
# Frame-sliced acquire (BurstSearch pattern) — keep aim every frame
if (Engine.get_process_frames() + _frame_offset) % max(1, acquire_interval_frames) == 0:
_acquire_target()
if _current_target and is_instance_valid(_current_target):
_aim_at_target()
else:
_current_target = null
func _acquire_target() -> void:
var potential: Array[Node3D] = []
for enemy in _targets_in_range:
if not is_instance_valid(enemy):
continue
if global_position.distance_squared_to(enemy.global_position) <= range * range:
potential.append(enemy)
if potential.is_empty():
_current_target = null
return
match target_priority:
Priority.FIRST:
potential.sort_custom(func(a, b): return float(a.get("progress")) > float(b.get("progress")))
Priority.LAST:
potential.sort_custom(func(a, b): return float(a.get("progress")) < float(b.get("progress")))
Priority.STRONGEST:
potential.sort_custom(func(a, b): return float(a.get("health")) > float(b.get("health")))
Priority.WEAKEST:
potential.sort_custom(func(a, b): return float(a.get("health")) < float(b.get("health")))
_current_target = potential[0]
func _aim_at_target() -> void:
var target_pos = _current_target.global_position
var target_vel = _current_target.velocity if "velocity" in _current_target else Vector3.ZERO
var dist = global_position.distance_to(target_pos)
var time_to_impact = dist / max(0.001, projectile_speed)
var predicted_pos = target_pos + (target_vel * time_to_impact)
var target_basis = Basis.looking_at(predicted_pos - global_position)
global_basis = global_basis.slerp(target_basis, 0.1)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_pathfollow3d.html
# - https://docs.godotengine.org/en/stable/tutorials/math/vectors_advanced.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md — lead prediction and priority targeting
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — First/Last/Strongest fairness under wave mixes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — 3D range and aim transforms
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-tower-defense/SKILL.md
# =============================================================================
scripts/tower.gd
# skills/genre-tower-defense/scripts/tower.gd
extends Node2D
## Tower Logic (Expert Pattern)
## Autonomous turret with targeting priority and projectile prediction.
## Separates visual rotation from firing logic.
class_name Tower
enum TargetPriority { FIRST, LAST, CLOSEST, STRONGEST }
@export var range_radius: float = 200.0
@export var fire_rate: float = 1.0 # Shots per second
@export var projectile_scene: PackedScene
@export var turret_visual: Node2D
@export var priority: TargetPriority = TargetPriority.FIRST
var targets_in_range: Array[Node2D] = []
var current_target: Node2D
var _cooldown: float = 0.0
func _ready() -> void:
# Setup Area2D for range
var area = Area2D.new()
var shape = CollisionShape2D.new()
var circle = CircleShape2D.new()
circle.radius = range_radius
shape.shape = circle
area.add_child(shape)
add_child(area)
area.body_entered.connect(_on_body_entered)
area.body_exited.connect(_on_body_exited)
func _physics_process(delta: float) -> void:
_cooldown -= delta
if not is_instance_valid(current_target):
_acquire_target()
if current_target:
_rotate_toward(current_target.global_position)
if _cooldown <= 0:
_fire()
func _acquire_target() -> void:
if targets_in_range.is_empty():
current_target = null
return
# Sort or pick based on priority
# Simplified: just pick first valid
current_target = targets_in_range[0]
func _fire() -> void:
_cooldown = 1.0 / fire_rate
if projectile_scene:
var proj = projectile_scene.instantiate()
get_tree().root.add_child(proj)
proj.global_position = global_position
# Assume projectile has setup method
if proj.has_method("setup") and current_target:
var dir = global_position.direction_to(current_target.global_position)
proj.setup(dir, global_position)
func _rotate_toward(pos: Vector2) -> void:
if turret_visual:
turret_visual.look_at(pos)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("enemy"):
targets_in_range.append(body)
func _on_body_exited(body: Node2D) -> void:
targets_in_range.erase(body)
if body == current_target:
current_target = null
## EXPERT USAGE:
## Assign projectile_scene. Ensure enemies are in "enemy" group.
## Adjust Range Radius in inspector.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html
# - https://docs.godotengine.org/en/stable/classes/class_area2d.html
# - https://docs.godotengine.org/en/stable/classes/class_circleshape2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-2d-physics/SKILL.md — Area2D range cache via body_entered/exited
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md — Idle/Acquire/Attack/Cooldown tower FSM
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md — projectile fire and hit resolution
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-tower-defense/SKILL.md
# =============================================================================
scripts/wave_manager.gd
# skills/genre-tower-defense/scripts/wave_manager.gd
extends Node
## TD Wave Manager (Expert Pattern)
## Data-driven wave spawning with support for multiple enemy types, delays, and wave intervals.
class_name WaveManager
signal wave_started(index: int)
signal wave_completed(index: int)
signal all_waves_complete
# Inner class for Wave Data (or use external Resources)
class WaveGroup:
var enemy_scene: PackedScene
var count: int
var interval: float = 1.0 # Time between spawns
var initial_delay: float = 0.0
var waves: Array[Array] = [] # Array of Arrays of WaveGroups
var current_wave_index: int = -1
var active_enemies: int = 0
var is_wave_active: bool = false
@export var spawn_points: Array[Node2D]
func _ready() -> void:
# Example setup - in prod, load this from Resources
_setup_debug_waves()
func start_next_wave() -> void:
if is_wave_active:
return
current_wave_index += 1
if current_wave_index >= waves.size():
all_waves_complete.emit()
return
is_wave_active = true
wave_started.emit(current_wave_index)
var wave_groups = waves[current_wave_index]
# Process groups in parallel or sequence? usually parallel logic per group
for group in wave_groups:
_process_wave_group(group)
func _process_wave_group(group: WaveGroup) -> void:
await get_tree().create_timer(group.initial_delay).timeout
for i in range(group.count):
_spawn_enemy(group.enemy_scene)
await get_tree().create_timer(group.interval).timeout
func _spawn_enemy(scene: PackedScene) -> void:
var spawn = spawn_points[0] # Simple single spawn logic
var enemy = scene.instantiate()
spawn.add_child(enemy) # Or add to a container
enemy.global_position = spawn.global_position
active_enemies += 1
# Connect signal safely
if enemy.has_signal("died"):
enemy.died.connect(_on_enemy_died)
else:
# Fallback if no specific signal, use tree_exiting (less reliable for "death" vs "freed")
enemy.tree_exiting.connect(_on_enemy_died)
func _on_enemy_died() -> void:
active_enemies -= 1
if active_enemies <= 0 and _all_spawns_finished():
is_wave_active = false
wave_completed.emit(current_wave_index)
func _all_spawns_finished() -> bool:
# Need a more robust check in real prod to ensure not just 0 enemies but 0 pending spawns
# For now, simplest check:
return true
func _setup_debug_waves() -> void:
# Placeholder to prevent crash
pass
## EXPERT USAGE:
## Populate 'waves' with WaveGroup resources. Link 'spawn_points'.
## Connect to 'wave_completed' to show UI or grant gold.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html
# - https://docs.godotengine.org/en/stable/classes/class_packedscene.html
# - https://docs.godotengine.org/en/stable/classes/class_timer.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-game-loop-waves/SKILL.md — prepare/defend/reward around wave_started
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md — WaveGroup data as Resources
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — wave_completed / all_waves_complete buses
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-tower-defense/SKILL.md
# =============================================================================
scripts/wave_resource_spawner.gd
extends Node
class_name WaveResourceSpawner
## Expert Wave Spawner (Godot 4.7).
## Uses WaveData Resources and Path3D for track-based enemy spawning.
@export var waves: Array[Resource] # Array of WaveData
@export var track: Path3D
var current_wave: int = 0
func spawn_wave() -> void:
if current_wave >= waves.size(): return
var data = waves[current_wave]
for i in data.count:
_spawn_unit(data.enemy_scene)
await get_tree().create_timer(data.interval).timeout
current_wave += 1
func _spawn_unit(scene: PackedScene) -> void:
# Expert Pattern: Standard TD movement using PathFollow3D child
var follower = PathFollow3D.new()
follower.loop = false
track.add_child(follower)
var unit = scene.instantiate()
follower.add_child(unit)
## [SKILL NOTICE]: Instantiate enemies as children of 'PathFollow3D'
## and update the 'progress' property to move them along the track.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_path3d.html
# - https://docs.godotengine.org/en/stable/classes/class_pathfollow3d.html
# - https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md — WaveData .tres spawn sequences
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-game-loop-waves/SKILL.md — interval spawning without switch spam
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — interval/count bands vs leak rate
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-tower-defense/SKILL.md
# =============================================================================
SKILL.md
---
name: godot-genre-tower-defense
description: "Expert blueprint for tower defense games (Bloons TD, Kingdom Rush, Fieldrunners) covering wave management, tower targeting logic, path algorithms, economy balance, and mazing mechanics. Use when building TD, lane defense, or tower placement strategy games. Keywords tower defense, wave spawner, pathfinding, targeting priority, mazing, NavigationServer baking."
---
## Core Loop
1. **Prepare**: Build/upgrade towers with available currency
2. **Wave**: Enemies spawn and traverse path toward goal
3. **Defend**: Towers auto-target and damage enemies
4. **Reward**: Kills grant currency
5. **Escalate**: Waves increase in difficulty/complexity
## NEVER Do (Expert Anti-Patterns)
### Design & Strategy
- NEVER make all towers have the same niche; strictly ensure distinct specialties: **Aura Slow**, **Armor Piercing**, **Anti-Air**, **Burst Sniper**, and **Splash Damage**.
- NEVER allow a "Death Spiral" with no exit; strictly provide small **comeback bonuses** or interest on saved gold to prevent early inevitable failure.
- NEVER make early waves feel like busywork; strictly provide an **"Early Call" bonus** to skip wait times and accelerate engagement.
- NEVER trust client-side economy updates; strictly require the **authoritative server** to validate currency addition and tower purchases in co-op.
### Pathing & Placement
- NEVER allow the player to "Seal" the exit in mazing games; strictly validate path existence with **`NavigationServer2D.map_get_path()`** before finalizing tower placement.
- NEVER use synchronous `bake_navigation_polygon()` for mazing; strictly offload to a **worker thread** to prevent 100ms+ frame hitches during placement.
- NEVER use global coordinates for grid logic; strictly convert to **Vector2i/Vector3i** to ensure pixel-perfect tower alignment.
### Performance & Systems
- NEVER call `get_overlapping_bodies()` every frame; strictly use **signals** (`body_entered`/`body_exited`) to maintain a local target cache.
- NEVER use `_process()` for projectile movement if count > 500; strictly use the **PhysicsServer2D/3D** directly for high-performance bullet-hell tiers.
- NEVER spawn hundreds of projectiles as full Nodes; strictly use **Object Pooling** to reuse resources and avoid garbage collection stutters.
- NEVER use standard Strings for priorities; strictly use `StringName` (&"first", &"strongest") for O(1) hash comparisons in targeting loops.
- NEVER ignore the `progress` property on PathFollow nodes; strictly use it as the O(1) way to identify the **target closest to exit**.
- NEVER process tower search logic every frame; strictly **throttle ACQUIRE searches** (e.g., every 5-10 frames) to save significant CPU cycles.
- NEVER scale Tower `CollisionShape` non-uniformly; strictly adjust the radius property of the Shape resource to preserve collision math.
- NEVER delete enemies immediately on death; strictly use **set_deferred("disabled", true)** and wait one frame to prevent physics server crashes.
- NEVER hardcode waves in huge switch statements; strictly use **Custom Resources (.tres)** for clean balancing and sequence editing.
---
## 🛠 Expert Components (scripts/)
### Original Expert Patterns
- [wave_manager.gd](scripts/wave_manager.gd) - Professional wave orchestrator with Resource-based enemy composition and cleanup.
- [tower.gd](scripts/tower.gd) - Base turret class with FSM state management and firing logic.
- [tower_targeting_system.gd](scripts/tower_targeting_system.gd) - Autonomous priority logic (First/Last/Strongest/Weakest) for efficient targeting.
### Modular Components
- [tower_defense_patterns.gd](scripts/tower_defense_patterns.gd) - Collection of patterns for furthest-target logic and PhysicsServer projectile optimization.
## Decision Trees (MANDATORY script reads)
### Path style
| Style | Approach | Scripts / APIs |
|-------|----------|----------------|
| Fixed lanes | `Path2D` / `PathFollow2D` `progress` | [wave_manager.gd](scripts/wave_manager.gd), [wave_resource_spawner.gd](scripts/wave_resource_spawner.gd) |
| Mazing | Seal-check before place | `NavigationServer2D.map_get_path` / `AStarGrid2D` (NEVER) |
| Organic curves | Bezier PathFollow `progress` | Prefer PathFollow over per-frame seek |
### Targeting priority
| Priority | Sort key | **MANDATORY** |
|----------|----------|---------------|
| FIRST | Highest `progress` (closest to exit) | [tower_targeting_system.gd](scripts/tower_targeting_system.gd) |
| LAST | Lowest `progress` | same — LAST implemented |
| STRONGEST / WEAKEST | `health` desc / asc | same — WEAKEST implemented |
Use **signal-cached** range `Area` enter/exit + **frame-sliced** acquire (`acquire_interval_frames`). Never `get_overlapping_bodies()` every frame.
### Economy
| Concern | Rule | Script |
|---------|------|--------|
| Wave composition | Resource `.tres` waves | [wave_manager.gd](scripts/wave_manager.gd) |
| Co-op purchases | Server validates gold | [tower_defense_patterns.gd](scripts/tower_defense_patterns.gd) |
| Comeback | Interest / early-call bonus | Design-level — not tower FSM |
## PhysicsServer Projectile Golden Path
When count > ~500:
1. **MANDATORY** [tower_defense_patterns.gd](scripts/tower_defense_patterns.gd) `spawn_fast_bullet` (`PhysicsServer3D` kinematic RIDs).
2. Pool RIDs; AoE via `intersect_shape`; defer collision disable on death.
3. Modest counts: [homing_projectile_3d.gd](scripts/homing_projectile_3d.gd) / pooled Nodes OK.
## Tower FSM
**MANDATORY**: [tower.gd](scripts/tower.gd) for idle → acquire → windup → fire. Targeting stays in [tower_targeting_system.gd](scripts/tower_targeting_system.gd).
## Deep recipes (on demand)
| Topic | Reference / script |
|-------|-------------------|
| Waves / towers / paths | [architecture-overview.md](references/architecture-overview.md) |
| Projectile lead & targeting | [key-mechanics.md](references/key-mechanics.md) |
| Maze validation & burst search | [elite-technical-patterns.md](references/elite-technical-patterns.md) + [grid_path_validator.gd](scripts/grid_path_validator.gd) |
## Reference
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
### Official Documentation
- [Navigation introduction (2D)](https://docs.godotengine.org/en/stable/tutorials/navigation/navigation_introduction_2d.html) — NavigationRegion2D baking and map queries for mazing TD path validity.
- [Using NavigationAgents](https://docs.godotengine.org/en/stable/tutorials/navigation/navigation_using_navigationagents.html) — agent path following and avoidance when towers reshape walkable space.
- [NavigationServer2D](https://docs.godotengine.org/en/stable/classes/class_navigationserver2d.html) — `map_get_path` seal checks before committing tower placement.
- [AStarGrid2D](https://docs.godotengine.org/en/stable/classes/class_astargrid2d.html) — integer-grid path probes that simulate build cells without a full nav bake.
- [PathFollow2D](https://docs.godotengine.org/en/stable/classes/class_pathfollow2d.html) — `progress` / `progress_ratio` for fixed-lane enemies and First/Last targeting.
- [PathFollow3D](https://docs.godotengine.org/en/stable/classes/class_pathfollow3d.html) — 3D track followers used by wave spawners and homing aim references.
- [Using Area2D](https://docs.godotengine.org/en/stable/tutorials/physics/using_area_2d.html) — signal-driven range caches (`body_entered` / `body_exited`) instead of per-frame overlap polls.
- [Physics introduction](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html) — layers/masks so tower ranges hit enemies, not other towers or walls.
- [Using servers](https://docs.godotengine.org/en/stable/tutorials/performance/using_servers.html) — PhysicsServer bodies for high-count projectiles without Node overhead.
- [Resources](https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html) — WaveDefinition `.tres` data instead of hard-coded spawn switches.
- [Using multiple threads](https://docs.godotengine.org/en/stable/tutorials/performance/using_multiple_threads.html) — WorkerThreadPool / Thread patterns for async navigation rebakes during placement.
- [Using TileMaps](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilemaps.html) — TileMapLayer grids for build cells, paths, and placement snapping.
### Related Skills
#### Prerequisites
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — project settings, layers, and scene structure before wave/tower wiring.
- [godot-navigation-pathfinding](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-navigation-pathfinding/SKILL.md) — NavigationServer bake, agents, and path queries that mazing TD depends on.
- [godot-tilemap-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tilemap-mastery/SKILL.md) — TileMapLayer/TileSet grids for buildable cells and lane painting.
#### Complements
- [godot-2d-physics](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-2d-physics/SKILL.md) — Area2D range, layers, and PhysicsServer2D projectile patterns for dense waves.
- [godot-state-machine-advanced](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md) — StringName tower FSM states (Idle/Acquire/Attack/Cooldown).
- [godot-economy-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-economy-system/SKILL.md) — kill rewards, interest, and early-call income without death spirals.
- [godot-combat-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md) — damage types, armor pierce, splash, and projectile hit resolution.
- [godot-resource-data-patterns](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md) — typed Wave/Tower Resources and `duplicate(true)` for balance edits.
- [godot-game-loop-waves](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-game-loop-waves/SKILL.md) — prepare/defend/reward phase orchestration around the spawner.
- [godot-signal-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md) — wave_started / enemy_died / currency_changed buses without frame polling.
- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — pooling, server bodies, and throttled acquire searches under heavy projectile counts.
- [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md) — sample wave DPS, leak rates, and economy bands before shipping difficulty curves.
#### Downstream / consumers
- [godot-genre-rts](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-rts/SKILL.md) — base defense and unit-placement loops that reuse path validation and economy pressure.
- [godot-multiplayer-networking](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md) — authoritative purchase validation and unreliable minion sync for co-op TD.
#### Master
- [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — library router and mirrored module entry for cross-skill discovery.