
Godot Turn System
- 174 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-turn-system for development tasks
About
godot-turn-system: A skill for development. This provides functionality for development workflows.
- godot-turn-system
Godot Turn System by the numbers
- 174 all-time installs (skills.sh)
- +21 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,250 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-turn-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 174 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-turn-system for development tasks
Files
Turn System
Turn order calculation, action points, phase management, and timeline systems define turn-based combat.
NEVER Do (Expert Anti-Patterns)
Order & Determinism
- NEVER recalculate turn order every action; strictly sort once per round or ONLY when a speed-relevant stat changes to prevent O(n log n) lag.
- NEVER use random tie-breaking for initiative; strictly use a secondary static attribute (Agility, ID, or persistent "luck") for deterministic replays.
- NEVER modify an active turn-order queue while iterating it; strictly iterate over a
duplicate()or apply queue modifications after the loop. - NEVER broadcast global turn state changes using immediate
call_group(); strictly use `call_group_flags(SceneTree.GROUP_CALL_DEFERRED, ...)` to prevent frame spikes when notifying hundreds of units.
- NEVER rely on the Node hierarchy as the source of truth; strictly use a Dictionary board state for logical grid coordinates.
Logic & Action Economy
- NEVER deduct Action Points (AP) before validation; strictly call
can_perform_action(cost)before applyingcurrent_ap -= costto prevent exploits. - NEVER hardcode phase transitions (
if phase == 0); strictly use an enum + match or a dedicated State Machine for Draw/Main/End phases. - NEVER emit "Turn Ended" before internal cleanup; strictly reset AP and tick status effects BEFORE signaling the next turn.
- NEVER use exact floating-point equality (
==) for AP checks; strictly use>=oris_equal_approx()for robust comparisons.
Tactical Grid & UI
- NEVER use generic
AStar2Dfor tile grids; strictly use `AStarGrid2D` for 10x faster pathfinding and native diagonal handling. - NEVER forget to call `update()` on
AStarGrid2Dafter changing obstacle states; if you toggleset_point_solid(), the grid MUST refresh before the next query. - NEVER lock the main thread with
whileloops for input; strictly use the await keyword or signals to yield execution back to the Tree. - NEVER handle turn decisions with
is_action_pressed(); strictly useis_action_just_pressed()for discrete, frame-locked menu input. - NEVER skip turn timeouts in networked games; strictly implement a server-side timer with a default "pass" action to prevent griefing.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- active_time_battle.gd - Framework for ATB systems with dynamic progress bars and async action support.
- timeline_turn_manager.gd - Advanced manager for timeline-based turns with interrupts and predictive visualization.
Modular Components
- turn_system_patterns.gd - Collection of patterns for match state machines, UndoRedo, and A* Grid setup.
---
# turn_manager.gd (AutoLoad)
extends Node
signal turn_started(combatant: Node)
signal turn_ended(combatant: Node)
signal round_ended
var combatants: Array[Node] = []
var turn_order: Array[Node] = []
var current_turn_index: int = 0
func start_combat(participants: Array[Node]) -> void:
combatants = participants
calculate_turn_order()
start_next_turn()
func calculate_turn_order() -> void:
turn_order = combatants.duplicate()
turn_order.sort_custom(func(a, b): return a.speed > b.speed)
func start_next_turn() -> void:
if current_turn_index >= turn_order.size():
current_turn_index = 0
round_ended.emit()
calculate_turn_order() # Recalculate each round
var current := turn_order[current_turn_index]
turn_started.emit(current)
func end_turn() -> void:
var current := turn_order[current_turn_index]
turn_ended.emit(current)
current_turn_index += 1
start_next_turn()Action Point System
# combatant.gd
extends Node
@export var max_action_points: int = 3
var current_action_points: int = 3
func start_turn() -> void:
current_action_points = max_action_points
func can_perform_action(cost: int) -> bool:
return current_action_points >= cost
func perform_action(cost: int) -> bool:
if not can_perform_action(cost):
return false
current_action_points -= cost
return trueTurn Phases
enum Phase { DRAW, MAIN, END }
var current_phase: Phase = Phase.DRAW
func advance_phase() -> void:
match current_phase:
Phase.DRAW:
current_phase = Phase.MAIN
Phase.MAIN:
current_phase = Phase.END
Phase.END:
TurnManager.end_turn()
current_phase = Phase.DRAWBest Practices
1. Speed-Based - Initiative determines order 2. Action Points - Limit actions per turn 3. Timeout - Add turn timer for online play
---
Elite Godot 4.x Patterns
1. Active Time Battle (ATB) Implementation
Track time elapsed in seconds using _process(delta) to advance combatant gauges independently of framerate.
# combat_atb_manager.gd
func _process(delta: float) -> void:
if not is_combat_active: return
for actor in combatants:
if actor.atb_gauge < 100.0:
actor.atb_gauge += actor.speed * delta
if actor.atb_gauge >= 100.0:
actor.atb_gauge = 100.0
is_combat_active = false # Pause for action
turn_ready.emit(actor)
break2. Turn Pre-visualization (Timeline Prediction)
Simulate ATB iterations mathematically to predict and display the future turn order in the UI.
# turn_predictor.gd
func predict_turns(actors: Array, count: int) -> Array:
var timeline := []
var sim_data := actors.map(func(a): return {"id": a, "gauge": a.atb_gauge, "speed": a.speed})
while timeline.size() < count:
for s in sim_data:
s.gauge += s.speed * 0.1 # Simulated step
if s.gauge >= 100.0:
timeline.append(s.id)
s.gauge = 0.0
if timeline.size() >= count: break
return timeline3. Combat Prediction Helper
Encapsulate predictive math within Resource scripts to show expected damage numbers to players before they commit to an action.
# combat_stats_resource.gd
func get_expected_damage(target: CombatStats) -> int:
# Deterministic calculation for UI display
var raw := attack_power - target.defense
return max(0, raw)
# UI usage
func _on_action_hover(target: Enemy):
var damage := player_stats.get_expected_damage(target.stats)
damage_preview_label.text = "Expected: %d" % damageReference
- Master Skill: godot-master
# skills/turn-system/scripts/active_time_battle.gd
extends Node
## Active Time Battle (ATB) Expert Pattern
## Async-aware ATB system managing unit charge, actions, and wait states.
class_name ActiveTimeBattle
signal turn_ready(unit: Node)
signal turn_ended(unit: Node)
enum State { CHARGING, WAIT_FOR_INPUT, EXECUTING }
@export var max_charge: float = 100.0
@export var fill_rate_multiplier: float = 1.0
var units: Array[Node] = [] # Expects units to have 'speed' and 'charge' properties
var current_state: State = State.CHARGING
var active_unit: Node
func _process(delta: float) -> void:
if current_state == State.CHARGING:
_process_charging(delta)
func _process_charging(delta: float) -> void:
for unit in units:
if is_instance_valid(unit):
# Logic: Speed * Multiplier * Delta
var speed = unit.get("speed") if "speed" in unit else 10.0
var current = unit.get("charge") if "charge" in unit else 0.0
current += speed * fill_rate_multiplier * delta
unit.set("charge", current)
if current >= max_charge:
_start_turn(unit)
return # Process one turn start per frame to avoid conflicts
func _start_turn(unit: Node) -> void:
current_state = State.WAIT_FOR_INPUT
active_unit = unit
turn_ready.emit(unit)
# Pause charging for others if "Wait" mode is desired
# set_process(false)
func submit_action(action_lambda: Callable) -> void:
if current_state != State.WAIT_FOR_INPUT:
return
current_state = State.EXECUTING
# Execute async action
await action_lambda.call()
_end_turn()
func _end_turn() -> void:
if is_instance_valid(active_unit):
active_unit.set("charge", 0.0)
turn_ended.emit(active_unit)
active_unit = null
current_state = State.CHARGING
# set_process(true)
## EXPERT USAGE:
## atb.turn_ready.connect(func(u): show_menu(u))
## atb.submit_action(func(): await u.attack(target))
# skills/turn-system/code/timeline_turn_manager.gd
extends Node
## Turn System Expert Pattern
## Implements Timeline-Based Initiative (CTB) and State Interrupts.
class Combatant:
var name: String
var speed: float = 10.0
var energy: float = 0.0 # Energy accumulates based on speed
var is_active: bool = true
var _combatants: Array[Combatant] = []
var _current_unit: Combatant = null
# 1. Timeline-Based Initiative
func process_timeline() -> Combatant:
# Expert logic: Accumulate energy until someone hits the 100 threshold.
while true:
for unit in _combatants:
if not unit.is_active: continue
unit.energy += unit.speed
if unit.energy >= 100.0:
unit.energy -= 100.0
_current_unit = unit
return unit
# 2. Interruption Mechanics (State Stack Logic)
func inject_interrupt(interrupt_unit: Combatant) -> void:
# Professional pattern: Temporarily halt the current turn
# to process a reaction or counter-attack.
print("Interrupt! ", interrupt_unit.name, " is reacting.")
var original_unit = _current_unit
_current_unit = interrupt_unit
# Process interrupt logic...
_current_unit = original_unit
print("Resuming turn for ", _current_unit.name)
# 3. Dynamic Turn Prediction
func get_predicted_order(steps: int = 10) -> Array[String]:
# Professional protocol: Simulate the timeline to show the player
# who moves next.
var prediction = []
var temp_energy = {}
for c in _combatants: temp_energy[c] = c.energy
for i in range(steps):
var next_unit = _simulate_next(temp_energy)
prediction.append(next_unit.name)
return prediction
func _simulate_next(energy_map: Dictionary) -> Combatant:
while true:
for unit in _combatants:
energy_map[unit] += unit.speed
if energy_map[unit] >= 100.0:
energy_map[unit] -= 100.0
return unit
## EXPERT NOTE:
## Use 'Signal-Based Turn Sync': Emit 'turn_started(unit)' and
## 'turn_ended(unit)' signals to the UI. The UI should NEVER poll
## the TurnManager; it only reacts to these signals.
## For 'turn-system', implement 'Asynchronous Sequence Execution':
## Allow multiple units with identical initiative to perform animations
## simultaneously to prevent "Turn Lag" in large battles.
## NEVER assume a combatant index will persist; if a unit dies
## during a round, use 'Combatant.is_active = false' and cleanup
## the array only at the start of a clear tick.
# turn_system_patterns.gd
extends Node
# 1. State Machine Pattern Matching
# EXPERT NOTE: Use Godot 4's advanced match syntax for clean, high-performance turn-phase logic.
func process_turn_logic(state: StringName) -> void:
match state:
&"player_turn":
await _handle_player_turn()
&"enemy_turn":
await _handle_enemy_turn()
&"resolution":
await _resolve_effects()
_:
push_error("Invalid turn state.")
# 2. Awaiting Player Input Safely
# EXPERT NOTE: Yield execution until a UI signal fires, preventing thread lock.
func wait_for_selection() -> void:
print("Awaiting player decision...")
# await some_ui_signal.pressed
pass
# 3. Robust Undo/Redo Turn History
# EXPERT NOTE: Uses the engine's built-in UndoRedo for historical state management.
var turn_undo_redo := UndoRedo.new()
func execute_unit_move(unit: Node2D, new_pos: Vector2) -> void:
turn_undo_redo.create_action("Move Unit")
turn_undo_redo.add_do_method(unit, &"set_position", new_pos)
turn_undo_redo.add_undo_method(unit, &"set_position", unit.position)
turn_undo_redo.commit_action()
# 4. Typed Dictionaries for Grid Board State
# EXPERT NOTE: Guarantees a rigid data structure for mapping coordinates to occupants.
var board_occupants: Dictionary[Vector2i, Node2D] = {}
# 5. Mapping Grid to Local Visual Space
# EXPERT NOTE: Safely maps logical array points to visual world space using TileMapLayer.
func snap_to_grid(layer: TileMapLayer, coords: Vector2i, entity: Node2D) -> void:
entity.position = layer.map_to_local(coords)
# 6. AStarGrid2D Fast Pathfinding
# EXPERT NOTE: Instantiates a high-speed grid navigation map bypassing physical nodes.
var astar_grid := AStarGrid2D.new()
func setup_astar(region_sz: Rect2i) -> void:
astar_grid.region = region_sz
astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
astar_grid.update()
# 7. Duck-Typing Turn Interfaces
# EXPERT NOTE: Use has_method to verify if an object can participate in a turn cycle.
func trigger_turn_if_capable(entity: Node) -> void:
if entity.has_method(&"take_turn"):
entity.call(&"take_turn")
# 8. Functional Condition Checks
# EXPERT NOTE: Uses optimized C++ lambdas (all/any) to evaluate combat win/loss states.
func check_victory(entities: Array[Node]) -> bool:
return entities.all(func(e): return e.get("is_dead") == true)
# 9. Dynamic Parameter Binding for UI
# EXPERT NOTE: Wire UI buttons to specific logic without extra wrapper functions.
func setup_action_button(btn: Button, target: Vector2i) -> void:
btn.pressed.connect(_on_action_performed.bind(target))
func _on_action_performed(_coords: Vector2i) -> void:
pass
# 10. Group Refreshing (Action Points)
# EXPERT NOTE: Instantly resets stats for all pertinent units in the scene tree.
func refresh_all_action_points() -> void:
get_tree().call_group(&"units", &"reset_ap")
func _handle_player_turn(): pass
func _handle_enemy_turn(): pass
func _resolve_effects(): pass