Skip to main content
design-npc is a skill from the Summer skill library: a guide your AI agent reads when the task matches, so it builds the good version on the first try. Design enemy, NPC, boss or companion behavior: perception, state machine, telegraphs, script stub.

When your agent uses it

  • Designing an enemy, NPC, boss, companion, civilian, or wave-spawned mob’s behavior — perception, intent, state machine, telegraphs, defeat handling
  • Enemies that patrol, chase, attack, or flee when they see the player (line of sight)

Install

summer setup installs every skill for your agent. To add only this one:
Swap claude-code for your agent, such as codex or cursor. See Skills for every option.

The skill

Overview

A good NPC is legible and alive. The player can read its mood, predict its next move, and feel the consequence of theirs — and the four enemies in a patrol squad don’t all behave like clones of one designer-authored script. This skill walks the canonical NPC AI pattern from the inside out: what the NPC perceives, what it wants (intent), what it does (action), what tells signal each state, and how it dies. Core principle: behavior is what the player sees. Two layers separate doing from wanting: an action state machine the body runs, and an intent layer above it that reads perception + personality and tells the action SM what to aim at. Second principle: identical NPCs are predictable, and predictable is boring. Every spawned instance jitters its personality knobs so a squad of four grunts produces emergent variety with zero designer authoring.

Steps

1. Pick the NPC archetype

Open with the question. Wait.
What’s the NPC for? Pick the closest:
  • Basic enemy (melee/ranged grunt, dies in 1–3 hits)
  • Boss (named, scripted phases, 30–120 sec fight)
  • Quest-giver (talks, gives objectives, doesn’t fight)
  • Friendly companion (follows player, helps in combat)
  • Civilian / ambient (reacts to events, no combat AI)
  • Wave-spawned mob (cheap, many at once, simple behavior)
Each archetype maps to a different complexity budget:

2. Walk the design pillars

Walk them in order. Don’t skip. The pillars are Perception → Personality → Intent → Action → Telegraph → Defeat.

a) Perception — what does this NPC sense?

Pick the minimum. Over-perception = unfair AI. Recommended setup for a basic enemy:
Two-step (Area3D filter, then RayCast LOS) is the right default. Don’t raycast to every potential target every frame.

b) Personality — four floats that make this NPC not a clone

Every NPC instance gets four knobs, each in [0.0, 1.0], declared as @export_range so designers can tune the base per archetype, then randomized per-instance at _ready so spawned siblings differ. Threshold mapping (used inside the intent layer): engage_threshold = 2.0 - aggression (higher aggression = engage faster), search_duration = 4.0 + patience * 6.0 (patient NPC searches up to 10s), break_los_chance = caution * (1 - hp_ratio) (cautious AND hurt = break LOS), and should_punish() = randf() < punishment (rolled when the player is recovering / reloading). Declare the knobs as @export_range(0.0, 1.0) so each archetype scene tunes them per-instance, and jitter them by +/- personality_jitter (default 0.15) inside _ready — same script + same archetype scene produces NPCs whose feel is shaped by four numbers alone. Full example in the script stub at step 7.

c) Intent layer — what the NPC is trying to do

The intent layer sits above the action SM. It runs each tick, reads perception + personality + group state, and writes a single current_intent. The action SM reads current_intent to pick its transitions; it never sets intent itself. See _decide_intent in the stub (step 7) for the canonical priority order: RESCUE_ALLY > RETREAT (low HP + caution) > KILL (pressure built) > INVESTIGATE (lost LOS, within memory) > HUNT (LOS, out of range) > PATROL / IDLE. Five-to-seven intents is the sweet spot: The action SM’s job is now narrow: given an intent, what locomotion + animation + hitbox state matches?

d) Action SM — what the body is currently doing

Four states is plenty for a basic enemy. They are the visible mood, not the goal.
Action transitions are computed from current_intent and a few action-local conditions (HP, animation locks). The intent layer does not transition action states directly — it sets the goal, action follows.

e) Telegraph — every aggressive action has a tell

Telegraph is non-negotiable for any attack. 0.3–0.7 sec of windup the player can react to. Faster than 0.3 = “unfair”; slower than 0.7 = “boring”. Personality bleeds into telegraph length:
Every aggressive action has at least one of: animation windup, audio cue, VFX glow, particle trail, hitbox preview. Cheap mobs can collapse all three into one anim — but the windup must exist.

f) Defeat — what happens when it dies

Plan it. “It just disappears” is bad. Choices:
  • Death animation plays, body stays for N sec, then queue_free.
  • Ragdoll: switch CollisionShape from CharacterBody3D to RigidBody3D for weight.
  • Loot drop: spawn pickup at death position.
  • Cleanup: leave any group, disconnect signals, free children.
  • Score / quest hook: emit died(global_position) for the gameplay layer.
  • Group signal: emit ally_defeated so squadmates can update their RESCUE_ALLY checks (or panic).

3. Group emergence — varied personality across a squad

