$npx -y skills add thedivergentai/GD-Agentic-Skills --skill godot-ability-systemExpert patterns for RPG/action ability systems including cooldown strategies, combo systems, ability chaining, skill trees with prerequisites, upgrade paths, and resource management. Use when implementing unlockable abilities, character progression, or complex skill systems. Trig
| 1 | ## Godot 4.7 Baseline |
| 2 | |
| 3 | - Expert patterns in this skill target **Godot 4.7+** (stable, 2026-06-18). |
| 4 | - Consult the [Godot 4.7 migration guide](https://docs.godotengine.org/en/4.7/tutorials/migrating/upgrading_to_godot_4.7.html) when upgrading projects from 4.6. |
| 5 | - **NEVER** assume 4.6 defaults (stretch mode, audio area_mask, RichTextLabel percent flags) without checking 4.7 migration notes. |
| 6 | |
| 7 | # Ability System |
| 8 | |
| 9 | Expert guidance for building flexible, extensible ability systems. |
| 10 | |
| 11 | ## NEVER Do |
| 12 | |
| 13 | - **NEVER use _process() for cooldown tracking** — Use timers or manual delta tracking in _physics_process(). _process() has variable delta and causes cooldown desync in slow frames. |
| 14 | - **NEVER forget global cooldown (GCD)** — Without GCD, players spam instant abilities. Add a small universal cooldown (0.5-1.5s) between all ability casts. |
| 15 | - **NEVER hardcode ability effects in manager code** — Use the Strategy pattern. Each ability is a Resource with execute() method, not a giant switch statement. |
| 16 | - **NEVER allow ability use during animation lock** — Check `is_casting` or `animation_playing` before allowing new casts. Interrupting animations breaks state machines. |
| 17 | - **NEVER save cooldown state without time normalization** — Save "cooldown_end_time" (OS.get_unix_time() + remaining), not "remaining_time". Prevents exploits (change system clock, reload game). |
| 18 | - **NEVER use Singletons (Autoloads) for combat managers** — Centralizing combat state in a global object makes tracking bugs difficult and breaks encapsulation. Keep abilities and stats scoped to the scenes that actually use them. |
| 19 | - **NEVER use Object Pooling with GDScript** — GDScript uses reference counting memory management, so you generally do not need to pool instantiated abilities or projectiles. Simply instantiate and queue_free(). |
| 20 | - **NEVER rely on deep inheritance trees** — Avoid having a BaseAbility -> MagicAbility -> FireAbility inheritance hell. Use node composition instead. |
| 21 | |
| 22 | --- |
| 23 | |
| 24 | ## Available Scripts |
| 25 | |
| 26 | > **MANDATORY**: Read the appropriate script before implementing the corresponding pattern. |
| 27 | |
| 28 | ### [ability_manager.gd](scripts/ability_manager.gd) |
| 29 | Ability orchestration with cooldown registry, can_use checks, and visual cooldown progress. Decoupled from character logic for use on players, enemies, or turrets. |
| 30 | |
| 31 | ### [ability_resource.gd](scripts/ability_resource.gd) |
| 32 | Scriptable ability resource base class with metadata, stats, and effects array. Virtual execute() method for inheritance (ProjectileAbility, BuffAbility). |
| 33 | |
| 34 | ### [buff_stat.gd](scripts/buff_stat.gd) |
| 35 | Resource-Driven Buff System setup. Extends Resource and creates highly modular, drag-and-drop ability data. |
| 36 | |
| 37 | --- |
| 38 | |
| 39 | ## Architecture Patterns |
| 40 | |
| 41 | ### Resource-Based Abilities |
| 42 | |
| 43 | ```gdscript |
| 44 | # ability_base.gd - Base class for all abilities |
| 45 | class_name Ability |
| 46 | extends Resource |
| 47 | |
| 48 | @export var ability_id: String |
| 49 | @export var display_name: String |
| 50 | @export var icon: Texture2D |
| 51 | @export var description: String |
| 52 | |
| 53 | @export_group("Costs") |
| 54 | @export var mana_cost: int = 0 |
| 55 | @export var stamina_cost: int = 0 |
| 56 | @export var health_cost: int = 0 # Life tap abilities |
| 57 | |
| 58 | @export_group("Timing") |
| 59 | @export var cooldown: float = 5.0 |
| 60 | @export var cast_time: float = 0.0 # 0 = instant |
| 61 | @export var channel_time: float = 0.0 # Channeled abilities |
| 62 | |
| 63 | @export_group("Unlocking") |
| 64 | @export var unlock_level: int = 1 |
| 65 | @export var prerequisites: Array[String] = [] # Other ability IDs |
| 66 | |
| 67 | ## Override these |
| 68 | func can_cast(caster: Node) -> bool: |
| 69 | return true # Additional checks (range, target, etc.) |
| 70 | |
| 71 | func execute(caster: Node, target: Node = null) -> void: |
| 72 | pass # Ability effect |
| 73 | |
| 74 | func on_cast_start(caster: Node) -> void: |
| 75 | pass # Animation, effects |
| 76 | |
| 77 | func on_cast_complete(caster: Node) -> void: |
| 78 | execute(caster) |
| 79 | |
| 80 | func on_cancel(caster: Node) -> void: |
| 81 | pass # Refund resources |
| 82 | ``` |
| 83 | |
| 84 | ### Concrete Ability Example |
| 85 | |
| 86 | ```gdscript |
| 87 | # fireball.gd |
| 88 | class_name FireballAbility |
| 89 | extends Ability |
| 90 | |
| 91 | @export var damage: int = 50 |
| 92 | @export var projectile_scene: PackedScene |
| 93 | @export var range: float = 500.0 |
| 94 | |
| 95 | func can_cast(caster: Node) -> bool: |
| 96 | var target = caster.get_target() |
| 97 | if not target: |
| 98 | return false |
| 99 | |
| 100 | var distance := caster.global_position.distance_to(target.global_position) |
| 101 | return distance <= range |
| 102 | |
| 103 | func execute(caster: Node, target: Node = null) -> void: |
| 104 | var projectile := projectile_scene.instantiate() |
| 105 | caster.get_parent().add_child(projectile) |
| 106 | projectile.global_position = caster.global_position |
| 107 | projectile.target = target |
| 108 | projectile.da |