summer-engine 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.claude-plugin/plugin.json +18 -10
  2. package/.codex-plugin/plugin.json +36 -8
  3. package/.cursor-plugin/plugin.json +19 -11
  4. package/.opencode/INSTALL.md +69 -0
  5. package/.opencode/plugins/summer.js +9 -4
  6. package/CHANGELOG.md +31 -1
  7. package/README.md +268 -86
  8. package/dist/bin/summer.js +0 -0
  9. package/dist/commands/assets.d.ts +2 -0
  10. package/dist/commands/assets.js +189 -0
  11. package/dist/commands/create.js +55 -17
  12. package/dist/commands/debug.d.ts +2 -0
  13. package/dist/commands/debug.js +127 -0
  14. package/dist/commands/list.js +24 -5
  15. package/dist/commands/project.d.ts +2 -0
  16. package/dist/commands/project.js +170 -0
  17. package/dist/commands/scene.d.ts +2 -0
  18. package/dist/commands/scene.js +290 -0
  19. package/dist/lib/banner.js +2 -2
  20. package/dist/lib/log-filters.d.ts +53 -0
  21. package/dist/lib/log-filters.js +130 -0
  22. package/dist/lib/plugin-manifests.test.d.ts +1 -0
  23. package/dist/lib/plugin-manifests.test.js +134 -0
  24. package/dist/lib/remote-templates.d.ts +25 -0
  25. package/dist/lib/remote-templates.js +86 -0
  26. package/dist/lib/skills-registry.d.ts +33 -1
  27. package/dist/lib/skills-registry.js +56 -1
  28. package/dist/lib/telemetry.d.ts +13 -0
  29. package/dist/lib/telemetry.js +63 -0
  30. package/dist/lib/with-engine.d.ts +14 -0
  31. package/dist/lib/with-engine.js +109 -0
  32. package/dist/mcp/tools/asset-tools.js +12 -13
  33. package/dist/mcp/tools/debug-tools.js +32 -9
  34. package/dist/mcp/tools/with-engine.js +4 -0
  35. package/gemini-extension.json +2 -2
  36. package/hooks/pre-commit-doctor.sh +0 -0
  37. package/hooks/run-hook.cmd +0 -0
  38. package/hooks/session-start.sh +0 -0
  39. package/marketplace.json +2 -2
  40. package/package.json +3 -3
  41. package/skills/README.md +1 -1
  42. package/skills/_meta/skill-create/SKILL.md +1 -1
  43. package/skills/_meta/skill-improve/SKILL.md +1 -1
  44. package/skills/_meta/skill-test/SKILL.md +1 -1
  45. package/skills/_meta/using-summer/SKILL.md +130 -0
  46. package/skills/_tests/specs/design-npc.md +43 -12
  47. package/skills/_tests/specs/fps-controller.md +33 -20
  48. package/skills/_tests/specs/host-authoritative-state.md +104 -0
  49. package/skills/_tests/specs/vfx.md +56 -38
  50. package/skills/ai-and-npcs/design-npc/SKILL.md +264 -122
  51. package/skills/asset-pipeline/asset-strategy/SKILL.md +1 -1
  52. package/skills/catalog.yaml +21 -4
  53. package/skills/character-controllers/fps-controller/SKILL.md +260 -79
  54. package/skills/debugging/debug/SKILL.md +102 -82
  55. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +332 -0
  56. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
  57. package/skills/scene-and-project/browse-templates/SKILL.md +105 -0
  58. package/skills/scene-and-project/make-game/SKILL.md +159 -346
  59. package/skills/scene-and-project/new-project/SKILL.md +106 -0
  60. package/skills/scene-and-project/scene-composition/SKILL.md +1 -1
  61. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
  62. package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
  63. package/skills/visual-effects/vfx/SKILL.md +233 -134
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: design-npc
3
- description: Use when the user wants to design an enemy, NPC, boss, companion, civilian, or wave-spawned mob's behavior. Walks perception, decision logic, escalation states, animation/audio tells, and defeat handling — outputs a state-machine GDScript stub plus the recommended node tree. Trigger on "enemy", "NPC", "boss", "companion", "AI", "behavior", "mob", "perception", "design enemy".
3
+ description: Use when the user wants to design an enemy, NPC, boss, companion, civilian, or wave-spawned mob's behavior. Walks perception, personality knobs, intent layer, action state machine, telegraphs, defeat handling, and group emergence — outputs a state-machine GDScript stub plus the recommended node tree. Trigger on "enemy", "NPC", "boss", "companion", "AI", "behavior", "mob", "perception", "design enemy".
4
4
  license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: ai-and-npcs
@@ -13,9 +13,11 @@ paths: ["**/*.gd", "**/*.tscn"]
13
13
 
14
14
  ## Overview
15
15
 
