Skip to content
Back to skills

Combat Basics

ASecurity

Damage: hitbox and hurtbox Areas, a reusable Health component, damage signals, knockback, projectiles.

  • 77 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentsgoshellnodetestingdebugging

Works with

  • claude code
  • cursor
  • cli
  • mcp

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add SummerEngine/summer --skill combat-basics --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Combat Basics?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Combat Basics
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/summerengine-combat-basics-summer/badge)](https://www.skillsdirectory.com/skills/summerengine-combat-basics-summer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: combat-basics
description: "Damage: hitbox and hurtbox Areas, a reusable Health component, damage signals, knockback, projectiles."
license: MIT
compatibility: [Cursor, Claude Code, Codex, Windsurf, Gemini, OpenCode]
category: gameplay-mechanics
user-invocable: false
allowed-tools: Read Grep summer_read_file summer_write_file summer_replace_text summer_add_node summer_set_prop summer_batch summer_project_setting summer_save_scene summer_get_script_errors summer_get_diagnostics
paths: ["**/*.gd", "**/*.tscn"]
---

# Combat Basics

The standard combat pattern is hitbox + hurtbox + health, wired by signals. Build it
component-first: the same three pieces work for player, enemies, and destructibles.

## The layer model (set this up first)

Attacks are `Area2D`/`Area3D`, not solid bodies. A hitbox (the attack) detects a
hurtbox (the thing that can be hurt) via collision layers/masks:

| Node | on layer | masks (detects) |
|------|----------|-----------------|
| Player hurtbox | PlayerHurt | EnemyHit |
| Player hitbox (sword) | PlayerHit | EnemyHurt |
| Enemy hurtbox | EnemyHurt | PlayerHit |
| Enemy hitbox | EnemyHit | PlayerHurt |

Rule: every combat Area masks only the OPPOSING side. This is why the player's
sword never damages the player. An Area reports `area_entered` only for areas on
a layer in its OWN mask, so the hurtbox below (which listens) must mask the
opposing hitbox layer; a hurtbox with an empty mask never registers a hit.

Name the layers once so you are not counting bits by hand —
`summer_project_setting(key="layer_names/2d_physics/layer_4", value="PlayerHit")`
(use `layer_names/3d_physics/...` for 3D), one call per layer.

## Health component (reuse everywhere)

Attach to any damageable node. It owns HP and fires signals; UI and death logic
just listen.

```gdscript
class_name Health extends Node

signal damaged(amount: int, current: int)
signal died

@export var max_health: int = 100
var current: int

func _ready() -> void:
	current = max_health

func take_damage(amount: int) -> void:
	if current <= 0:
		return
	current = maxi(0, current - amount)
	damaged.emit(amount, current)
	if current == 0:
		died.emit()

func heal(amount: int) -> void:
	current = mini(max_health, current + amount)
```

## Hurtbox -> Health

The hurtbox is an Area on the body; when a hitbox overlaps it, read the hitbox's
damage and route it to the Health node.

```gdscript
extends Area2D   # Hurtbox, child of the character

@export var health_path: NodePath
@onready var health: Health = get_node(health_path)

func _ready() -> void:
	area_entered.connect(_on_area_entered)

func _on_area_entered(hitbox: Area2D) -> void:
	if hitbox.has_method("get_damage"):
		health.take_damage(hitbox.get_damage())
```

## Hitbox

```gdscript
extends Area2D   # Hitbox, child of weapon or attack

@export var damage: int = 10
func get_damage() -> int:
	return damage
```

Enable the hitbox only during the active frames of a swing (toggle
`monitoring`/`monitorable` or the CollisionShape's `disabled`), driven by an
AnimationPlayer track. An always-on melee hitbox hits on touch, which feels wrong.

## Death, knockback, feedback

- On `died`: `queue_free()`, spawn a death VFX, drop loot, `add_kill()` on your
  game manager. For the player, switch to a GAME_OVER state instead of freeing.
- Knockback: push the victim `velocity = (victim.global_position -
  attacker.global_position).normalized() * force`, decay it each frame.
- Every hit needs feedback or it reads as broken: a flash (`modulate` to white for
  0.05s), a damage number (floating Label), a small screen shake, a hitstop
  (`Engine.time_scale = 0.05` for one frame). Silent damage feels like a no-op.

## Projectiles (short version)

An `Area2D/3D` that moves forward and calls `take_damage` on what it hits, then
frees itself.

```gdscript
extends Area2D   # projectile

@export var speed: float = 600.0
@export var damage: int = 15
var direction: Vector2 = Vector2.RIGHT

func _ready() -> void:
	body_entered.connect(_on_hit)
	get_tree().create_timer(3.0).timeout.connect(queue_free)  # lifetime

func _physics_process(delta: float) -> void:
	position += direction * speed * delta

func _on_hit(body: Node) -> void:
	if body.has_node("Health"):
		body.get_node("Health").take_damage(damage)
	queue_free()
```

Homing, piercing, AoE, and pooling many bullets are in this skill's
`references/projectiles-and-aoe.md` — read it only when you need one of those.

## Traps

- Hitbox on the wrong layer/mask = no hits or friendly fire. Verify the layer
  table above before debugging the scripts.
- Two Areas with neither `monitoring` nor `monitorable` never report overlap.
- Freeing a node mid-signal can error - defer with `queue_free()` (safe), not
  `free()`.
- Damaging in `_process` instead of on overlap = continuous per-frame damage.

## Applying it in Summer Engine

- Scripts (`health.gd`, `hurtbox.gd`, `hitbox.gd`, `projectile.gd`) are files:
  create with `summer_write_file(path=..., content=..., create_only=true)`,
  change with `summer_replace_text`. Check each with `summer_get_script_errors`.
- Nodes (the Area, its CollisionShape, the Health child) are scene mutations:
  `summer_add_node` / `summer_set_prop` with `scenePath`, or one `summer_batch`
  with `AddNode` / `SetProp` ops so the scene saves once. Set `collision_layer`
  and `collision_mask` as integers (bit values), not layer names.
- Prove a hit with a `RunVerification` probe (see `playtesting-a-feature`):
  spawn or find one enemy, press the attack action, `report()` the enemy's
  `Health.current` before and after. A static scene check cannot show that a
  hit registered.

<!-- shell-commands: generated from the operation catalog by npm run generate:registry; do not edit by hand -->
## From a shell

No Summer MCP tools in this session? Run each one as a command; when `summer --version` is missing or below 4.0, use `npx -y summer-engine@4.3.0` in place of `summer`. Fields are flags in kebab-case (`--scene-path res://main.tscn`) or one `--input '<json>'`, and `summer ops describe <op>` prints the exact input. The `summer-cli` skill has the rest.

| In this skill | Command |
|---|---|
| `summer_add_node` | `summer engine add-node` |
| `summer_batch` | `summer engine batch` |
| `summer_get_diagnostics` | `summer engine get-diagnostics` |
| `summer_get_script_errors` | `summer engine get-script-errors` |
| `summer_project_setting` | `summer engine project-setting` |
| `summer_read_file` | `summer engine read-file` |
| `summer_replace_text` | `summer engine replace-text` |
| `summer_save_scene` | `summer engine save-scene` |
| `summer_set_prop` | `summer engine set-prop` |
| `summer_write_file` | `summer engine write-file` |
<!-- /shell-commands -->

Files in this skill

  • SKILL.md6.6 KB
  • references/projectiles-and-aoe.md2.8 KB
  • resource.yaml1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.