references/advanced-meta-systems.md
# Advanced shooter meta-systems
Load for lag compensation, explosion queries, and server rewind — pairs with [godot-adapt-single-to-multiplayer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md).
## Client prediction + server authority
```gdscript
# CLIENT — juice immediately
func fire_client() -> void:
play_effects_immediate()
local_tracer_only()
rpc_id(1, "server_validate_shot", camera.global_transform)
# SERVER — damage only here
@rpc("any_peer")
func server_validate_shot(xform: Transform3D) -> void:
var hit := perform_server_hitscan(xform)
if hit and is_valid_shot(hit):
rpc("confirm_hit", hit.victim_id, hit.damage)
```
- Predict tracers locally; never sync every bullet.
- Server wins on mismatch — show "no reg" feedback.
- Use ENet/UDP, not TCP, for fire events.
## Lag compensation (rewind)
Ring-buffer transforms; on shot, rewind targets to client timestamp, raycast, restore — [lag_compensator.gd](../scripts/lag_compensator.gd).
> **CAUTION:** Rewind, raycast, and restore in **one** physics frame — leaving bodies displaced breaks other server sim.
## Shape explosion query
Sphere `intersect_shape` without Area3D spam — [shooter_patterns.gd](../scripts/shooter_patterns.gd) pattern 3; also see explosion helper in patterns file.
## Hit zones
Use collider/shape names or bones for head/chest multipliers — avoid `==` on floats; use `is_equal_approx()` for damage math.
## Monte Carlo balance
Weapon matrices / TTK simulation → [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md).
references/migration-notes.md
# Migration notes: godot-genre-shooter
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)
- `Area2D.priority` type is `int` (was `float`).
- `PhysicsDirectSpaceState2D.collide_shape` returns `Array[Vector2]` (was `Array[PackedVector2Array]`).
- Viewports with Physics Picking enabled auto-mark InputEvents handled — adjust if you relied on unhandled propagation.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
*No skill-relevant breaking changes for this hop.*
## 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).
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- Android sensors disabled by default — enable under Project Settings → Input Devices → Sensors.
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
*No skill-relevant breaking changes for this hop.*
## 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.
## 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.
- `PhysicsServer2D.body_set_shape_as_one_way_collision` adds optional `direction` (relative to shape).
- `CollisionShape2D` one-way collision direction is relative to the shape, not only global up.
- Mouse/keyboard device IDs are `InputEvent.DEVICE_ID_MOUSE` / `DEVICE_ID_KEYBOARD` (not `0`) — joypads may use `0`.
references/tps-gunplay-core.md
# TPS / hybrid gunplay core
Shared combat theory for this skill (TPS, cover, soft-lock). FPS viewmodel/recoil polish → [godot-genre-shooter-fps](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md).
## WeaponData (never hardcode in nodes)
```gdscript
class_name WeaponData extends Resource
@export var damage: int = 20
@export var fire_rate: float = 0.1
@export var magazine_size: int = 30
@export var range: float = 100.0
@export var is_hitscan: bool = true
@export var projectile_scene: PackedScene
```
Balance in `.tres`, not scattered `@export` on scene logic.
## Hitscan vs projectile
| Mode | When | Script |
|------|------|--------|
| Hitscan | Rifles, SMGs — instant feedback | [shooter_patterns.gd](../scripts/shooter_patterns.gd), [advanced_weapon_controller.gd](../scripts/advanced_weapon_controller.gd) |
| Projectile | Rockets, arrows — gravity/bounce | [projectile.gd](../scripts/projectile.gd) |
Hitscan must run in `_physics_process` with `PhysicsDirectSpaceState3D.intersect_ray`, **not** `_process` or `Area3D` overlap.
```gdscript
var query := PhysicsRayQueryParameters3D.create(origin, origin + dir * range)
query.exclude = [shooter.get_rid()]
query.collision_mask = enemy_mask
var hit := space.intersect_ray(query)
```
## Recoil (three layers)
1. **Camera kick** — visual rotation, recover over time
2. **Spread bloom** — accuracy loss while firing
3. **Learnable pattern** — `Array[Vector2]` spray resource ([weapon_recoil_pattern.gd](../scripts/weapon_recoil_pattern.gd))
> **NEVER** apply recoil only to weapon mesh — players feel kick via camera + crosshair spread.
## TPS-specific
| System | Script |
|--------|--------|
| Over-shoulder camera | [tps_camera_spring_arm.gd](../scripts/tps_camera_spring_arm.gd) |
| Cover validity | [cover_validator_rays.gd](../scripts/cover_validator_rays.gd) |
| Soft-lock / assist | [soft_lock_aim_assist.gd](../scripts/soft_lock_aim_assist.gd) |
Controller assist: friction near targets + subtle magnetism — tune in soft-lock script; do not snap aim.
## Weapon balance heuristics
| Archetype | Feel |
|-----------|------|
| SMG | High ROF, low damage, tracking aim |
| Sniper | Low ROF, high damage, precision |
| Shotgun | Multi-pellet spread, <10m effective |
| AR | Medium all stats |
Hitscan for bullets; physical sim for explosives.
## Feel polish
- Layered gunfire audio (mechanical + shot + tail) on separate `AudioStreamPlayer3D` — [godot-audio-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-audio-systems/SKILL.md)
- Impact **Decal** nodes, not flat `Sprite3D` billboards
- Crosshair: anchor center, not pixel offsets
## Pitfalls
| Symptom | Fix |
|---------|-----|
| Weak impacts | Triple audio + shake + decal + damage number |
| Identical guns | Unique spray patterns per archetype |
| No skill ceiling | Learnable patterns, not pure RNG |
| Controller frustration | Soft-lock + friction, not zero assist |
scripts/advanced_weapon_controller.gd
# skills/genre-shooter/scripts/advanced_weapon_controller.gd
extends Node3D
## Advanced Weapon Controller
## Procedural Recoil, Bloom, and Hybrid Hitscan/Projectile Logic.
class_name AdvancedWeaponController
signal weapon_fired(current_ammo: int)
@export_group("Stats")
@export var fire_rate: float = 0.1
@export var max_ammo: int = 30
@export var damage: float = 25.0
@export var is_hitscan: bool = true
@export var projectile_scene: PackedScene
@export var projectile_speed: float = 50.0
@export_group("Recoil & Spread")
@export var recoil_kick: Vector2 = Vector2(0.5, 2.0) # Horizontal, Vertical (deg)
@export var recoil_recovery: float = 10.0 # deg/sec
@export var max_recoil_x: float = 5.0
@export var max_recoil_y: float = 10.0
@export var spread_per_shot: float = 0.5
@export var max_spread: float = 5.0
# Dependencies
@onready var camera: Camera3D = get_viewport().get_camera_3d()
# State
var current_ammo: int
var _fire_timer: float = 0.0
var _current_recoil: Vector2 = Vector2.ZERO
var _current_spread: float = 0.0
var _trigger_held: bool = false
func _ready() -> void:
current_ammo = max_ammo
func _process(delta: float) -> void:
_fire_timer -= delta
# Recoil Recovery
_current_recoil = _current_recoil.move_toward(Vector2.ZERO, recoil_recovery * delta)
_current_spread = move_toward(_current_spread, 0.0, recoil_recovery * delta)
# Apply visual rotation to camera (or weapon model)
if camera:
# Note: In real FPS, apply this as a separate offset/rotation to avoid drifting the actual view permanently
# For this snippet, we'll assume a 'recoil_container' or similar approach is best,
# but here is the logic for the offsets:
pass
func trigger_down() -> void:
_trigger_held = true
if _fire_timer <= 0:
_fire()
func trigger_up() -> void:
_trigger_held = false
func _fire() -> void:
if current_ammo <= 0: return # Play dry fire sound
current_ammo -= 1
_fire_timer = fire_rate
# calculate spread
var spread_angle = deg_to_rad(_current_spread)
var spread_vector = Vector3(randf_range(-spread_angle, spread_angle), randf_range(-spread_angle, spread_angle), 0)
if is_hitscan and camera:
var forward = -camera.global_transform.basis.z
# Apply spread rotation
var aim_dir = forward + camera.global_transform.basis * spread_vector
aim_dir = aim_dir.normalized()
# Raycast
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(camera.global_position, camera.global_position + aim_dir * 1000.0)
var result = space.intersect_ray(query)
if result:
if result.collider.has_method("take_damage"):
result.collider.take_damage(damage)
elif projectile_scene:
var proj = projectile_scene.instantiate()
get_tree().root.add_child(proj)
proj.global_transform = camera.global_transform
# Apply spread to projectile
proj.rotation.x += randf_range(-deg_to_rad(_current_spread), deg_to_rad(_current_spread))
proj.rotation.y += randf_range(-deg_to_rad(_current_spread), deg_to_rad(_current_spread))
# Apply Recoil kick
_current_recoil.x = clamp(_current_recoil.x + randf_range(-recoil_kick.x, recoil_kick.x), -max_recoil_x, max_recoil_x)
_current_recoil.y = clamp(_current_recoil.y + recoil_kick.y, 0, max_recoil_y) # Kick up
_current_spread = clamp(_current_spread + spread_per_shot, 0, max_spread)
weapon_fired.emit(current_ammo)
# Auto-fire logic
if _trigger_held and fire_rate > 0:
await get_tree().create_timer(fire_rate).timeout
if _trigger_held: _fire()
## EXPERT USAGE:
## Call trigger_down()/target_up() from Input.
## Bind 'current_recoil' to a CameraGL/SpringArm offset script for visual shake.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html
# - https://docs.godotengine.org/en/stable/classes/class_physicsrayqueryparameters3d.html
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md — damage pipeline after hitscan/projectile fire
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md — recoil kick applied to camera, not model only
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/cover_validator_rays.gd
extends Node3D
class_name CoverValidatorRays
## Expert Cover Detection (Godot 4.7).
## Uses a cluster of RayCasts to detect cover height and peeking.
@onready var ray_low: RayCast3D = $RayLow
@onready var ray_high: RayCast3D = $RayHigh
@onready var ray_left: RayCast3D = $RayLeft
@onready var ray_right: RayCast3D = $RayRight
enum CoverState { NONE, HALF, FULL }
func get_cover_state() -> CoverState:
if not ray_low.is_colliding(): return CoverState.NONE
if ray_high.is_colliding(): return CoverState.FULL
return CoverState.HALF
func can_peek_side() -> int:
# Returns -1 (Left), 1 (Right), or 0 (None)
if not ray_left.is_colliding(): return -1
if not ray_right.is_colliding(): return 1
return 0
## [SKILL NOTICE]: Cluster multiple RayCast3D nodes to detect
## environmental context (like cover height) in a single physics frame.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_raycast3d.html
# - https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md — multi-ray cover height / peek clusters
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-stealth/SKILL.md — cover state used beside detection loops
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/lag_compensator.gd
class_name LagCompensator
extends Node
## Server-side position history for rewind hit validation.
const MAX_BACKTRACK_MS := 200
var _history: Array[Dictionary] = [] # { "time": int, "transform": Transform3D }
func _physics_process(_delta: float) -> void:
_history.append({
"time": Time.get_ticks_msec(),
"transform": owner.global_transform
})
if _history.size() > 60:
_history.pop_front()
func backtrack_to(timestamp: int) -> void:
if _history.is_empty():
return
var best := _history[0]
for entry in _history:
if absi(entry.time - timestamp) < absi(best.time - timestamp):
best = entry
owner.global_transform = best.transform
func restore_latest() -> void:
if not _history.is_empty():
owner.global_transform = _history[-1].transform
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md — prediction shells
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md — RPC authority
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/projectile.gd
# skills/genre-shooter/scripts/projectile.gd
extends CharacterBody3D
## Projectile Expert Pattern
## Physical bullet with gravity, bounce, and lifetime management.
class_name Projectile
@export var speed: float = 50.0
@export var damage: int = 25
@export var gravity_scale: float = 1.0
@export var max_lifetime: float = 5.0
@export var bounces: int = 0
var _velocity: Vector3 = Vector3.ZERO
var _timer: float = 0.0
func setup(direction: Vector3, start_pos: Vector3) -> void:
look_at_from_position(start_pos, start_pos + direction)
_velocity = direction * speed
func _physics_process(delta: float) -> void:
_timer += delta
if _timer >= max_lifetime:
queue_free()
return
# Gravity
_velocity.y -= 9.8 * gravity_scale * delta
velocity = _velocity
var collision = move_and_collide(velocity * delta)
if collision:
_handle_collision(collision)
func _handle_collision(collision: KinematicCollision3D) -> void:
var collider = collision.get_collider()
if collider.has_method("take_damage"):
collider.take_damage(damage)
queue_free()
elif bounces > 0:
bounces -= 1
_velocity = _velocity.bounce(collision.get_normal())
# Reflect visual
look_at(global_position + _velocity)
else:
# Spawn impact effect
queue_free()
## EXPERT USAGE:
## Instantiate from WeaponController. Call setup().
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_characterbody3d.html
# - https://docs.godotengine.org/en/stable/classes/class_kinematiccollision3d.html
# - https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — move_and_collide projectile bodies and bounce
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md — take_damage on collider impact
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/shooter_patterns.gd
# shooter_patterns.gd
extends Node
# 1. Server-Bypassing Hitscan (PhysicsDirectSpaceState3D)
# EXPERT NOTE: Direct raycasting is faster than standard ray nodes for high-frequency fire.
func fire_hitscan(origin: Vector3, direction: Vector3, shooter_rid: RID) -> void:
var space_state := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(origin, origin + direction * 100)
query.exclude = [shooter_rid]
var result := space_state.intersect_ray(query)
if result and result.collider.has_method(&"take_damage"):
result.collider.call(&"take_damage", 10)
# 2. Applying Random Spread (Normal Distribution)
# EXPERT NOTE: randfn generates numbers clustered around the mean (0.0) for natural spread.
func calculate_spread(forward_vector: Vector3, spread_factor: float) -> Vector3:
var deviation := Vector3(randfn(0.0, spread_factor), randfn(0.0, spread_factor), 0)
return (forward_vector + deviation).normalized()
# 3. ShapeCast3D for AoE Explosions
# EXPERT NOTE: Efficient sphere casting to detect multiple targets for explosions.
func detonate_rocket(shape_rid: RID, pos: Transform3D) -> void:
var space_state := get_world_3d().direct_space_state
var query := PhysicsShapeQueryParameters3D.new()
query.shape_rid = shape_rid
query.transform = pos
var results := space_state.intersect_shape(query)
for hit in results:
if hit.collider.has_method(&"apply_impulse"):
hit.collider.apply_impulse(Vector3.UP * 10)
# 4. Spawning Bullet Hole Decals
# EXPERT NOTE: Use RenderingServer for low-overhead decal placement.
func spawn_decal(pos: Vector3, normal: Vector3) -> void:
var decal_rid := RenderingServer.decal_create()
RenderingServer.instance_set_transform(decal_rid, Transform3D(Basis(), pos))
# Additional setup: set texture, size, and world scenario
# 5. Projectile Server Instantiation (Bypassing Nodes)
# EXPERT NOTE: Create physics bodies directly in the server for bullet hell performance.
func spawn_server_bullet(transform: Transform3D) -> RID:
var body_rid := PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(body_rid, PhysicsServer3D.BODY_MODE_KINEMATIC)
PhysicsServer3D.body_set_space(body_rid, get_world_3d().space)
PhysicsServer3D.body_set_state(body_rid, PhysicsServer3D.BODY_STATE_TRANSFORM, transform)
return body_rid
# 6. Recoil Interpolation via Tweens
# EXPERT NOTE: Procedural recoil that smoothly returns to center.
func apply_recoil(camera: Camera3D, kickback: float) -> void:
var tween := create_tween()
tween.tween_property(camera, "rotation:x", camera.rotation.x + kickback, 0.05)
tween.tween_property(camera, "rotation:x", camera.rotation.x, 0.1)
# 7. AI Taking Cover (NavigationServer3D)
# EXPERT NOTE: Inform the navigation agent of its position manually for server-side AI.
func move_ai_to_cover(agent_rid: RID, cover_pos: Vector3, global_pos: Vector3) -> void:
NavigationServer3D.agent_set_position(agent_rid, global_pos)
# Target setting happens in background gen
NavigationServer3D.agent_set_velocity(agent_rid, (cover_pos - global_pos).normalized() * 5.0)
# 8. Server-Authoritative Firing
# EXPERT NOTE: Clients request; server validates and executes the shot.
@rpc("any_peer", "call_local", "reliable")
func request_fire(target_vector: Vector3) -> void:
if multiplayer.is_server():
var sender_id := multiplayer.get_remote_sender_id()
# Validate ammo, cooldown, and line-of-sight here
print("Server validated shot for player: ", sender_id)
# 9. Dynamic Weapon Crosshair Binding
# EXPERT NOTE: Use unbind(1) to connect signals with extra arguments to simple UI functions.
func connect_ui_effects(weapon_node: Node, crosshair_node: Node) -> void:
if weapon_node.has_signal(&"fired"):
weapon_node.connect(&"fired", crosshair_node.call.bind(&"expand").unbind(1))
# 10. Low-Latency Input Buffering
# EXPERT NOTE: Flush buffered events to ensure frame-perfect reaction to reload/fire.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed(&"reload"):
Input.flush_buffered_events()
# Trigger reload logic
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_physicsdirectspacestate3d.html
# - https://docs.godotengine.org/en/stable/classes/class_physicsshapequeryparameters3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_decals.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md — direct-space hitscan and shape AoE helpers
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md — server-side fire validation patterns
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/soft_lock_aim_assist.gd
extends Node
class_name SoftLockAimAssist
## Expert Aim Assist (Godot 4.7).
## Uses Dot Product to find targets and Slerp for smooth tracking.
@export var assist_strength: float = 4.0
@export var threshold: float = 0.97 # Cone of influence
@onready var camera: Camera3D = get_viewport().get_camera_3d()
func _physics_process(delta: float) -> void:
var target = _find_best_target()
if target:
_apply_soft_lock(target, delta)
func _find_best_target() -> Node3D:
var best_target: Node3D = null
var best_dot: float = -1.0
var forward = -camera.global_transform.basis.z
for enemy in get_tree().get_nodes_in_group("enemies"):
var dir = camera.global_position.direction_to(enemy.global_position)
var dot = forward.dot(dir)
if dot > threshold and dot > best_dot:
best_dot = dot
best_target = enemy
return best_target
func _apply_soft_lock(target: Node3D, delta: float) -> void:
var target_basis = Basis.looking_at(target.global_position - camera.global_position)
var current_q = camera.global_transform.basis.get_rotation_quaternion()
var target_q = target_basis.get_rotation_quaternion()
# Expert Pattern: Spherical linear interpolation for smooth rotation
var result_q = current_q.slerp(target_q, assist_strength * delta)
camera.global_transform.basis = Basis(result_q)
## [SKILL NOTICE]: Use 'dot product' to filter proximity to screen center,
## and 'slerp' for smooth aim-assist pull. Avoid hard-snapping rotation.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.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-input-handling/SKILL.md — stick look curves before soft-lock slerp
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md — assist rotates camera basis toward target
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/tps_camera_spring_arm.gd
extends SpringArm3D
class_name TPSCameraSpringArm
## Expert TPS Camera (Godot 4.7).
## Collision-aware SpringArm with dynamic shoulder swapping.
@export var shoulder_offset: float = 0.6
@onready var camera: Camera3D = get_child(0)
func _ready() -> void:
# Add the player to exclusion to prevent self-collision
add_excluded_object(get_parent().get_rid())
func swap_shoulder(to_right: bool) -> void:
var target = shoulder_offset if to_right else -shoulder_offset
# Expert Pattern: Use 'h_offset' to shift view without moving the collision ray
var tween = create_tween()
tween.tween_property(camera, "h_offset", target, 0.25).set_trans(Tween.TRANS_SINE)
## [SKILL NOTICE]: Use 'h_offset' on the Camera3D child of a SpringArm3D
## for shoulder swapping. This keeps the collision ray centered on the player.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/spring_arm.html
# - https://docs.godotengine.org/en/stable/classes/class_springarm3d.html
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md — SpringArm boom, exclusions, shoulder swap
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md — look input driving arm rotation
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
scripts/weapon_recoil_pattern.gd
class_name WeaponRecoilPattern
extends Resource
## Inspector-editable spray curve for learnable recoil (CS-style).
@export var spray_points: Array[Vector2] = []
@export var horizontal_variance: float = 0.1
@export var vertical_variance: float = 0.1
func get_recoil_at(shot_index: int) -> Vector2:
if spray_points.is_empty():
return Vector2.ZERO
var base := spray_points[shot_index % spray_points.size()]
return base + Vector2(
randf_range(-horizontal_variance, horizontal_variance),
randf_range(-vertical_variance, vertical_variance)
)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_resource.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md — WeaponData resources
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md — FPS recoil polish
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md
# =============================================================================
SKILL.md
---
name: godot-genre-shooter
description: "Expert blueprint for TPS/hybrid shooters: soft-lock aim assist, cover validation rays, SpringArm TPS camera, and genre routing to FPS sibling for viewmodel/hitscan feel. Use for third-person/cover shooters and hybrids. Keywords: TPS, soft_lock, cover, SpringArm3D, hybrid shooter, aim assist — not FPS-only movement."
---
## Core Loop
`Engage → Aim (soft-lock/cover) → Fire → Confirm → Acquire Next`
## NEVER Do (TPS / hybrid)
- **NEVER put FPS viewmodel/bob/look recipes in this skill** — route to shooter-fps.
- **NEVER use `_process()` for hit registration** — `_physics_process` + space-state queries.
- **NEVER trust the client for authoritative hits** — server validate / rewind.
- **NEVER use Area3D overlap for bullets** — ray / shape queries.
- **NEVER hardcode weapon stats in logic** — `WeaponData` Resources.
- **NEVER skip excluding the shooter's RID** on queries.
- **NEVER use TCP for shooter net sync** — ENet/UDP.
## MANDATORY scripts (non-FPS paths)
> Read before implementing the matching system:
1. [soft_lock_aim_assist.gd](scripts/soft_lock_aim_assist.gd) — controller/hybrid assist & sticky aim
2. [cover_validator_rays.gd](scripts/cover_validator_rays.gd) — cover / peek validity rays
3. [tps_camera_spring_arm.gd](scripts/tps_camera_spring_arm.gd) — SpringArm3D TPS camera collision
Also available: [advanced_weapon_controller.gd](scripts/advanced_weapon_controller.gd) (shared weapon controller — prefer FPS skill for FPS feel), [shooter_patterns.gd](scripts/shooter_patterns.gd), [projectile.gd](scripts/projectile.gd), [weapon_recoil_pattern.gd](scripts/weapon_recoil_pattern.gd) (spray Resource), [lag_compensator.gd](scripts/lag_compensator.gd) (server rewind).
## Decision trees
### Camera / stance
| Need | Action |
|---|---|
| Over-shoulder TPS | [tps_camera_spring_arm.gd](scripts/tps_camera_spring_arm.gd) |
| Cover check before peek-fire | [cover_validator_rays.gd](scripts/cover_validator_rays.gd) |
| Soft-lock / assist | [soft_lock_aim_assist.gd](scripts/soft_lock_aim_assist.gd) |
| True FPS digsite | → [godot-genre-shooter-fps](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md) |
### Fire mode
| Need | Action |
|---|---|
| Hitscan / projectile theory | Shared WeaponData + space-state; FPS implementation details in shooter-fps |
| Pooled projectiles | [projectile.gd](scripts/projectile.gd) / patterns script |
| Explosion radius | ShapeCast3D pattern in [shooter_patterns.gd](scripts/shooter_patterns.gd) |
### Net
| Need | Action |
|---|---|
| Client juice | Predict tracers locally |
| Damage | Server authoritative; show no-reg on mismatch |
| Net rewind / lag comp | [lag_compensator.gd](scripts/lag_compensator.gd) + [advanced-meta-systems.md](references/advanced-meta-systems.md) |
| Learnable spray | [weapon_recoil_pattern.gd](scripts/weapon_recoil_pattern.gd) |
## Weapon selection (short)
Hitscan for rifles/SMGs; physical projectiles for rockets/arrows; balance in **`WeaponData` Resources** (`.tres`), never hardcoded on nodes.
| Archetype | ROF | Damage | Implementation |
|-----------|-----|--------|----------------|
| SMG | High | Low | Hitscan + tight vertical spray |
| Sniper | Low | High | Hitscan + tracer |
| Shotgun | Burst | Medium | Multi-pellet spread, <10m |
| Rocket | Slow | High | [projectile.gd](scripts/projectile.gd) + shape AoE |
Gunplay theory → [tps-gunplay-core.md](references/tps-gunplay-core.md). Net rewind → [advanced-meta-systems.md](references/advanced-meta-systems.md).
## Recoil and feel (WHY)
Apply kick to **camera rotation** and **spread bloom**, not weapon mesh alone. Learnable spray via [weapon_recoil_pattern.gd](scripts/weapon_recoil_pattern.gd). Layer gunfire: mechanical + shot + reverb tail on separate 3D players.
## Multiplayer (short)
Client predicts VFX/tracers; server validates hits (`rpc` + rewind). Never trust client damage. ENet/UDP, not TCP. Server wins on mismatch — show no-reg feedback.
## 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
- [Ray-casting](https://docs.godotengine.org/en/stable/tutorials/physics/ray-casting.html) — `PhysicsDirectSpaceState3D.intersect_ray` hitscan queries, exceptions, and collision masks for frame-rate-independent fire.
- [Physics introduction](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html) — layers/masks, `_physics_process` timing, and body types that keep hit registration deterministic.
- [PhysicsDirectSpaceState3D](https://docs.godotengine.org/en/stable/classes/class_physicsdirectspacestate3d.html) — direct `intersect_ray` / `intersect_shape` for high-frequency shots without RayCast3D node overhead.
- [PhysicsRayQueryParameters3D](https://docs.godotengine.org/en/stable/classes/class_physicsrayqueryparameters3d.html) — from/to, exclude RIDs, and collision_mask for shooter-owned hitscan queries.
- [CharacterBody3D](https://docs.godotengine.org/en/stable/classes/class_characterbody3d.html) — `move_and_collide` projectile bodies with gravity, bounce, and lifetime.
- [SpringArm](https://docs.godotengine.org/en/stable/tutorials/3d/spring_arm.html) — collision-aware TPS camera boom and shoulder offset without clipping into cover.
- [Using decals](https://docs.godotengine.org/en/stable/tutorials/3d/using_decals.html) — perspective-correct bullet holes and impact marks instead of Sprite3D billboards.
- [Controllers, gamepads, and joysticks](https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html) — stick look input that aim-assist friction/magnetism modulates.
- [High-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html) — RPC authority and unreliable modes for fire events with server-validated hits.
- [Camera3D](https://docs.godotengine.org/en/stable/classes/class_camera3d.html) — FOV punch, aim rays from `-global_basis.z`, and `h_offset` for TPS shoulder swap.
- [PhysicsShapeQueryParameters3D](https://docs.godotengine.org/en/stable/classes/class_physicsshapequeryparameters3d.html) — sphere/shape queries for rocket/grenade AoE without Area3D spam.
- [AudioStreamPlayer3D](https://docs.godotengine.org/en/stable/classes/class_audiostreamplayer3d.html) — layered shot/mechanical/tail players for punchy spatial gunfire.
### Related Skills
#### Prerequisites
- [godot-physics-3d](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md) — CharacterBody3D/RigidBody3D, collision layers, and direct space queries that hitscan and projectiles depend on.
- [godot-input-handling](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md) — look/fire/reload action maps and gamepad stick curves before aim assist and recoil kick.
- [godot-camera-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md) — FPS/TPS camera rigs, FOV, and SpringArm patterns that weapon kick and soft-lock rotate.
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — Resource-based WeaponData, scene structure, and import defaults before balancing archetypes.
#### Complements
- [godot-combat-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md) — damage events, hit zones, and health pipelines that consume shooter `take_damage` results.
- [godot-raycasting-queries](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md) — reusable ray/shape query helpers for cover checks, LOS, and hitscan without duplicating query setup.
- [godot-audio-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-audio-systems/SKILL.md) — bus routing and 3D attenuation for mechanical + shot + reverb-tail gunfire layers.
- [godot-multiplayer-networking](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-multiplayer-networking/SKILL.md) — ENet peers, RPC modes, and authority so fire is predicted client-side and damage is server-validated.
- [godot-adapt-single-to-multiplayer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md) — prediction, reconciliation, and lag-compensation shells around hitscan validation.
- [godot-particles](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md) — muzzle flash, tracers, and impact bursts that sell gunplay without blocking the fire path.
#### Downstream / consumers
- [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md) — simulate TTK, spray patterns, and weapon asymmetry matrices so archetype damage/recoil stay competitive.
- [godot-genre-shooter-fps](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter-fps/SKILL.md) — FPS-specialized movement and viewmodel polish that builds on this genre gunplay lattice.
- [godot-genre-battle-royale](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-battle-royale/SKILL.md) — large-scale matches that reuse hitscan/projectile combat inside drop/zone loops.
#### 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.