16
- A good NPC is legible: the player can read its mood, predict its next move, and feel the consequence of theirs. This skill walks the design from the inside out — what does the NPC perceive, how does it decide, what tells signal each state, and how does it die or leave the scene. Output: a state-machine GDScript stub plus the scene tree.
16
+ 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.
17
17
 
18
- **Core principle:** behavior is what the player *sees*, not what the code does. Every state needs a tell.
18
+ **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.
19
+
20
+ **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.
19
21
 
20
22
  ## Steps
21
23
 
@@ -33,30 +35,29 @@ Open with the question. Wait.
33
35
 
34
36
  Each archetype maps to a different complexity budget:
35
37
 
36
- | Archetype | States | Perception | Decision logic | Tells per state |
38
+ | Archetype | Action states | Intents | Perception | Personality range |
37
39
  |---|---|---|---|---|
38
- | Basic enemy | 4 (idle / alert / attack / dead) | Sight cone | State machine | Animation + 1 sound |
39
- | Boss | 6–10 (per phase) | Sight + hearing + scripted | Behavior tree or hand-coded FSM | Full anim suite + VO |
40
- | Quest-giver | 2 (idle / talking) | Trigger area | None / dialogue tree | Idle anim + greet |
41
- | Companion | 5 (follow / combat / wait / heal / dead) | Sight + group awareness | State machine + utility scoring | Anim + voice |
42
- | Civilian | 3 (wander / panic / dead) | Hearing (loud events) | Simple FSM | Anim + scream |
43
- | Wave mob | 2 (charge / dead) | Direct line to player | None | Anim + impact |
40
+ | Basic enemy | 4 (calm / alert / aggressive / defeated) | 5 (idle / investigate / hunt / kill / retreat) | Sight cone + LOS raycast | Wide |
41
+ | Boss | 6–10 (per phase) | 5–7 incl. `RESCUE_ALLY` | Sight + hearing + scripted | Narrow, biased high |
42
+ | Quest-giver | 2 (idle / talking) | 1 (idle) | Trigger area | None — fixed |
43
+ | Companion | 5 | 4 (follow / support / engage / retreat) | Sight + group | Narrow, biased low aggression |
44
+ | Civilian | 3 (wander / panic / dead) | 2 (idle / retreat) | Hearing | Caution biased high |
45
+ | Wave mob | 2 (charge / dead) | 1 (kill) | Direct line to player | None |
44
46
 
45
- ### 2. Sketch the four design pillars
47
+ ### 2. Walk the design pillars
46
48
 
47
- Walk them in order. Don't skip.
49
+ Walk them in order. Don't skip. The pillars are **Perception → Personality → Intent → Action → Telegraph → Defeat**.
48
50
 
49
- #### a) Perception — what does this NPC *sense*?
51
+ #### a) Perception — what does this NPC sense?
50
52
 
51
- Pick the minimum. Over-perception = unfair AI. Common sensors:
53
+ Pick the minimum. Over-perception = unfair AI.
52
54
 
53
55
  | Sensor | Node | Cost | Use case |
54
56
  |---|---|---|---|
55
57
  | Sight cone | RayCast3D + dot product check | Cheap | Standard enemy |
56
58
  | Sphere proximity | Area3D with sphere shape | Cheap | "Felt your presence" / aggro range |
57
- | Hearing event | Signal from world events (gunshot, footstep) | Cheapest (event-driven) | Alert nearby NPCs |
58
- | Group awareness | Shared signal bus (autoload) | Cheap | Pack tactics |
59
- | Scripted scripted (boss) | Manual state changes | Free | Phase transitions |
59
+ | Hearing event | Signal from world events | Cheapest | Alert nearby NPCs |
60
+ | Group awareness | Shared signal bus (autoload) | Cheap | Pack tactics, rescue intent |
60
61
 
61
62
  Recommended setup for a basic enemy:
62
63
 
@@ -66,68 +67,115 @@ Enemy (CharacterBody3D)
66
67
  ├── CollisionShape3D
67
68
  ├── Vision (Area3D) # sphere shape, radius = sight range
68
69
  │ └── CollisionShape3D
