Design Npc logo

Design Npc

Organization
SummerEngine
design-npc

Design enemy, NPC, boss or companion behavior: perception, state machine, telegraphs, script stub.

Overview

PublisherSummerEngine
Repositorysummer
Skill namedesign-npc
Stars
91
Forks
8
Bundled files
1
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by SummerEngine on GitHub. Read the source before you install it.

Installation

Install the Design Npc AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/SummerEngine/summer/tree/main/library/skills/design-npc
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/SummerEngine/summer.git /tmp/summer
mkdir -p .claude/skills
cp -r /tmp/summer/library/skills/design-npc .claude/skills/design-npc
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Design Npc in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Design Npc on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Design Npc is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

/design-npc — Design an NPC's Behavior End-to-End

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:

ArchetypeAction statesIntentsPerceptionPersonality range
Basic enemy4 (calm / alert / aggressive / defeated)5 (idle / investigate / hunt / kill / retreat)Sight cone + LOS raycastWide
Boss6–10 (per phase)5–7 incl. RESCUE_ALLYSight + hearing + scriptedNarrow, biased high
Quest-giver2 (idle / talking)1 (idle)Trigger areaNone — fixed
Companion54 (follow / support / engage / retreat)Sight + groupNarrow, biased low aggression
Civilian3 (wander / panic / dead)2 (idle / retreat)HearingCaution biased high
Wave mob2 (charge / dead)1 (kill)Direct line to playerNone

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.

SensorNodeCostUse case
Sight coneRayCast3D + dot product checkCheapStandard enemy
Sphere proximityArea3D with sphere shapeCheap"Felt your presence" / aggro range
Hearing eventSignal from world eventsCheapestAlert nearby NPCs
Group awarenessShared signal bus (autoload)CheapPack tactics, rescue intent

Recommended setup for a basic enemy:

Enemy (CharacterBody3D)
├── MeshInstance3D
├── CollisionShape3D
├── Vision (Area3D)        # sphere shape, radius = sight range
│   └── CollisionShape3D
└── Sight (RayCast3D)      # confirms LOS for candidates inside Vision

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.

KnobWhat it controlsHigh value reads asLow value reads as
aggressionHow fast the intent layer escalates to KILL; shorter pre-attack hesitationBerserker that closes distanceCautious skirmisher that waits
patienceHow long the NPC stays in INVESTIGATE before giving up; tolerance for circling without attackingWill hunt the player across the levelLoses interest quickly, returns to patrol
cautionHow eagerly it breaks line-of-sight when hurt; how often it backs off after a hit tradeKites and uses coverTrades blows recklessly
punishmentProbability of swapping intent to KILL when the player is recovering, reloading, healingPunishes whiffs and animation locksLets the player breathe

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:

IntentWhenEffect on action SM
IDLENo target, no patrol routeAction SM stays in Calm
PATROLNo target, route existsCalm, walks the path
INVESTIGATELost LOS, within memory windowAlert, moves to last-known position
HUNTLOS, out of attack rangeAlert/Aggressive, closes distance
KILLLOS, in attack range, pressure builtAggressive, executes attack
RETREATLow HP and high caution, OR over-extendedAlert, moves away, breaks LOS
RESCUE_ALLYAllied NPC emitted needs_helpAlert, moves to ally

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.

gdscript
enum State { CALM, ALERT, AGGRESSIVE, DEFEATED }
StateReads intentAnimationAudioVFX
CALMIDLE, PATROLBreathing loop, look-aroundAmbientNone
ALERTINVESTIGATE, HUNT, RETREAT, RESCUE_ALLYPosture up, head tracks target"Huh?" / radioEye glow
AGGRESSIVEKILLCombat pose, weapon raised, swing windupBattle cryMuzzle flash / windup glow
DEFEATED(terminal)Death anim → ragdollDeath soundLoot drop / dissolve

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:

