| name | godot-style-guide |
| description | Provides the strict, official style guide for all GDScript code generation, including naming conventions (snake_case, PascalCase) and mandatory static typing. This skill should be used when writing or reviewing GDScript code to ensure consistency with Godot best practices and Wing Commander Saga migration standards. |
| version | 1.0.0 |
GDScript Style Guide - Wing Commander Saga Migration
This guide defines the mandatory coding standards for all GDScript code generated during the Wing Commander Saga migration. Adherence to these standards ensures code quality, maintainability, and compatibility with Godot 4.x.
1. Naming Conventions (Mandatory)
Class Names and Enums
- Must use PascalCase for class_name and enum declarations
- Use descriptive names that clearly indicate purpose and domain
- Avoid abbreviations unless widely understood in aerospace/gaming context
- Include domain context when names might be ambiguous
# Ship-related classes
class_name ShipController
class_name WeaponSystem
class_name ShieldGenerator
class_name EngineSystem
# AI-related classes
class_name AIStateController
class_name BehaviorTreeNode
class_name PatrolAIController
# Data structures
class_name ShipStats
class_name WeaponData
class_name MissionParameters
# Enums with clear semantic meaning
enum ShipState {
IDLE,
PATROL,
COMBAT,
RETREAT,
DOCKED,
DISABLED
}
enum WeaponType {
PRIMARY_LASER,
SECONDARY_MISSILE,
BEAM_WEAPON,
FLAK_CANNON
}
enum ThreatLevel {
NONE,
LOW,
MEDIUM,
HIGH,
CRITICAL
}
Functions and Variables
- Must use snake_case for all function and variable names
- Use descriptive verbs for functions (action-oriented naming)
- Use descriptive nouns for variables (data-oriented naming)
- Private members use leading underscore
- Boolean variables should use is_, has_, can_, or should_ prefixes
# Functions - clear verb-noun patterns
func calculate_intercept_course(target_position: Vector3, target_velocity: Vector3) -> Vector3:
pass
func fire_weapon_at_target(weapon_index: int, target: Node3D) -> bool:
pass
func apply_damage_to_subsystem(subsystem_name: String, damage_amount: float) -> void:
pass
func get_ship_status() -> Dictionary:
pass
# Variables - clear noun descriptors
var current_health: float
var target_velocity: Vector3
var weapon_lock_time: float
# Boolean variables with clear prefixes
var is_engaged: bool
var has_target_lock: bool
var can_fire: bool
var should_evade: bool
var shields_active: bool
# Private variables with underscore prefix
var _internal_state: Dictionary
var _ai_update_timer: float
var _weapon_cooldowns: Dictionary
Constants
- Must use CONSTANT_CASE (all uppercase with underscores)
- Use descriptive names that indicate the constant's purpose
- Group related constants with logical organization
- Include units in names where applicable (when not obvious from context)
# Physics constants
const MAX_VELOCITY: float = 100.0
const DEFAULT_TURN_RATE: float = 2.0
const GRAVITY_ACCELERATION: float = 9.81
# Combat constants
const MAX_SHIELD_STRENGTH: float = 1000.0
const WEAPON_COOLDOWN_TIME: float = 0.5
const MISSILE_LOCK_TIME: float = 2.0
const COLLISION_DAMAGE_MULTIPLIER: float = 1.5
# UI constants
const RADAR_RANGE_KM: int = 50
const HUD_UPDATE_FREQUENCY: float = 30.0
const WARNING_FLASH_DURATION: float = 0.5
# Configuration constants
const MAX_AI_WAYPOINTS: int = 10
const SAVE_GAME_INTERVAL: float = 300.0
const NETWORK_TIMEOUT_SECONDS: int = 30
Signals
- Use past tense or event-based naming conventions
- Include parameter types in signal declarations
- Use clear, descriptive names that indicate what happened
- Group related signals with consistent prefixes
# Combat events
signal weapon_fired(weapon_index: int, projectile_position: Vector3)
signal target_locked(target_node: Node3D, lock_strength: float)
signal missile_launched(missile_type: String, target_position: Vector3)
signal shields_hit(damage_amount: float, impact_position: Vector3)
# State change events
signal health_changed(new_health: float, old_health: float)
signal shield_status_changed(active: bool, current_strength: float)
signal engine_status_changed(status: String, thrust_percentage: float)
signal weapon_status_changed(weapon_index: int, status: String)
# Mission events
signal objective_completed(objective_id: String, completion_time: float)
signal waypoint_reached(waypoint_index: int)
signal mission_failed(reason: String, failure_time: float)
signal cargo_delivered(cargo_type: String, destination: String)
# AI events
signal ai_state_changed(old_state: String, new_state: String)
signal target_acquired(target_node: Node3D, threat_level: int)
signal formation_changed(formation_type: String, leader_node: Node3D)
2. Static Typing (Mandatory)
All variables, function parameters, and return values must use static typing to improve code clarity, enable IDE support, and reduce runtime errors.
Variable Typing Rules
- Always specify types for variable declarations
- Use specific types (Vector3, String, int) instead of Variant when possible
- Use collection types with element typing (Array[String], Dictionary[String, int])
- Cast types explicitly when using get_node() or similar functions
# Good - Explicit and specific typing
var player_ship: ShipController = get_node("../PlayerShip") as ShipController
var weapon_systems: Array[WeaponSystem] = []
var ship_stats: ShipStats = load("res://resources/ships/apollo.tres") as ShipStats
var target_list: Array[Node3D] = []
var configuration_data: Dictionary[String, Variant] = {}
# Variables with inferred types from literals
const DEFAULT_HEALTH: float = 100.0
var ship_name: String = "GTF Apollo"
var is_destroyed: bool = false
var crew_count: int = 1
# Bad - Missing or vague typing
var player_ship = get_node("../PlayerShip") # Type is Node, not ShipController
var weapon_systems = [] # Array of unknown type
var ship_stats # No type specified, defaults to Variant
var config_data = {} # Dictionary with unknown key/value types
Function Signature Typing
- All parameters must have explicit types
- Return types must be declared for all functions except _ready(), _process(), etc.
- Use void return type for functions that don't return values
- Use Optional types (?) for parameters that can be null
# Good - Complete typing with documentation
func fire_weapon(weapon_index: int, target_position: Vector3) -> bool:
"""Fires the specified weapon at the target position.
Args:
weapon_index: Index of the weapon to fire (0-based)
target_position: World position to aim at
Returns:
True if weapon was fired successfully, False otherwise
"""
if weapon_index < 0 or weapon_index >= weapons.size():
return false
var weapon: Weapon = weapons[weapon_index]
return weapon.fire(target_position)
# Function with optional parameter
func set_target(target: Node3D = null) -> void:
current_target = target
# Function returning custom type
func get_weapon_status(weapon_index: int) -> WeaponStatus:
if weapon_index >= weapons.size():
return WeaponStatus.INVALID
return weapons[weapon_index].get_status()
# Bad - Missing or incomplete typing
func fire_weapon(weapon_index, target_position): # No parameter types
if weapon_index < 0 or weapon_index >= weapons.size():
return False # Should be bool return type
var weapon = weapons[weapon_index] # Weapon type not specified
return weapon.fire(target_position) # Return type unknown
Collection Typing Best Practices
- Use strongly typed collections whenever possible
- Use Dictionary with specific key and value types
- Use Array with specific element types
- Consider using TypedArray for custom types
# Good - Strongly typed collections
var weapon_list: Array[Weapon] = []
var ship_properties: Dictionary[String, Variant] = {}
var target_list: Array[Node3D] = []
var weapon_cooldowns: Dictionary[String, float] = {}
var mission_objectives: Array[MissionObjective] = []
# Using PackedStringArray for performance
var ship_tags: PackedStringArray = ["fighter", "terran", "player"]
var available_weapons: PackedStringArray = ["laser", "missile", "beam"]
# Complex nested structures
var squadron_composition: Dictionary[String, Array[String]] = {
"alpha": ["apollo", "zeus", "hercules"],
"beta": ["apollo", "apollo"]
}
# Bad - Generic collections lose type safety
var weapon_list = [] # Could contain anything
var ship_properties = {} # Key/value types unknown
var target_list = [] # Element type not specified
3. Node Path Best Practices
Avoid Absolute Paths
Do not use absolute node paths as they break when scenes are restructured:
# Bad - Brittle absolute paths that break with scene changes
@onready var player = get_node("/root/World/PlayerShip/Controller")
@onready var weapon = get_node("/root/World/PlayerShip/Weapons/Primary")
@onready var radar = get_node("/root/Game/UI/HUD/RadarDisplay")
Use Relative Paths and @onready
Use relative paths that are resilient to scene changes:
# Good - Resilient relative paths
@onready var player_ship = get_node("../PlayerShip")
@onready var primary_weapons = get_node("Weapons/Primary")
@onready var health_component = get_node("Systems/Health")
# Best - @onready with unique node paths (% prefix)
@onready var cockpit_camera: Camera3D = %CockpitCamera
@onready var radar_system: RadarSystem = %RadarSystem
@onready var damage_receiver: Area3D = %DamageReceiver
@onready var engines: EngineSystem = %Engines
Node Path Safety Patterns
Always include type casting and null checks when using node paths:
# Good - Safe node reference with type casting
@onready var weapon_system: WeaponSystem = %WeaponSystem as WeaponSystem
@onready var ai_controller: AIController = %AIController as AIController
func _ready() -> void:
# Verify critical node connections
if not weapon_system:
push_error("WeaponSystem not found in " + name)
return
if not ai_controller:
push_error("AIController not found in " + name)
return
# Safe to use now
weapon_system.weapon_fired.connect(_on_weapon_fired)
ai_controller.target_changed.connect(_on_target_changed)
4. Signal Handling Best Practices
Modern Signal Syntax
Must use the signal_name.connect(callable) syntax, not deprecated string-based methods:
# Good - Modern syntax with type checking and binding
health_changed.connect(_on_health_changed.bind(self))
target_acquired.connect(_on_target_acquired)
weapon_fired.connect(_on_weapon_fired)
# With additional context binding
damage_taken.connect(_on_damage_taken.bind(self, ship_name))
mission_completed.connect(_on_mission_completed.bind(current_mission_id))
# Bad - Deprecated string syntax (no type checking)
health_changed.connect(self, "_on_health_changed")
target_acquired.connect(self, "_on_target_acquired")
weapon_fired.connect(self, "_on_weapon_fired")
Signal Callback Naming Convention
Use the on
# Signal declarations
signal shield_absorbed(damage: float, impact_normal: Vector3)
signal engines_damaged(damage_percentage: float)
signal missile_lock_acquired(target: Node3D, lock_time: float)
# Corresponding callbacks
func _on_shield_absorbed(damage: float, impact_normal: Vector3) -> void:
shield_strength -= damage
shield_impact_effect.emit(impact_normal)
func _on_engines_damaged(damage_percentage: float) -> void:
max_velocity *= (1.0 - damage_percentage)
engine_damage_effect.emit(damage_percentage)
func _on_missile_lock_acquired(target: Node3D, lock_time: float) -> void:
current_missile_target = target
missile_warning_sound.emit()
Signal Disconnection and Cleanup
Always disconnect signals when nodes are being removed to prevent memory leaks:
func _exit_tree() -> void:
# Disconnect all connected signals
if health_changed.is_connected(_on_health_changed):
health_changed.disconnect(_on_health_changed)
if target_acquired.is_connected(_on_target_acquired):
target_acquired.disconnect(_on_target_acquired)
# Disconnect from external nodes
if game_manager:
if game_manager.mission_started.is_connected(_on_mission_started):
game_manager.mission_started.disconnect(_on_mission_started)
5. Script File Structure (Mandatory)
Follow this standard order for all GDScript files to ensure consistency and readability:
Standard File Organization
- Header comments (tool mode, file description)
- class_name and extends
- Signals
- Enums
- Constants
- @export variables
- @onready variables
- Public variables
- Private variables
- _ready() function
- _process() and _physics_process()
- Public methods
- Private methods
- Signal callbacks
Complete Example Structure
# =============================================================================
# Wing Commander Saga - Ship Controller
# Controls ship movement, combat systems, and state management
# =============================================================================
@tool
extends Node3D
class_name ShipController
# =============================================================================
# Signals
# =============================================================================
signal health_changed(new_health: float, old_health: float)
signal target_locked(target: Node3D, lock_strength: float)
signal weapon_fired(weapon_index: int, projectile_type: String)
signal shields_status_changed(active: bool, current_strength: float)
# =============================================================================
# Enums
# =============================================================================
enum ShipState {
IDLE,
PATROL,
COMBAT,
EVADING,
DOCKED,
DISABLED
}
enum WeaponStatus {
READY,
RELOADING,
DAMAGED,
OUT_OF_AMMO
}
# =============================================================================
# Constants
# =============================================================================
const MAX_HEALTH: float = 1000.0
const MAX_SHIELD_STRENGTH: float = 500.0
const DEFAULT_TURN_RATE: float = 2.0
const WEAPON_COOLDOWN_TIME: float = 0.5
# =============================================================================
# @export Variables (Editor Properties)
# =============================================================================
@export var max_speed: float = 100.0
@export var acceleration: float = 50.0
@export var turn_speed: float = 2.0
@export var ship_stats: ShipStats
# =============================================================================
# @onready Variables (Node References)
# =============================================================================
@onready var engines: EngineSystem = %Engines as EngineSystem
@onready var weapons: WeaponSystem = %Weapons as WeaponSystem
@onready var health_component: HealthComponent = %HealthComponent as HealthComponent
@onready var shield_generator: ShieldGenerator = %ShieldGenerator as ShieldGenerator
@onready var radar_system: RadarSystem = %RadarSystem as RadarSystem
# =============================================================================
# Public Variables
# =============================================================================
var current_state: ShipState = ShipState.IDLE
var current_target: Node3D = null
var current_health: float = MAX_HEALTH
var current_shield_strength: float = MAX_SHIELD_STRENGTH
# =============================================================================
# Private Variables
# =============================================================================
var _internal_velocity: Vector3 = Vector3.ZERO
var _state_timer: float = 0.0
var _weapon_cooldowns: Dictionary[String, float] = {}
var _ai_decision_timer: float = 0.0
# =============================================================================
# _ready() Function
# =============================================================================
func _ready() -> void:
# Initialize systems
current_health = ship_stats.max_health
current_shield_strength = ship_stats.max_shield_strength
# Connect signals
health_component.health_changed.connect(_on_health_changed)
shield_generator.shields_changed.connect(_on_shields_changed)
weapons.weapon_fired.connect(_on_weapon_fired)
# Initialize weapon cooldowns
_initialize_weapon_cooldowns()
# =============================================================================
# _process() and _physics_process()
# =============================================================================
func _process(delta: float) -> void:
_update_state(delta)
_update_weapon_cooldowns(delta)
func _physics_process(delta: float) -> void:
_update_physics(delta)
# =============================================================================
# Public Methods
# =============================================================================
func set_target(target: Node3D) -> void:
"""Sets the current target and transitions to combat state.
Args:
target: The node to target, or null to clear target
"""
current_target = target
if target:
current_state = ShipState.COMBAT
target_locked.emit(target, 1.0)
else:
current_state = ShipState.PATROL
func engage_thrusters(direction: Vector3, intensity: float) -> void:
"""Engages ship thrusters in specified direction.
Args:
direction: Normalized direction for thrust
intensity: Thrust intensity from 0.0 to 1.0
"""
engines.apply_thrust(direction * intensity * max_speed)
func fire_primary_weapon(weapon_index: int) -> bool:
"""Fires the specified primary weapon.
Args:
weapon_index: Index of the weapon to fire
Returns:
True if weapon was fired successfully
"""
return weapons.fire_primary(weapon_index)
# =============================================================================
# Private Methods
# =============================================================================
func _update_state(delta: float) -> void:
match current_state:
ShipState.IDLE:
_handle_idle_state(delta)
ShipState.PATROL:
_handle_patrol_state(delta)
ShipState.COMBAT:
_handle_combat_state(delta)
ShipState.EVADING:
_handle_evading_state(delta)
func _handle_combat_state(delta: float) -> void:
if current_target:
# Calculate intercept course and engage
var intercept_pos: Vector3 = _calculate_intercept_position(current_target)
look_at(intercept_pos, Vector3.UP)
# Check weapon status
_evaluate_weapon_status()
func _calculate_intercept_position(target: Node3D) -> Vector3:
"""Calculates the intercept position for a moving target."""
# Implementation details...
return target.global_position
func _initialize_weapon_cooldowns() -> void:
"""Initializes weapon cooldown tracking."""
for i in range(ship_stats.primary_banks):
_weapon_cooldowns["primary_" + str(i)] = 0.0
func _update_weapon_cooldowns(delta: float) -> void:
"""Updates all weapon cooldown timers."""
for weapon_id in _weapon_cooldowns:
if _weapon_cooldowns[weapon_id] > 0:
_weapon_cooldowns[weapon_id] -= delta
# =============================================================================
# Signal Callbacks
# =============================================================================
func _on_health_changed(new_health: float, old_health: float) -> void:
current_health = new_health
health_changed.emit(new_health, old_health)
if new_health <= 0.0:
current_state = ShipState.DISABLED
_handle_ship_destruction()
func _on_shields_changed(active: bool, current_strength: float) -> void:
current_shield_strength = current_strength
shields_status_changed.emit(active, current_strength)
func _on_weapon_fired(weapon_index: int, projectile_type: String) -> void:
weapon_fired.emit(weapon_index, projectile_type)
func _handle_ship_destruction() -> void:
"""Handles ship destruction effects and cleanup."""
# Create explosion effect
# Drop cargo if applicable
# Update mission status
pass
6. Documentation Standards
Public Method Documentation
All public methods must have comprehensive documentation:
## Calculates the optimal firing solution for the given target.
##
## This function computes the intercept point considering target velocity,
## projectile speed, and ship positioning. Uses quadratic equation solving
## for time-to-intercept calculation.
##
## @param target_node: The target ship to calculate firing solution for
## @param weapon_index: Which weapon to use for the calculation
## @return: Dictionary containing firing solution data or empty dict if no solution
## - "position": Vector3 - Aim position in world space
## - "time_to_impact": float - Time until projectile hits target
## - "hit_probability": float - Probability of successful hit (0.0-1.0)
func calculate_firing_solution(target_node: Node3D, weapon_index: int) -> Dictionary:
# Implementation...
pass
Complex Algorithm Documentation
Add inline comments for complex mathematical or logical algorithms:
# Calculate intercept point using relative motion equations
# We need to solve: |target_pos + target_vel * t - shooter_pos - shooter_vel * t| = projectile_speed * t
# This simplifies to a quadratic equation in t
var relative_pos: Vector3 = target.global_position - global_position
var relative_vel: Vector3 = target.velocity - current_velocity
var projectile_speed: float = weapons[weapon_index].projectile_speed
# Quadratic coefficients: at² + bt + c = 0
var a: float = relative_vel.length_squared() - projectile_speed * projectile_speed
var b: float = 2.0 * relative_pos.dot(relative_vel)
var c: float = relative_pos.length_squared()
# Calculate discriminant to check if solution exists
var discriminant: float = b * b - 4.0 * a * c
if discriminant < 0:
return {} # No intercept solution possible
# Calculate time to intercept (positive root only)
var time_to_intercept: float = (-b + sqrt(discriminant)) / (2.0 * a)
if time_to_intercept < 0:
return {} # Target is moving away too fast
7. Error Handling and Safety
Type Safety and Casting
Always use type casting with safety checks:
func set_pilot(pilot_node: Node) -> void:
"""Sets the ship's pilot with type safety validation."""
var pilot: PilotController = pilot_node as PilotController
if not pilot:
push_error("Invalid pilot node assigned to " + name +
". Expected PilotController, got " + pilot_node.get_class())
return
# Additional validation
if not pilot.is_qualified_for_ship(ship_stats.ship_class):
push_warning("Pilot " + pilot.pilot_name + " is not qualified for " + ship_class)
return
current_pilot = pilot
pilot_assigned.emit(pilot)
Null Safety and Optional Types
Check for null before accessing optional nodes:
func fire_primary_weapons() -> void:
"""Fires all primary weapons with null safety checks."""
if not weapons:
push_warning("Weapons system not available on " + name)
return
if not current_target:
push_warning("No target locked for " + name)
return
for i in range(weapons.primary_count):
if not _can_fire_weapon(i):
continue
var result: bool = weapons.fire_primary(i, current_target.global_position)
if result:
_weapon_cooldowns["primary_" + str(i)] = weapons[i].cooldown_time
Error Recovery Patterns
Implement graceful error recovery for critical systems:
func _handle_system_failure(system_name: String, error_code: int) -> void:
"""Handles system failures with appropriate fallback behaviors."""
match system_name:
"weapons":
# Switch to backup weapons or disable combat
weapons_available = false
system_failure.emit("weapons", "Switching to defensive protocols")
"engines":
# Reduce thrust, enable emergency thrusters
max_speed *= 0.5
emergency_thrusters_active = true
system_failure.emit("engines", "Emergency thrusters activated")
"shields":
# Reinforce armor, alert player
shield_generator.active = false
armor_reinforced = true
system_failure.emit("shields", "Armor reinforcement activated")
_:
push_error("Unknown system failure: " + system_name)
This comprehensive style guide ensures that all GDScript code generated during the Wing Commander Saga migration follows consistent, maintainable, and high-quality standards. Adherence to these guidelines is mandatory for all generated code.