69
- └── Sight (RayCast3D) # confirms line-of-sight when target in Vision area
70
+ └── Sight (RayCast3D) # confirms LOS for candidates inside Vision
70
71
  ```
71
72
 
72
- The two-step (Area3D for proximity, RayCast3D for line-of-sight) is the right default. Don't raycast every frame to every potential target — first filter by Area3D overlap, then raycast only the candidates.
73
+ Two-step (Area3D filter, then RayCast LOS) is the right default. Don't raycast to every potential target every frame.
74
+
75
+ #### b) Personality — four floats that make this NPC *not* a clone
76
+
77
+ 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.
78
+
79
+ | Knob | What it controls | High value reads as | Low value reads as |
80
+ |---|---|---|---|
81
+ | `aggression` | How fast the intent layer escalates to `KILL`; shorter pre-attack hesitation | Berserker that closes distance | Cautious skirmisher that waits |
82
+ | `patience` | How long the NPC stays in `INVESTIGATE` before giving up; tolerance for circling without attacking | Will hunt the player across the level | Loses interest quickly, returns to patrol |
83
+ | `caution` | How eagerly it breaks line-of-sight when hurt; how often it backs off after a hit trade | Kites and uses cover | Trades blows recklessly |
84
+ | `punishment` | Probability of swapping intent to `KILL` when the player is recovering, reloading, healing | Punishes whiffs and animation locks | Lets the player breathe |
85
+
86
+ **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).
73
87
 
74
- #### b) Decision logic — state machine vs behavior tree
88
+ 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.
75
89
 
76
- **Strong recommendation:** start with a state machine. Always.
90
+ #### c) Intent layer — what the NPC is *trying* to do
77
91
 
78
- | | State machine | Behavior tree |
92
+ 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`.
93
+
94
+ Five-to-seven intents is the sweet spot:
95
+
96
+ | Intent | When | Effect on action SM |
79
97
  |---|---|---|
80
- | Best for | < 10 states, scripted bosses, basic enemies | Reactive complex AI, branching priorities |
81
- | Author cost | Low — read top to bottom | High — tree structure has its own learning curve |
82
- | Debug | Print current state | Tree visualizer needed |
83
- | Common failure | State explosion at ~15 states | Premature complexity |
98
+ | `IDLE` | No target, no patrol route | Action SM stays in `Calm` |
99
+ | `PATROL` | No target, route exists | `Calm`, walks the path |
100
+ | `INVESTIGATE` | Lost LOS, within memory window | `Alert`, moves to last-known position |
101
+ | `HUNT` | LOS, out of attack range | `Alert/Aggressive`, closes distance |
102
+ | `KILL` | LOS, in attack range, pressure built | `Aggressive`, executes attack |
103
+ | `RETREAT` | Low HP and high caution, OR over-extended | `Alert`, moves away, breaks LOS |
104
+ | `RESCUE_ALLY` | Allied NPC emitted `needs_help` | `Alert`, moves to ally |
84
105
 
85
- Use a behavior tree when:
86
- - You have 10+ states with priority interactions.
87
- - You want designers (not coders) editing logic.
88
- - The NPC has simultaneous goals (hunt + survive + cover).
106
+ The action SM's job is now narrow: given an intent, what locomotion + animation + hitbox state matches?
89
107
 
90
- For everything else: FSM. See `scripting-patterns/state-machine-patterns/SKILL.md`.
108
+ #### d) Action SM — what the body is *currently doing*
91
109
 
92
- #### c) Escalation — tells per state
110
+ Four states is plenty for a basic enemy. They are the visible mood, not the goal.
93
111
 
94
- Every state must have at least one *tell* the player can read. List them up front:
112
+ ```gdscript
113
+ enum State { CALM, ALERT, AGGRESSIVE, DEFEATED }
114
+ ```
95
115
 
96
- | State | Animation tell | Audio tell | VFX tell |
97
- |---|---|---|---|
98
- | Idle | Breathing loop, occasional look | Ambient (snore, hum) | None |
99
- | Alert | Head turn toward target, posture change | "Huh?" / radio chatter | Eye glow / aggro line |
100
- | Aggressive | Combat pose, weapon raised | Battle cry | Muzzle flash / windup glow |
101
- | Attacking | Strike anim with telegraph | Swing/shoot sound | Particle trail |
102
- | Hurt | Stagger anim | Pain sound | Hit flash (see `vfx`) |
103
- | Defeated | Death anim + ragdoll | Death sound | Loot drop / dissolve |
116
+ | State | Reads intent | Animation | Audio | VFX |
117
+ |---|---|---|---|---|
118
+ | `CALM` | `IDLE`, `PATROL` | Breathing loop, look-around | Ambient | None |
119
+ | `ALERT` | `INVESTIGATE`, `HUNT`, `RETREAT`, `RESCUE_ALLY` | Posture up, head tracks target | "Huh?" / radio | Eye glow |
120
+ | `AGGRESSIVE` | `KILL` | Combat pose, weapon raised, swing windup | Battle cry | Muzzle flash / windup glow |
121
+ | `DEFEATED` | (terminal) | Death anim → ragdoll | Death sound | Loot drop / dissolve |
104
122
 
105
- **Telegraph is non-negotiable for boss/heavy attacks:** 0.3–0.7 sec of windup the player can react to. Faster than 0.3 sec = "unfair"; slower than 0.7 sec = "boring".
123
+ 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.
106
124
 
107
- #### d) Defeat handling — what happens when it dies?
125
+ #### e) Telegraph — every aggressive action has a tell
126
+
127
+ **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:
128
+
129
+ ```gdscript
130
+ func attack_windup_time() -> float:
131
+ # high aggression = shorter hesitation, but never below 0.25
132
+ return maxf(0.25, attack_telegraph * (1.5 - aggression))
133
+ ```
134
+
135
+ 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.
136
+
137
+ #### f) Defeat — what happens when it dies
108
138
 
