godot-ability-system

安装量: 61
排名: #12142

安装

npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-ability-system
Ability System
Expert guidance for building flexible, extensible ability systems.
NEVER Do
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.
NEVER forget global cooldown (GCD)
— Without GCD, players spam instant abilities. Add a small universal cooldown (0.5-1.5s) between all ability casts.
NEVER hardcode ability effects in manager code
— Use the Strategy pattern. Each ability is a Resource with execute() method, not a giant switch statement.
NEVER allow ability use during animation lock
— Check
is_casting
or
animation_playing
before allowing new casts. Interrupting animations breaks state machines.
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).
Available Scripts
MANDATORY
Read the appropriate script before implementing the corresponding pattern. ability_manager.gd Ability orchestration with cooldown registry, can_use checks, and visual cooldown progress. Decoupled from character logic for use on players, enemies, or turrets. ability_resource.gd Scriptable ability resource base class with metadata, stats, and effects array. Virtual execute() method for inheritance (ProjectileAbility, BuffAbility). Architecture Patterns Resource-Based Abilities

ability_base.gd - Base class for all abilities

class_name Ability extends Resource @ export var ability_id : String @ export var display_name : String @ export var icon : Texture2D @ export var description : String @ export_group ( "Costs" ) @ export var mana_cost : int = 0 @ export var stamina_cost : int = 0 @ export var health_cost : int = 0

Life tap abilities

@ export_group ( "Timing" ) @ export var cooldown : float = 5.0 @ export var cast_time : float = 0.0

0 = instant

@ export var channel_time : float = 0.0

Channeled abilities

@ export_group ( "Unlocking" ) @ export var unlock_level : int = 1 @ export var prerequisites : Array [ String ] = [ ]

Other ability IDs

Override these

func can_cast ( caster : Node ) -> bool : return true

Additional checks (range, target, etc.)

func execute ( caster : Node , target : Node = null ) -> void : pass

Ability effect

func on_cast_start ( caster : Node ) -> void : pass

Animation, effects

func on_cast_complete ( caster : Node ) -> void : execute ( caster ) func on_cancel ( caster : Node ) -> void : pass

Refund resources

Concrete Ability Example

fireball.gd

class_name FireballAbility extends Ability @ export var damage : int = 50 @ export var projectile_scene : PackedScene @ export var range : float = 500.0 func can_cast ( caster : Node ) -> bool : var target = caster . get_target ( ) if not target : return false var distance := caster . global_position . distance_to ( target . global_position ) return distance <= range func execute ( caster : Node , target : Node = null ) -> void : var projectile := projectile_scene . instantiate ( ) caster . get_parent ( ) . add_child ( projectile ) projectile . global_position = caster . global_position projectile . target = target projectile . damage = damage Ability Manager (Centralized) Core Manager

ability_manager.gd

class_name AbilityManager extends Node signal ability_cast ( ability_id : String ) signal ability_ready ( ability_id : String ) signal cooldown_started ( ability_id : String , duration : float ) var abilities : Dictionary = { }

ability_id → Ability

var cooldowns : Dictionary = { }

ability_id → float (time remaining)

var is_casting : bool = false var global_cooldown : float = 0.0

GCD timer

@ export var gcd_duration : float = 1.0

Global cooldown

func register_ability ( ability : Ability ) -> void : abilities [ ability . ability_id ] = ability cooldowns [ ability . ability_id ] = 0.0 func can_use_ability ( ability_id : String , caster : Node ) -> bool : var ability := abilities . get ( ability_id ) as Ability if not ability : return false

Check GCD

if global_cooldown

0.0 : return false

Check specific cooldown

if cooldowns . get ( ability_id , 0.0 )

0.0 : return false

Check if already casting

if is_casting and ability . cast_time

0.0 : return false

Check resources

if not has_resources ( caster , ability ) : return false

Ability-specific checks

return ability . can_cast ( caster ) func use_ability ( ability_id : String , caster : Node , target : Node = null ) -> bool : if not can_use_ability ( ability_id , caster ) : return false var ability := abilities [ ability_id ]

Consume resources

consume_resources ( caster , ability )

Start cast