gdscript
func attack_windup_time() -> float:
    # high aggression = shorter hesitation, but never below 0.25
    return maxf(0.25, attack_telegraph * (1.5 - aggression))

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:

InstanceaggressionEmergent roleWhy
10.3SniperSlow to engage, stays at range, builds pressure slowly
20.5AnchorHolds the line, neither rushes nor retreats
30.7FlankerCloses mid-fight, looks for openings
40.9BerserkerRushes immediately, short windups

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

summer_get_scene_tree
summer_inspect_node "./World"

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:

summer_run_script(scenePath="res://main.tscn", source="""
func run(ctx):
    var enemy = ctx.add_node("CharacterBody3D", "Enemy", ctx.find("Enemies"))
    ctx.add_node("CollisionShape3D", "Body", enemy, {"shape": CapsuleShape3D.new()})
    ctx.add_node("MeshInstance3D", "Mesh", enemy, {"mesh": CapsuleMesh.new()})
    var vision = ctx.add_node("Area3D", "Vision", enemy)
    var sight_sphere = SphereShape3D.new()
    sight_sphere.radius = 12.0
    ctx.add_node("CollisionShape3D", "VisionShape", vision, {"shape": sight_sphere})
    ctx.add_node("RayCast3D", "Sight", enemy, {"enabled": true, "target_position": Vector3(0, 0, -12)})
    ctx.save_scene()
""")

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(scenePath="res://main.tscn", nodePath="./World/Enemies/Enemy/Vision/VisionShape", resourceProperty="shape", subProperty="radius", value="14.0")

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:

summer_connect_signal(scenePath="res://main.tscn", emitter="./World/Enemies/Enemy/Vision", signal="body_entered", receiver="./World/Enemies/Enemy", method="_on_body_entered")

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:

gdscript
class_name EnemyAI
extends CharacterBody3D

enum State { CALM, ALERT, AGGRESSIVE, DEFEATED }
enum Intent { IDLE, PATROL, INVESTIGATE, HUNT, KILL, RETREAT, RESCUE_ALLY }

# Tuning
@export var move_speed: float = 3.5
@export var attack_range: float = 2.0
@export var attack_telegraph: float = 0.5
@export var attack_damage: int = 10
@export var max_health: int = 30
@export var memory_duration: float = 8.0

# Personality (base, jittered at spawn)
@export_range(0.0, 1.0) var aggression: float = 0.5
@export_range(0.0, 1.0) var patience: float = 0.5
@export_range(0.0, 1.0) var caution: float = 0.3
@export_range(0.0, 1.0) var punishment: float = 0.5
@export var personality_jitter: float = 0.15

@onready var sight: RayCast3D = $Sight
@onready var vision: Area3D = $Vision
@onready var anim: AnimationPlayer = $AnimationPlayer

signal died(position: Vector3)
signal needs_help(who: Node3D)
signal ally_defeated(who: Node3D)

var _state: State = State.CALM
var current_intent: Intent = Intent.IDLE
var _target: Node3D = null
var _last_seen_pos: Vector3 = Vector3.ZERO
var _memory_timer: float = 0.0
var _intent_timer: float = 0.0
var _aggression_buildup: float = 0.0
var _retreat_desire: float = 0.0
var _health: int = 30