109
139
  Plan it. "It just disappears" is bad. Choices:
110
140
 
111
141
  - **Death animation** plays, body stays for N sec, then `queue_free`.
112
- - **Ragdoll**: switch CollisionShape from KinematicBody to RigidBody. Adds weight to the kill.
113
- - **Loot drop**: spawn a pickup at death position.
114
- - **Cleanup**: remove from group, disconnect signals, free children. Otherwise leak.
115
- - **Score / quest hook**: emit a `died` signal the gameplay layer listens for.
142
+ - **Ragdoll**: switch CollisionShape from CharacterBody3D to RigidBody3D for weight.
143
+ - **Loot drop**: spawn pickup at death position.
144
+ - **Cleanup**: leave any group, disconnect signals, free children.
145
+ - **Score / quest hook**: emit `died(global_position)` for the gameplay layer.
146
+ - **Group signal**: emit `ally_defeated` so squadmates can update their `RESCUE_ALLY` checks (or panic).
147
+
148
+ ### 3. Group emergence — varied personality across a squad
149
+
150
+ When you spawn a squad of four with the same archetype but personality jittered per-instance, you get behavioral roles for free.
151
+
152
+ Worked example — four basic_grunts with `aggression` after jitter at `0.3 / 0.5 / 0.7 / 0.9`:
153
+
154
+ | Instance | aggression | Emergent role | Why |
155
+ |---|---|---|---|
156
+ | 1 | 0.3 | Sniper | Slow to engage, stays at range, builds pressure slowly |
157
+ | 2 | 0.5 | Anchor | Holds the line, neither rushes nor retreats |
158
+ | 3 | 0.7 | Flanker | Closes mid-fight, looks for openings |
159
+ | 4 | 0.9 | Berserker | Rushes immediately, short windups |
116
160
 
117
- ### 3. Read the scene to find an attach point
161
+ 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.
162
+
163
+ **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.
164
+
165
+ ### 4. Read the scene to find an attach point
118
166
 
119
167
  ```
120
168
  summer_get_scene_tree
121
169
  summer_inspect_node "./World"
122
170
  ```
123
171
 
124
- Identify where the NPC will live (a `Enemies` parent, or directly under World). Confirm the player exists for line-of-sight.
172
+ Identify where the NPC will live (an `Enemies` parent, or directly under World). Confirm the player exists for line-of-sight.
125
173
 
126
- ### 4. Propose the bundle
174
+ ### 5. Propose the bundle
127
175
 
128
- > I'm about to design a basic patrol-and-chase enemy. Scene tree: Enemy (CharacterBody3D) + Vision Area3D + Sight RayCast3D + CollisionShape3D + MeshInstance3D. Script `scripts/enemy_ai.gd` with 4 states (idle / alert / attack / dead). Sight cone radius 12m, line-of-sight raycast. Telegraph: 0.5 sec windup before attack. Drop loot on death. May I create the scene + script?
176
+ > 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?
129
177
 
130
- ### 5. Build it
178
+ ### 6. Build it
131
179
 
132
180
  **Preferred (Summer MCP):**
133
181
 
@@ -142,113 +190,180 @@ summer_set_prop(path="./World/Enemies/Enemy/Sight", key="enabled", value="true")
142
190
  summer_set_prop(path="./World/Enemies/Enemy/Sight", key="target_position", value="Vector3(0, 0, -12)")
143
191
  ```
144
192
 
145
- Set the vision shape to a SphereShape3D with `radius = 12`. Save as standalone `.tres` (do NOT inline sub_resource — see `_shared/mcp-tools-reference.md` § "Trap"):
193
+ Save the Vision sphere as standalone `.tres` (do NOT inline sub_resource — see `_shared/mcp-tools-reference.md` § "Trap"):
146
194
 
147
195
  ```
148
- # Save shape resource externally, then:
149
196
  summer_set_prop(path="./World/Enemies/Enemy/Vision/VisionShape", key="shape", value="res://shapes/vision_sphere.tres")
150
197
  ```
151
198
 
152
- Connect the area signal:
199
+ Connect signals:
153
200
 
154
201
  ```
155
- summer_connect_signal(from="./World/Enemies/Enemy/Vision", signal="body_entered", to="./World/Enemies/Enemy", method="_on_target_seen")
202
+ summer_connect_signal(from="./World/Enemies/Enemy/Vision", signal="body_entered", to="./World/Enemies/Enemy", method="_on_body_entered")
156
203
  ```
157
204
 
158
- **Fallback (no MCP):** write the scene as `.tscn` text. Ask user to paste their existing scene first, then propose a unified diff.
205
+ **Fallback (no MCP):** write the scene as `.tscn` text. Ask the user to paste their existing scene first, then propose a unified diff.
159
206
 
160
- ### 6. Drop the script
207
+ ### 7. Drop the script
161
208
 
162
- `scripts/enemy_ai.gd` (FSM stub):
209
+ `scripts/enemy_ai.gd`:
163
210
 
164
211
  ```gdscript