When you spawn a squad of four with the same archetype but personality jittered per-instance, you get behavioral roles for free. Worked example — four basic_grunts with aggression after jitter at 0.3 / 0.5 / 0.7 / 0.9: No designer authored “the sniper”. The jitter on a single knob produced a four-role squad. Stack caution and patience on top and you get richer roles (a high-caution / low-aggression NPC reads as “the careful one who hangs back and supports”). The squad feels coordinated even though each NPC is making local decisions. Implementation note: spawners should call randomize() once at _ready and let each NPC’s own _ready handle the jitter. Don’t try to author roles centrally — the variation is the design.

4. Read the scene to find an attach point

Identify where the NPC will live (an Enemies parent, or directly under World). Confirm the player exists for line-of-sight.

5. Propose the bundle

I’m about to design a basic patrol-and-chase enemy with the personality + intent layer. Scene tree: Enemy (CharacterBody3D) + Vision Area3D + Sight RayCast3D + CollisionShape3D + MeshInstance3D. Script scripts/enemy_ai.gd with personality knobs (aggression / patience / caution / punishment), intent enum (IDLE / PATROL / INVESTIGATE / HUNT / KILL / RETREAT / RESCUE_ALLY), action SM (Calm / Alert / Aggressive / Defeated). Telegraph 0.3–0.7 sec, scaled by aggression. Drop loot on death, emit died signal. May I create the scene + script?

6. Build it

Preferred (Summer MCP): One script builds the whole enemy; scenePath names the exact res:// scene, and the run is one undo step, so one Ctrl+Z removes the enemy:
The script embeds the sight sphere in the scene. When several enemy archetypes want the same sight radius, share one file instead: {"shape": load("res://shapes/vision_sphere.tres")}. Tuning one value later is one call:
summer_set_resource_property works against scene-embedded ([sub_resource]) resources exactly as it does against external .tres files — it reads the property off the node and writes the sub-property onto whatever Resource is there. Structural failures are explicit errors (node not found, property is not a resource, resource is null); a bad value shape is not — a JSON object instead of an engine literal string, a misspelled subProperty, or a wrong-typed value returns ok:true yet silently no-ops or coerces destructively on current engines. Pass class names and literal strings ("12.0", "Color(r, g, b, a)") and confirm the result in the saved .tscn, never from ok alone. The order that matters is: assign the resource first, then set sub-properties. Connect signals:
The parameters are scenePath, emitter, signal, receiver, method. There is no from / to; those spellings fail schema validation. Fallback (no MCP): write the scene as .tscn text. Ask the user to paste their existing scene first, then propose a unified diff.

7. Drop the script

scripts/enemy_ai.gd:
Key design notes embedded above:
  • aggression, patience, caution, punishment are tuned per-archetype and jittered per-instance.
  • Intent SM (_update_intent / _decide_intent) sits above the action SM (_update_action).
  • Action SM reads current_intent. It does not write intent.
  • Telegraph length scales with aggression — angrier NPCs swing faster.
  • Damage triggers a caution * (1 - hp_ratio) chance to set _retreat_desire, so cautious + hurt = back off.
  • died signal is the gameplay-layer hook; ally_defeated is the squad hook.

8. Tunable matrices — concrete ranges per archetype

These are the base values; the spawn-time personality_jitter (default 0.15) widens them further. Set base values via @export in the scene’s inspector, not hard-coded in script. That keeps the same enemy_ai.gd reusable across archetypes.

9. Verify

If the enemy doesn’t aggro, common causes:
  • Player isn’t in player group.
  • Vision Area3D’s shape was never assigned, so its radius writes errored with resource is null.
  • Sight RayCast3D enabled = false or collide_with_areas = true blocking on its own Vision area.
  • Personality jitter pushed aggression close to 0 — check inspector values.

Common mistakes

Anti-patterns

  • “AI that dodges everything.” Enemies that 100% dodge feel cheap. A caution knob plus randomized reaction time (50–250 ms) reads as skill, not aimbot.
  • Boss with 12 unique mechanics. Cap at 4–6 distinct mechanics across phases. Players can’t track more.
  • Companion that walks through doors. NavigationAgent3D with proper navmesh. Don’t fake it with raw move_toward.
  • Civilian that screams forever. Add a calm-down state on a 5–10 sec timer.
  • Setting current_intent from inside an action callback. Intent is computed by the intent layer from inputs (perception + personality + group). Letting action callbacks write intent inverts the architecture.

Collaborative protocol

This skill writes scenes and scripts. Always ask before each step. Group related writes into one ask.

Want a working starter?

For an animated NPC in a runnable project: summer create 3d-fps-simple-animated-npc. Run summer list templates for the current list.

See also

  • animation-tree — wire the action states to animation clips
  • ai-characters-in-games — NPCs that answer free-form player text
  • vfx-hit-spark — hit flash recipe
  • vfx-dissolve — death dissolve recipe
  • game-feel — screen-feel discipline (camera shake, hit-stop)
  • gdscript-patterns — generic state machine and script patterns

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@latest 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.
  • game-feel: Add juice: hit-flash, trauma camera shake and audio ducking, one hit fires all three.
  • vfx-dissolve: Dissolve effect: a mesh burns away with a glowing edge, from a noise-threshold ShaderMaterial.
  • vfx-hit-spark: Hit-spark effect: a one-shot burst of additive particles oriented to the surface, fired on impact.

Source: library/skills/design-npc/SKILL.md, rendered from summer-engine@4.3.0 on npm.