godot-combat-system
Expert patterns for combat systems including hitbox/hurtbox architecture, damage calculation (DamageData class), health components, combat state machines, combo systems, ability cooldowns, and damage popups. Use for action games, RPGs, or fighting games. Trigger keywords: Hitbox, Hurtbox, DamageData, HealthComponent, combat_state, combo_system, ability_cooldown, invincibility_frames, damage_popup.
What this skill does
# Combat System
Expert guidance for building flexible, component-based combat systems.
## NEVER Do
- **NEVER use direct damage references (`target.health -= 10`)** — This bypasses armor, resistances, and invincibility logic. Always use a `DamageData` + `HealthComponent` pattern for consistent results.
- **NEVER forget invincibility frames (i-frames)** — Without them, multi-hit attacks deal damage every single frame. Always apply a brief invincibility period (0.1–0.5s) after taking a hit.
- **NEVER keep hitboxes active permanently** — This causes unintended "ghost" damage. Enable and disable hitboxes precisely using `AnimationPlayer` tracks or code-timed triggers.
- **NEVER use groups for physics-based hit filtering** — Collision layers are evaluated in C++ and are significantly faster. Groups don't restrict physics intersections adequately for high-performance combat.
- **NEVER emit damage signals without a DamageData object** — A raw number loses critical context like damage type, source, and knockback direction.
- **NEVER use try/catch blocks with validate targets** — GDScript does not support exceptions. Use `has_method(&"take_damage")` or the `is` operator for safe type checking.
- **NEVER hardcode hitstun pauses using OS.delay_msec()** — This blocks the entire OS thread and freezes the game. Use `create_tween()` or `Engine.time_scale` for visual hit-stop effects.
- **NEVER apply massive impulses to a RigidBody inside _process()** — Physics-altering impulses must happen in `_physics_process()` or `_integrate_forces()` to remain deterministic and stable.
- **NEVER couple UI lifebars directly inside the Player script** — Use a `health_changed` signal. This keeps your combat logic clean and independent of UI implementation details.
- **NEVER leave CollisionShapes active on dead entities** — Corpses will block players and towers. Disable them immediately using `set_deferred("disabled", true)`.
- **NEVER scale CollisionShapes non-uniformly** — Non-uniform scaling breaks the physics engine's collision math. Always scale the internal resource (e.g., `CircleShape2D.radius`) instead.
- **NEVER use instanced Nodes for base stat data** — Nodes carry unnecessary overhead. Use Godot's `Resource` class for lightweight, efficient, and inspectable stat containers.
- **NEVER use raw strings for elemental damage types** — Strings are slow and error-prone. Use `enum` flags (optionally with `@export_flags`) to manage multi-type damage efficiently.
- **NEVER use standard strings for state names in high-frequency loops** — Use `StringName` (&"attacking", &"stunned") to drastically improve dictionary lookups and hash comparison speeds.
- **NEVER forget to duplicate() a shared Resource stats block** — If you don't call `duplicate()` when instancing a mob, all enemies of that type will share the same health pool.
---
## Available Scripts
> **MANDATORY**: Read the appropriate script before implementing the corresponding pattern.
### [combat_system_patterns.gd](scripts/combat_system_patterns.gd)
10 Expert patterns: Safe duck-typing, hitstun tweens, nodeless AoE shape casting, and frame-perfect sync.
### [hitbox_hurtbox.gd](scripts/hitbox_hurtbox.gd)
Component-based hitbox with hit-stop and knockback logic.
---
## Damage System
```gdscript
# damage_data.gd
class_name DamageData
extends RefCounted
var amount: float
var source: Node
var damage_type: String = "physical"
var knockback: Vector2 = Vector2.ZERO
var is_critical: bool = false
func _init(dmg: float, src: Node = null) -> void:
amount = dmg
source = src
```
## Hurtbox/Hitbox Pattern
```gdscript
# hurtbox.gd
extends Area2D
class_name Hurtbox
signal damage_received(data: DamageData)
@export var health_component: Node
func _ready() -> void:
area_entered.connect(_on_area_entered)
func _on_area_entered(area: Area2D) -> void:
if area is Hitbox:
var damage := area.get_damage()
damage_received.emit(damage)
if health_component:
health_component.take_damage(damage)
```
```gdscript
# hitbox.gd
extends Area2D
class_name Hitbox
@export var damage: float = 10.0
@export var damage_type: String = "physical"
@export var knockback_force: float = 100.0
@export var owner_node: Node
func get_damage() -> DamageData:
var data := DamageData.new(damage, owner_node)
data.damage_type = damage_type
# Calculate knockback direction
if owner_node:
var direction := (global_position - owner_node.global_position).normalized()
data.knockback = direction * knockback_force
return data
```
## Health Component
```gdscript
# health_component.gd
extends Node
class_name HealthComponent
signal health_changed(old_health: float, new_health: float)
signal died
signal healed(amount: float)
@export var max_health: float = 100.0
@export var current_health: float = 100.0
@export var invincible: bool = false
func take_damage(data: DamageData) -> void:
if invincible:
return
var old_health := current_health
current_health -= data.amount
current_health = clampf(current_health, 0, max_health)
health_changed.emit(old_health, current_health)
if current_health <= 0:
died.emit()
func heal(amount: float) -> void:
var old_health := current_health
current_health += amount
current_health = minf(current_health, max_health)
healed.emit(amount)
health_changed.emit(old_health, current_health)
func is_dead() -> bool:
return current_health <= 0
```
## Combat State Machine
```gdscript
# combat_state.gd
extends Node
class_name CombatState
enum State { IDLE, ATTACKING, BLOCKING, DODGING, STUNNED }
var current_state: State = State.IDLE
var can_act: bool = true
func enter_attack_state() -> bool:
if not can_act:
return false
current_state = State.ATTACKING
can_act = false
return true
func enter_block_state() -> void:
current_state = State.BLOCKING
func enter_dodge_state() -> bool:
if not can_act:
return false
current_state = State.DODGING
can_act = false
return true
func exit_state() -> void:
current_state = State.IDLE
can_act = true
```
## Combo System
```gdscript
# combo_system.gd
extends Node
class_name ComboSystem
signal combo_executed(combo_name: String)
@export var combo_window: float = 0.5
var combo_buffer: Array[String] = []
var last_input_time: float = 0.0
func register_input(action: String) -> void:
var current_time := Time.get_ticks_msec() / 1000.0
if current_time - last_input_time > combo_window:
combo_buffer.clear()
combo_buffer.append(action)
last_input_time = current_time
check_combos()
func check_combos() -> void:
# Light → Light → Heavy = Special Attack
if combo_buffer.size() >= 3:
var last_three := combo_buffer.slice(-3)
if last_three == ["light", "light", "heavy"]:
execute_combo("special_attack")
combo_buffer.clear()
func execute_combo(combo_name: String) -> void:
combo_executed.emit(combo_name)
```
## Ability System
```gdscript
# ability.gd
class_name Ability
extends Resource
@export var ability_name: String
@export var cooldown: float = 1.0
@export var damage: float = 25.0
@export var range: float = 100.0
@export var animation: String
var is_on_cooldown: bool = false
func can_use() -> bool:
return not is_on_cooldown
func use(caster: Node) -> void:
if not can_use():
return
is_on_cooldown = true
# Execute ability logic
_execute(caster)
# Start cooldown
await caster.get_tree().create_timer(cooldown).timeout
is_on_cooldown = false
func _execute(caster: Node) -> void:
# Override in derived abilities
pass
```
## Damage Popups
```gdscript
# damage_popup.gd
extends Label
func show_damage(amount: float, is_crit: bool = false) -> void:
text = str(int(amount))
if is_crit:
modulate = Color.RED
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.