165
212
  class_name EnemyAI
166
213
  extends CharacterBody3D
167
214
 
168
- enum State { IDLE, ALERT, ATTACK, DEAD }
215
+ enum State { CALM, ALERT, AGGRESSIVE, DEFEATED }
216
+ enum Intent { IDLE, PATROL, INVESTIGATE, HUNT, KILL, RETREAT, RESCUE_ALLY }
169
217
 
218
+ # Tuning
170
219
  @export var move_speed: float = 3.5
171
220
  @export var attack_range: float = 2.0
172
221
  @export var attack_telegraph: float = 0.5
173
222
  @export var attack_damage: int = 10
174
223
  @export var max_health: int = 30
224
+ @export var memory_duration: float = 8.0
225
+
226
+ # Personality (base, jittered at spawn)
227
+ @export_range(0.0, 1.0) var aggression: float = 0.5
228
+ @export_range(0.0, 1.0) var patience: float = 0.5
229
+ @export_range(0.0, 1.0) var caution: float = 0.3
230
+ @export_range(0.0, 1.0) var punishment: float = 0.5
231
+ @export var personality_jitter: float = 0.15
175
232
 
176
233
  @onready var sight: RayCast3D = $Sight
177
234
  @onready var vision: Area3D = $Vision
178
235
  @onready var anim: AnimationPlayer = $AnimationPlayer
179
236
 
180
237
  signal died(position: Vector3)
238
+ signal needs_help(who: Node3D)
239
+ signal ally_defeated(who: Node3D)
181
240
 
182
- var _state: State = State.IDLE
241
+ var _state: State = State.CALM
242
+ var current_intent: Intent = Intent.IDLE
183
243
  var _target: Node3D = null
244
+ var _last_seen_pos: Vector3 = Vector3.ZERO
245
+ var _memory_timer: float = 0.0
246
+ var _intent_timer: float = 0.0
247
+ var _aggression_buildup: float = 0.0
248
+ var _retreat_desire: float = 0.0
184
249
  var _health: int = 30
185
250
 
186
251
  func _ready() -> void:
187
252
  _health = max_health