if ability . cast_time

0.0 : start_cast ( ability , caster , target ) else :

Instant cast

ability . execute ( caster , target ) trigger_cooldown ( ability_id , ability . cooldown ) ability_cast . emit ( ability_id ) return true func start_cast ( ability : Ability , caster : Node , target : Node ) -> void : is_casting = true ability . on_cast_start ( caster )

Create timer for cast completion

var timer := get_tree ( ) . create_timer ( ability . cast_time ) await timer . timeout if is_casting :

Not interrupted

ability . on_cast_complete ( caster ) trigger_cooldown ( ability . ability_id , ability . cooldown ) is_casting = false func interrupt_cast ( ) -> void : if is_casting : is_casting = false

Trigger ability.on_cancel() if needed

func trigger_cooldown ( ability_id : String , duration : float ) -> void : cooldowns [ ability_id ] = duration global_cooldown = gcd_duration cooldown_started . emit ( ability_id , duration ) func _physics_process ( delta : float ) -> void :

Tick cooldowns

for ability_id in cooldowns . keys ( ) : if cooldowns [ ability_id ]

0.0 : cooldowns [ ability_id ] -= delta if cooldowns [ ability_id ] <= 0.0 : ability_ready . emit ( ability_id )

Tick GCD

if global_cooldown

0.0 : global_cooldown -= delta func has_resources ( caster : Node , ability : Ability ) -> bool : return ( caster . mana = ability . mana_cost and caster . stamina = ability . stamina_cost and caster . health

ability . health_cost ) func consume_resources ( caster : Node , ability : Ability ) -> void : caster . mana -= ability . mana_cost caster . stamina -= ability . stamina_cost caster . health -= ability . health_cost Advanced Patterns Combo System

combo_tracker.gd

extends Node var combo_chain : Array [ String ] = [ ] var combo_window : float = 2.0

Seconds to continue combo

var last_ability_time : float = 0.0 func register_ability_use ( ability_id : String ) -> void : var current_time := Time . get_ticks_msec ( ) * 0.001

Reset if too much time passed

if current_time - last_ability_time

combo_window : combo_chain . clear ( ) combo_chain . append ( ability_id ) last_ability_time = current_time

Check for combo completion

check_combos ( ) func check_combos ( ) -> void :

Example: "slash" → "slash" → "spin" = "whirlwind"

if combo_chain . size ( )

= 3 : var last_three := combo_chain . slice ( - 3 ) if last_three == [ "slash" , "slash" , "spin" ] : trigger_combo_ability ( "whirlwind" ) combo_chain . clear ( ) func trigger_combo_ability ( combo_id : String ) -> void :

Execute powerful combo ability

pass Charge-Based Abilities

charge_ability.gd - Abilities with multiple charges (like League of Legends Flash)

class_name ChargeAbility extends Ability @ export var max_charges : int = 2 @ export var charge_recharge_time : float = 20.0 var current_charges : int = max_charges var recharge_timer : float = 0.0 func can_cast ( caster : Node ) -> bool : return current_charges

0 func execute ( caster : Node , target : Node = null ) -> void : current_charges -= 1

Start recharging if not at max

if current_charges < max_charges and recharge_timer == 0.0 : recharge_timer = charge_recharge_time func tick ( delta : float ) -> void : if recharge_timer

0.0 : recharge_timer -= delta if recharge_timer <= 0.0 : current_charges += 1 if current_charges < max_charges : recharge_timer = charge_recharge_time

Continue recharging

else : recharge_timer = 0.0 Skill Tree System Skill Node

skill_node.gd

class_name SkillNode extends Resource @ export var skill_id : String @ export var display_name : String @ export var description : String @ export var icon : Texture2D @ export_group ( "Requirements" ) @ export var prerequisites : Array [ String ] = [ ]

Other skill_ids

@ export var character_level_required : int = 1 @ export var points_required : int = 1 @ export var mutually_exclusive_with : Array [ String ] = [ ]

Can't have both

@ export_group ( "Progression" ) @ export var max_rank : int = 1 @ export var current_rank : int = 0 @ export_group ( "Effects" ) @ export var unlocks_ability : String = ""