func _ready() -> void:
    _health = max_health
    aggression = clampf(aggression + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
    patience   = clampf(patience   + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
    caution    = clampf(caution    + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
    punishment = clampf(punishment + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
    vision.body_entered.connect(_on_body_entered)

func _physics_process(delta: float) -> void:
    if _state == State.DEFEATED:
        return
    _update_memory(delta)
    _update_intent(delta)
    _update_action(delta)
    move_and_slide()

# --- Intent layer (above action SM) -----------------------------------------

func _update_intent(delta: float) -> void:
    _intent_timer += delta
    if _state == State.AGGRESSIVE:
        _aggression_buildup += delta * (1.0 - patience) * 0.5
    else:
        _aggression_buildup = maxf(_aggression_buildup - delta * 0.3, 0.0)
    _retreat_desire = maxf(_retreat_desire - delta * 0.4, 0.0)

    var prev: Intent = current_intent
    current_intent = _decide_intent()
    if current_intent != prev:
        _intent_timer = 0.0

func _decide_intent() -> Intent:
    if _target == null:
        return Intent.IDLE
    var dist: float = global_position.distance_squared_to(_target.global_position)
    var attack_sq: float = attack_range * attack_range
    if _retreat_desire > 0.5:
        return Intent.RETREAT
    if _aggression_buildup > (2.0 - aggression) and dist < attack_sq:
        return Intent.KILL
    if not _has_line_of_sight(_target):
        return Intent.INVESTIGATE if _intent_timer < (4.0 + patience * 6.0) else Intent.IDLE
    if dist < attack_sq * 4.0:
        return Intent.HUNT
    return Intent.HUNT

# --- Action SM (reads intent) -----------------------------------------------

func _update_action(delta: float) -> void:
    match current_intent:
        Intent.IDLE, Intent.PATROL:
            _enter_state(State.CALM)
            velocity = Vector3.ZERO
        Intent.INVESTIGATE:
            _enter_state(State.ALERT)
            _move_toward(_last_seen_pos, delta)
        Intent.HUNT:
            _enter_state(State.ALERT)
            if _target != null:
                _move_toward(_target.global_position, delta)
        Intent.KILL:
            _enter_state(State.AGGRESSIVE)
            velocity = Vector3.ZERO
            _try_attack()
        Intent.RETREAT:
            _enter_state(State.ALERT)
            if _target != null:
                _move_toward(global_position * 2.0 - _target.global_position, delta)
        Intent.RESCUE_ALLY:
            _enter_state(State.ALERT)
            # locomotion handled by ally-position lookup elsewhere

func _enter_state(new_state: State) -> void:
    if _state == new_state:
        return
    _state = new_state
    match new_state:
        State.CALM:
            anim.play("idle")
        State.ALERT:
            anim.play("alert")
        State.AGGRESSIVE:
            anim.play("ready")
        State.DEFEATED:
            anim.play("death")
            died.emit(global_position)
            ally_defeated.emit(self)

# --- Helpers ----------------------------------------------------------------

func _move_toward(target_pos: Vector3, _delta: float) -> void:
    var dir: Vector3 = (target_pos - global_position).normalized()
    velocity = dir * move_speed
    look_at(target_pos, Vector3.UP)

func _try_attack() -> void:
    if _state != State.AGGRESSIVE or _target == null:
        return
    anim.play("attack_windup")
    var windup: float = maxf(0.25, attack_telegraph * (1.5 - aggression))
    await get_tree().create_timer(windup).timeout
    if _state != State.AGGRESSIVE or _target == null:
        return
    if global_position.distance_squared_to(_target.global_position) < (attack_range * 1.2) * (attack_range * 1.2):
        if _target.has_method("take_damage"):
            _target.take_damage(attack_damage)
    anim.play("attack_recover")

func _update_memory(delta: float) -> void:
    if _target != null and _has_line_of_sight(_target):
        _last_seen_pos = _target.global_position
        _memory_timer = memory_duration
    else:
        _memory_timer = maxf(_memory_timer - delta, 0.0)
        if _memory_timer <= 0.0:
            _target = null

func _has_line_of_sight(target: Node3D) -> bool:
    sight.target_position = sight.to_local(target.global_position)
    sight.force_raycast_update()
    if not sight.is_colliding():
        return true
    return sight.get_collider() == target

func _on_body_entered(body: Node3D) -> void:
    if body.is_in_group("player") and _state == State.CALM:
        _target = body
        _last_seen_pos = body.global_position
        _memory_timer = memory_duration

func take_damage(amount: int) -> void:
    if _state == State.DEFEATED:
        return
    _health -= amount
    var hp_ratio: float = float(_health) / float(max_health)
    if randf() < caution * (1.0 - hp_ratio):
        _retreat_desire = 1.0
    if _health <= 0:
        _enter_state(State.DEFEATED)
        await get_tree().create_timer(2.0).timeout
        queue_free()

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.

ArchetypeaggressionpatiencecautionpunishmentNotes
basic_grunt0.5–0.70.3–0.50.2–0.40.4–0.6Wants to engage; not too smart
ranged_skirmisher0.3–0.50.5–0.70.6–0.80.5–0.7Kites, breaks LOS, punishes reload
heavy_brute0.7–0.90.2–0.40.1–0.30.6–0.8Closes fast, low retreat
boss0.7–0.90.7–0.90.5–0.70.8–1.0Patient, punishing, never panics
wave_mob0.9–1.00.0–0.10.00.0Single intent: KILL
civilian0.00.8–1.00.7–0.90.0Retreat-only
companion0.4–0.60.6–0.80.5–0.70.3–0.5Supports, doesn't over-commit

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

summer_save_scene(scenePath="res://main.tscn")
summer_get_script_errors(path="res://scripts/enemy_ai.gd")   # one file per call; path required
summer_play
# user tests
summer_stop

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

Don'tDoWhy
360° perceptionSight cone or sphere with raycast LOS"Sees through walls" feels unfair
Instant attack0.3–0.7 sec telegraph, scaled by aggressionPlayer needs to read the swing
All NPCs identical at spawnJitter personality knobs in _readyIdentical NPCs are predictable; predictable is boring
Action SM writes intentIntent layer writes intent, action readsTangled SMs are unmaintainable
One enum for action + intentTwo enums, two SMsDifferent time scales, different inputs
Hard-coded role per spawn ("this is the sniper")Let personality variation produce rolesSquad emergence comes for free; authored roles fight the system
Behavior tree for a gruntAction SM with 4 states + intent layerBT overhead unjustified at this complexity
Per-frame group lookupCache target on aggro, listen for ally_defeatedTree walks aren't free
distance_to in _physics_processdistance_squared_to for comparisonsSquare root costs at scale
Same animation in all statesDistinct anim per action stateThe state machine is invisible; animation makes it visible
queue_free() instantly on deathDeath anim → 2 sec → freeInstant despawn breaks immersion + skips loot drops
Setting shape's radius before shape holds a resourceAssign the shape, then set sub-propertiesSetResourceProperty errors with resource is null when the property is empty
100+ enemies with full FSMWave-mob archetype (no intent layer, single KILL intent)Cheap mobs need cheap brains
No died / ally_defeated signalsEmit bothGameplay + squad logic both need hooks

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
  • summer-router — NPCs that answer free-form player text in a game on Summer Games
  • 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.

In this skillCommand
summer_add_nodesummer engine add-node
summer_connect_signalsummer engine connect-signal
summer_get_scene_treesummer engine get-scene-tree
summer_get_script_errorssummer engine get-script-errors
summer_input_map_bindsummer engine input-map-bind
summer_inspect_nodesummer engine inspect-node
summer_playsummer engine play
summer_run_scriptsummer engine run-script
summer_save_scenesummer engine save-scene
summer_set_propsummer engine set-prop
summer_set_resource_propertysummer engine set-resource-property
summer_stopsummer engine stop

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Design Npc AI skill do?

Design enemy, NPC, boss or companion behavior: perception, state machine, telegraphs, script stub.

Why use Design Npc on TypingMind?

Because you install it once and use it with any model. Design Npc is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Design Npc in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/SummerEngine/summer/tree/main/library/skills/design-npc. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Design Npc?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Design Npc?

As many as you like. As long as a model supports skills, you can use Design Npc with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Design Npc AI skill free?

Yes. It is published on GitHub by SummerEngine under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