253
+ aggression = clampf(aggression + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
254
+ patience = clampf(patience + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
255
+ caution = clampf(caution + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
256
+ punishment = clampf(punishment + randf_range(-personality_jitter, personality_jitter), 0.0, 1.0)
188
257
  vision.body_entered.connect(_on_body_entered)
189
258
 
190
259
  func _physics_process(delta: float) -> void:
191
- match _state:
192
- State.IDLE:
193
- _process_idle(delta)
194
- State.ALERT:
195
- _process_alert(delta)
196
- State.ATTACK:
197
- _process_attack(delta)
198
- State.DEAD:
199
- pass
260
+ if _state == State.DEFEATED:
261
+ return
262
+ _update_memory(delta)
263
+ _update_intent(delta)
264
+ _update_action(delta)
200
265
  move_and_slide()
201
266
 
202
- func _process_idle(_delta: float) -> void:
203
- velocity = Vector3.ZERO
204
- # transition handled by _on_body_entered
267
+ # --- Intent layer (above action SM) -----------------------------------------
205
268
 
206
- func _process_alert(_delta: float) -> void:
207
- if _target == null:
208
- _change_state(State.IDLE)
209
- return
210
- if not _has_line_of_sight(_target):
211
- _change_state(State.IDLE)
212
- _target = null
213
- return
214
- var dir := (_target.global_position - global_position).normalized()
215
- velocity = dir * move_speed
216
- look_at(_target.global_position, Vector3.UP)
217
- if global_position.distance_squared_to(_target.global_position) < attack_range * attack_range:
218
- _change_state(State.ATTACK)
269
+ func _update_intent(delta: float) -> void:
270
+ _intent_timer += delta
271
+ if _state == State.AGGRESSIVE:
272
+ _aggression_buildup += delta * (1.0 - patience) * 0.5
273
+ else:
274
+ _aggression_buildup = maxf(_aggression_buildup - delta * 0.3, 0.0)
275
+ _retreat_desire = maxf(_retreat_desire - delta * 0.4, 0.0)
219
276
 
220
- func _process_attack(_delta: float) -> void:
221
- velocity = Vector3.ZERO
222
- # attack handled in _change_state via async telegraph
277
+ var prev: Intent = current_intent
278
+ current_intent = _decide_intent()
279
+ if current_intent != prev:
280
+ _intent_timer = 0.0
223
281
 
224
- func _change_state(new_state: State) -> void:
282
+ func _decide_intent() -> Intent:
283
+ if _target == null:
284
+ return Intent.IDLE
285
+ var dist: float = global_position.distance_squared_to(_target.global_position)
286
+ var attack_sq: float = attack_range * attack_range
287
+ if _retreat_desire > 0.5:
288
+ return Intent.RETREAT
289
+ if _aggression_buildup > (2.0 - aggression) and dist < attack_sq:
290
+ return Intent.KILL
291
+ if not _has_line_of_sight(_target):
292
+ return Intent.INVESTIGATE if _intent_timer < (4.0 + patience * 6.0) else Intent.IDLE
293
+ if dist < attack_sq * 4.0:
294
+ return Intent.HUNT
295
+ return Intent.HUNT
296
+
297
+ # --- Action SM (reads intent) -----------------------------------------------
298
+
299
+ func _update_action(delta: float) -> void:
300
+ match current_intent:
301
+ Intent.IDLE, Intent.PATROL:
302
+ _enter_state(State.CALM)
303
+ velocity = Vector3.ZERO
304
+ Intent.INVESTIGATE:
305
+ _enter_state(State.ALERT)
306
+ _move_toward(_last_seen_pos, delta)
307
+ Intent.HUNT:
308
+ _enter_state(State.ALERT)
309
+ if _target != null:
310
+ _move_toward(_target.global_position, delta)
311
+ Intent.KILL:
312
+ _enter_state(State.AGGRESSIVE)
313
+ velocity = Vector3.ZERO
314
+ _try_attack()
315
+ Intent.RETREAT:
316
+ _enter_state(State.ALERT)
317
+ if _target != null:
318
+ _move_toward(global_position * 2.0 - _target.global_position, delta)
319
+ Intent.RESCUE_ALLY:
320
+ _enter_state(State.ALERT)
321
+ # locomotion handled by ally-position lookup elsewhere
322
+
323
+ func _enter_state(new_state: State) -> void:
225
324
  if _state == new_state:
226
325
  return
227
326
  _state = new_state
228
327
  match new_state:
229
- State.IDLE:
328
+ State.CALM:
230
329
  anim.play("idle")
231
330
  State.ALERT:
232
331
  anim.play("alert")
233
- State.ATTACK:
234
- _execute_attack()
235
- State.DEAD:
332
+ State.AGGRESSIVE:
333
+ anim.play("ready")
334
+ State.DEFEATED:
236
335
  anim.play("death")
237
336
  died.emit(global_position)
238
- await get_tree().create_timer(2.0).timeout
239
- queue_free()
337
+ ally_defeated.emit(self)
338
+
339
+ # --- Helpers ----------------------------------------------------------------
340
+
341
+ func _move_toward(target_pos: Vector3, _delta: float) -> void:
342
+ var dir: Vector3 = (target_pos - global_position).normalized()
343
+ velocity = dir * move_speed
344
+ look_at(target_pos, Vector3.UP)
240
345
 
241
- func _execute_attack() -> void:
346
+ func _try_attack() -> void:
347
+ if _state != State.AGGRESSIVE or _target == null:
348
+ return
242
349
  anim.play("attack_windup")
243
- await get_tree().create_timer(attack_telegraph).timeout
244
- if _state != State.ATTACK or _target == null:
350
+ var windup: float = maxf(0.25, attack_telegraph * (1.5 - aggression))
351
+ await get_tree().create_timer(windup).timeout
352
+ if _state != State.AGGRESSIVE or _target == null:
245
353
  return
246
- if global_position.distance_squared_to(_target.global_position) < (attack_range * 1.2) ** 2:
354
+ if global_position.distance_squared_to(_target.global_position) < (attack_range * 1.2) * (attack_range * 1.2):
247
355
  if _target.has_method("take_damage"):
248
356
  _target.take_damage(attack_damage)
249
357
  anim.play("attack_recover")
250
- await anim.animation_finished
251
- _change_state(State.ALERT)
358
+
359
+ func _update_memory(delta: float) -> void:
360
+ if _target != null and _has_line_of_sight(_target):
361
+ _last_seen_pos = _target.global_position
362
+ _memory_timer = memory_duration
363
+ else:
364
+ _memory_timer = maxf(_memory_timer - delta, 0.0)
365
+ if _memory_timer <= 0.0:
366
+ _target = null
252
367
 
253
368
  func _has_line_of_sight(target: Node3D) -> bool:
254
369
  sight.target_position = sight.to_local(target.global_position)
@@ -258,26 +373,49 @@ func _has_line_of_sight(target: Node3D) -> bool:
258
373
  return sight.get_collider() == target
259
374
 
260
375
  func _on_body_entered(body: Node3D) -> void:
261
- if body.is_in_group("player") and _state == State.IDLE:
376
+ if body.is_in_group("player") and _state == State.CALM:
262
377
  _target = body
263
- _change_state(State.ALERT)
378
+ _last_seen_pos = body.global_position
379
+ _memory_timer = memory_duration
264
380
 
265
381
  func take_damage(amount: int) -> void:
266
- if _state == State.DEAD:
382
+ if _state == State.DEFEATED:
267
383
  return
268
384
  _health -= amount
385
+ var hp_ratio: float = float(_health) / float(max_health)
386
+ if randf() < caution * (1.0 - hp_ratio):
387
+ _retreat_desire = 1.0
269
388
  if _health <= 0:
270
- _change_state(State.DEAD)
389
+ _enter_state(State.DEFEATED)
390
+ await get_tree().create_timer(2.0).timeout
391
+ queue_free()
271
392
  ```
272
393
 
273
394
  Key design notes embedded above:
274
- - Telegraph (`attack_telegraph`) is a parameter — designers can tune feel.
275
- - `take_damage` is the public hook the player's weapon calls.
276
- - `died` signal is the gameplay-layer hook (loot, score, quest progress).
277
- - `distance_squared_to` for comparisons (no sqrt cost).
278
- - LOS check uses `force_raycast_update` for one-shot precision.
395
+ - `aggression`, `patience`, `caution`, `punishment` are tuned per-archetype and jittered per-instance.
396
+ - Intent SM (`_update_intent` / `_decide_intent`) sits above the action SM (`_update_action`).
397
+ - Action SM reads `current_intent`. It does not write intent.
398
+ - Telegraph length scales with aggression — angrier NPCs swing faster.
399
+ - Damage triggers a `caution * (1 - hp_ratio)` chance to set `_retreat_desire`, so cautious + hurt = back off.
400
+ - `died` signal is the gameplay-layer hook; `ally_defeated` is the squad hook.
279
401
 
280
- ### 7. Verify
402
+ ### 8. Tunable matrices — concrete ranges per archetype
403
+
404
+ These are the *base* values; the spawn-time `personality_jitter` (default 0.15) widens them further.
405
+
406
+ | Archetype | aggression | patience | caution | punishment | Notes |
407
+ |---|---|---|---|---|---|
408
+ | `basic_grunt` | 0.5–0.7 | 0.3–0.5 | 0.2–0.4 | 0.4–0.6 | Wants to engage; not too smart |
409
+ | `ranged_skirmisher` | 0.3–0.5 | 0.5–0.7 | 0.6–0.8 | 0.5–0.7 | Kites, breaks LOS, punishes reload |
410
+ | `heavy_brute` | 0.7–0.9 | 0.2–0.4 | 0.1–0.3 | 0.6–0.8 | Closes fast, low retreat |
411
+ | `boss` | 0.7–0.9 | 0.7–0.9 | 0.5–0.7 | 0.8–1.0 | Patient, punishing, never panics |
412
+ | `wave_mob` | 0.9–1.0 | 0.0–0.1 | 0.0 | 0.0 | Single intent: KILL |
413
+ | `civilian` | 0.0 | 0.8–1.0 | 0.7–0.9 | 0.0 | Retreat-only |
414
+ | `companion` | 0.4–0.6 | 0.6–0.8 | 0.5–0.7 | 0.3–0.5 | Supports, doesn't over-commit |
415
+
416
+ 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.
417
+
418
+ ### 9. Verify
281
419
 
282
420
  ```
283
421
  summer_save_scene
@@ -291,28 +429,34 @@ If the enemy doesn't aggro, common causes:
291
429
  - Player isn't in `player` group.
292
430
  - Vision Area3D's sphere shape is inline sub_resource (silent fail).
293
431
  - Sight RayCast3D `enabled = false` or `collide_with_areas = true` blocking on its own Vision area.
432
+ - Personality jitter pushed `aggression` close to 0 — check inspector values.
294
433
 
295
434
  ## Common mistakes
296
435
 
297
436
  | Don't | Do | Why |
298
437
  |---|---|---|
299
438
  | 360° perception | Sight cone or sphere with raycast LOS | "Sees through walls" feels unfair |
300
- | Instant attack | 0.3–0.7 sec telegraph | Player needs to read the swing |
301
- | Behavior tree for a grunt | State machine with 4 states | BT overhead unjustified at low complexity |
302
- | Per-frame `get_tree().get_first_node_in_group("player")` | Cache target on aggro | Tree walks aren't free |
439
+ | Instant attack | 0.3–0.7 sec telegraph, scaled by aggression | Player needs to read the swing |
440
+ | All NPCs identical at spawn | Jitter personality knobs in `_ready` | Identical NPCs are predictable; predictable is boring |
441
+ | Action SM writes intent | Intent layer writes intent, action reads | Tangled SMs are unmaintainable |
442
+ | One enum for action + intent | Two enums, two SMs | Different time scales, different inputs |
443
+ | Hard-coded role per spawn ("this is the sniper") | Let personality variation produce roles | Squad emergence comes for free; authored roles fight the system |
444
+ | Behavior tree for a grunt | Action SM with 4 states + intent layer | BT overhead unjustified at this complexity |
445
+ | Per-frame group lookup | Cache target on aggro, listen for `ally_defeated` | Tree walks aren't free |
303
446
  | `distance_to` in `_physics_process` | `distance_squared_to` for comparisons | Square root costs at scale |
304
- | Same animation in all states | Distinct animations per state | The state machine is invisible; animations make it visible |
447
+ | Same animation in all states | Distinct anim per action state | The state machine is invisible; animation makes it visible |
305
448
  | `queue_free()` instantly on death | Death anim → 2 sec → free | Instant despawn breaks immersion + skips loot drops |
306
449
  | Inline SphereShape3D for Vision Area3D | Standalone `.tres` | Inline sub_resources break SetResourceProperty |
307
- | 100+ enemies with full FSM | Wave-mob archetype (2 states, no LOS) | Cheap mobs need cheap brains |
308
- | No `died` signal | Emit on death | Gameplay layer needs the hook |
450
+ | 100+ enemies with full FSM | Wave-mob archetype (no intent layer, single KILL intent) | Cheap mobs need cheap brains |
451
+ | No `died` / `ally_defeated` signals | Emit both | Gameplay + squad logic both need hooks |
309
452
 
310
453
  ## Anti-patterns
311
454
 
312
- - **"AI that dodges everything."** Enemies that 100% dodge feel cheap. Random reaction time (50–250 ms) reads as skill, not aimbot.
455
+ - **"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.
313
456
  - **Boss with 12 unique mechanics.** Cap at 4–6 distinct mechanics across phases. Players can't track more.
314
457
  - **Companion that walks through doors.** NavigationAgent3D with proper navmesh. Don't fake it with raw `move_toward`.
315
458
  - **Civilian that screams forever.** Add a calm-down state on a 5–10 sec timer.
459
+ - **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.
316
460
 
317
461
  ## Collaborative protocol
318
462
 
@@ -323,9 +467,7 @@ This skill writes scenes and scripts. Always ask before each step. Group related
323
467
  For a fully-rigged enemy with anim controller + loot drop:
324
468
 
325
469
  → **template-id**: `template-3d-fps` — ships with a sample patrol enemy
326
- → **Repo**: https://github.com/SummerEngine/FPS-template-Summer-Engine
327
-
328
- For higher-complexity AI (boss with phases, behavior trees), defer to `ai-and-npcs/behavior-trees/SKILL.md`.
470
+ → For higher-complexity AI (boss with phases, behavior trees), defer to `ai-and-npcs/behavior-trees/SKILL.md`.
329
471
 
330
472
  ## See also
331
473
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: asset-strategy
3
- description: Decision guide for how to create different game asset types — generated 3D, library search, AI textures, or primitives. Use when planning asset creation, picking an asset pipeline, or writing image-to-3D prompts. Trigger on "assets", "3D models", "textures", "art pipeline".
3
+ description: Use when planning asset creation, picking an asset pipeline, or writing image-to-3D prompts — decision guide for whether to generate 3D, search the library, generate AI textures, or use primitives. Includes prompt templates for the AI image-to-3D pipeline. Trigger on "assets", "3D models", "textures", "art pipeline", "make a model", "generate asset".
4
4
  license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: asset-pipeline
@@ -146,6 +146,20 @@ skills:
146
146
  recommended: true
147
147
  user-invocable: true
148
148
  last-tested: 2026-05-05
149
+ - name: new-project
150
+ category: scene-and-project
151
+ status: HAVE
152
+ public: true
153
+ recommended: true
154
+ user-invocable: true
155
+ last-tested: 2026-05-09
156
+ - name: browse-templates
157
+ category: scene-and-project
158
+ status: HAVE
159
+ public: true
160
+ recommended: true
161
+ user-invocable: true
162
+ last-tested: 2026-05-09
149
163
  - name: scene-composition
150
164
  category: scene-and-project
151
165
  status: HAVE
@@ -163,8 +177,9 @@ skills:
163
177
  category: scene-and-project
164
178
  status: HAVE
165
179
  public: true
166
- recommended: false
167
- last-tested: 2026-05-05
180
+ recommended: true
181
+ user-invocable: true
182
+ last-tested: 2026-05-09
168
183
  - name: godot-project-conventions
169
184
  category: scene-and-project
170
185
  status: NEXT
@@ -437,16 +452,18 @@ skills:
437
452
  last-tested: 2026-05-05
438
453
  - name: peer-to-peer-multiplayer
439
454
  category: multiplayer-and-networking
440
- status: NEXT
455
+ status: HAVE
441
456
  public: true
442
457
  recommended: true
443
458
  user-invocable: true
459
+ last-tested: 2026-05-05
444
460
  - name: host-authoritative-state
445
461
  category: multiplayer-and-networking
446
- status: NEXT
462
+ status: HAVE
447
463
  public: true
448
464
  recommended: true
449
465
  user-invocable: true
466
+ last-tested: 2026-05-05
450
467
  - name: multiplayerapi-basics
451
468
  category: multiplayer-and-networking
452
469
  status: NEXT