Ability ID to grant

@ export var stat_bonuses : Dictionary = { }

"strength": 5, "crit_chance": 0.05

func can_unlock ( player_skills : Dictionary , player_level : int , available_points : int ) -> bool :

Already maxed

if current_rank

= max_rank : return false

Not enough points

if available_points < points_required : return false

Level requirement

if player_level < character_level_required : return false

Prerequisites

for prereq_id in prerequisites : if not player_skills . has ( prereq_id ) or player_skills [ prereq_id ] . current_rank == 0 : return false

Mutual exclusivity

for exclusive_id in mutually_exclusive_with : if player_skills . has ( exclusive_id ) and player_skills [ exclusive_id ] . current_rank

0 : return false return true func unlock ( ) -> void : current_rank += 1 Skill Tree Manager

skill_tree.gd

class_name SkillTree extends Node signal skill_unlocked ( skill_id : String , rank : int ) signal points_changed ( new_total : int ) var skills : Dictionary = { }

skill_id → SkillNode

var skill_points : int = 0 func add_skill ( skill : SkillNode ) -> void : skills [ skill . skill_id ] = skill func can_unlock_skill ( skill_id : String , player_level : int ) -> bool : var skill := skills . get ( skill_id ) as SkillNode if not skill : return false return skill . can_unlock ( skills , player_level , skill_points ) func unlock_skill ( skill_id : String , player_level : int ) -> bool : if not can_unlock_skill ( skill_id , player_level ) : return false var skill := skills [ skill_id ] skill . unlock ( ) skill_points -= skill . points_required

Apply effects

apply_skill_effects ( skill ) skill_unlocked . emit ( skill_id , skill . current_rank ) points_changed . emit ( skill_points ) return true func apply_skill_effects ( skill : SkillNode ) -> void :

Grant ability if specified

if skill . unlocks_ability != "" : var ability_manager := get_node ( "/root/AbilityManager" )

Register new ability

Apply stat bonuses

var player := get_tree ( ) . get_first_node_in_group ( "player" ) for stat_name in skill . stat_bonuses . keys ( ) : var bonus = skill . stat_bonuses [ stat_name ] player . set ( stat_name , player . get ( stat_name ) + bonus ) func add_skill_points ( amount : int ) -> void : skill_points += amount points_changed . emit ( skill_points ) func reset_tree ( refund_points : bool = true ) -> void : var total_spent := 0 for skill in skills . values ( ) : total_spent += skill . current_rank * skill . points_required skill . current_rank = 0 if refund_points : skill_points += total_spent points_changed . emit ( skill_points ) Cooldown Strategies Per-Ability Cooldown (Standard)

Already shown in AbilityManager above

Each ability has independent cooldown

Shared Cooldown (Hearthstone-style)

All abilities of type "summon" share cooldown

var summon_cooldown : float = 0.0 func use_summon_ability ( ability : Ability ) -> void : ability . execute ( ) summon_cooldown = 3.0

All summons on 3s cooldown

Charge System (Already shown above) Multiple uses, recharges over time. Edge Cases Cooldown Persistence

save_system.gd

func save_ability_cooldowns ( ) -> Dictionary : var data := { } var current_time := Time . get_unix_time_from_system ( ) for ability_id in ability_manager . cooldowns . keys ( ) : var remaining := ability_manager . cooldowns [ ability_id ] if remaining

0.0 : data [ ability_id ] = current_time + remaining

Absolute time

return data func load_ability_cooldowns ( data : Dictionary ) -> void : var current_time := Time . get_unix_time_from_system ( ) for ability_id in data . keys ( ) : var end_time : float = data [ ability_id ] var remaining := max ( 0.0 , end_time - current_time ) ability_manager . cooldowns [ ability_id ] = remaining Animation Lock

Prevent ability spam during attack animations

func on_animation_player_animation_started ( anim_name : String ) -> void : if anim_name . begins_with ( "attack" ) : ability_manager . is_casting = true func on_animation_player_animation_finished ( anim_name : String ) -> void : if anim_name . begins_with ( "attack" ) : ability_manager . is_casting = false Reference Master Skill: godot-master

返回排行榜