summer-engine 1.3.2 → 1.5.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 (32) hide show
  1. package/.claude-plugin/plugin.json +59 -0
  2. package/.codex-plugin/plugin.json +38 -0
  3. package/.cursor-plugin/plugin.json +42 -0
  4. package/.mcp.json +8 -0
  5. package/.opencode/plugins/summer.js +49 -0
  6. package/README.md +13 -0
  7. package/dist/lib/skills-registry.d.ts +96 -0
  8. package/dist/lib/skills-registry.js +205 -0
  9. package/gemini-extension.json +21 -0
  10. package/hooks/hooks-cursor.json +21 -0
  11. package/hooks/hooks.json +31 -0
  12. package/hooks/pre-commit-doctor.sh +123 -0
  13. package/hooks/run-hook.cmd +23 -0
  14. package/hooks/session-start.sh +120 -0
  15. package/marketplace.json +24 -0
  16. package/package.json +9 -1
  17. package/skills/_tests/specs/design-npc.md +43 -12
  18. package/skills/_tests/specs/fps-controller.md +33 -20
  19. package/skills/_tests/specs/host-authoritative-state.md +104 -0
  20. package/skills/_tests/specs/peer-to-peer-multiplayer.md +99 -0
  21. package/skills/_tests/specs/vfx.md +56 -38
  22. package/skills/ai-and-npcs/design-npc/SKILL.md +264 -122
  23. package/skills/asset-pipeline/asset-strategy/SKILL.md +8 -0
  24. package/skills/catalog.yaml +84 -0
  25. package/skills/character-controllers/fps-controller/SKILL.md +277 -69
  26. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +332 -0
  27. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +378 -0
  28. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +20 -0
  29. package/skills/scene-and-project/make-game/SKILL.md +10 -0
  30. package/skills/scene-and-project/scene-composition/SKILL.md +8 -0
  31. package/skills/ui-and-ux/ui-basics/SKILL.md +28 -0
  32. package/skills/visual-effects/vfx/SKILL.md +233 -134
@@ -48,6 +48,13 @@ skills:
48
48
  public: true
49
49
 
50
50
  # ── gameplay-mechanics ──
51
+ - name: design-mechanic
52
+ category: gameplay-mechanics
53
+ status: HAVE
54
+ public: true
55
+ recommended: true
56
+ user-invocable: true
57
+ last-tested: 2026-05-05
51
58
  - name: melee-combat
52
59
  category: gameplay-mechanics
53
60
  status: NEXT
@@ -132,6 +139,13 @@ skills:
132
139
  public: true
133
140
 
134
141
  # ── scene-and-project ──
142
+ - name: brainstorm-game
143
+ category: scene-and-project
144
+ status: HAVE
145
+ public: true
146
+ recommended: true
147
+ user-invocable: true
148
+ last-tested: 2026-05-05
135
149
  - name: scene-composition
136
150
  category: scene-and-project
137
151
  status: HAVE
@@ -177,6 +191,13 @@ skills:
177
191
  public: true
178
192
 
179
193
  # ── rendering-and-lighting ──
194
+ - name: art-direction
195
+ category: rendering-and-lighting
196
+ status: HAVE
197
+ public: true
198
+ recommended: true
199
+ user-invocable: true
200
+ last-tested: 2026-05-05
180
201
  - name: 3d-lighting
181
202
  category: rendering-and-lighting
182
203
  status: HAVE
@@ -255,6 +276,13 @@ skills:
255
276
  public: true
256
277
 
257
278
  # ── visual-effects ──
279
+ - name: vfx
280
+ category: visual-effects
281
+ status: HAVE
282
+ public: true
283
+ recommended: true
284
+ user-invocable: true
285
+ last-tested: 2026-05-05
258
286
  - name: gpuparticles-3d-basics
259
287
  category: visual-effects
260
288
  status: NEXT
@@ -333,6 +361,13 @@ skills:
333
361
  public: true
334
362
 
335
363
  # ── audio ──
364
+ - name: audio-direction
365
+ category: audio
366
+ status: HAVE
367
+ public: true
368
+ recommended: true
369
+ user-invocable: true
370
+ last-tested: 2026-05-05
336
371
  - name: audio-bus-setup
337
372
  category: audio
338
373
  status: NEXT
@@ -393,6 +428,27 @@ skills:
393
428
  public: true
394
429
 
395
430
  # ── multiplayer-and-networking ──
431
+ - name: setup-multiplayer
432
+ category: multiplayer-and-networking
433
+ status: HAVE
434
+ public: true
435
+ recommended: true
436
+ user-invocable: true
437
+ last-tested: 2026-05-05
438
+ - name: peer-to-peer-multiplayer
439
+ category: multiplayer-and-networking
440
+ status: HAVE
441
+ public: true
442
+ recommended: true
443
+ user-invocable: true
444
+ last-tested: 2026-05-05
445
+ - name: host-authoritative-state
446
+ category: multiplayer-and-networking
447
+ status: HAVE
448
+ public: true
449
+ recommended: true
450
+ user-invocable: true
451
+ last-tested: 2026-05-05
396
452
  - name: multiplayerapi-basics
397
453
  category: multiplayer-and-networking
398
454
  status: NEXT
@@ -423,6 +479,13 @@ skills:
423
479
  public: true
424
480
 
425
481
  # ── ai-and-npcs ──
482
+ - name: design-npc
483
+ category: ai-and-npcs
484
+ status: HAVE
485
+ public: true
486
+ recommended: true
487
+ user-invocable: true
488
+ last-tested: 2026-05-05
426
489
  - name: navmesh-pathfinding
427
490
  category: ai-and-npcs
428
491
  status: NEXT
@@ -453,6 +516,13 @@ skills:
453
516
  public: true
454
517
 
455
518
  # ── level-design ──
519
+ - name: design-level
520
+ category: level-design
521
+ status: HAVE
522
+ public: true
523
+ recommended: true
524
+ user-invocable: true
525
+ last-tested: 2026-05-05
456
526
  - name: gridmap-building
457
527
  category: level-design
458
528
  status: NEXT
@@ -479,6 +549,13 @@ skills:
479
549
  public: true
480
550
 
481
551
  # ── performance ──
552
+ - name: tune-performance
553
+ category: performance
554
+ status: HAVE
555
+ public: true
556
+ recommended: true
557
+ user-invocable: true
558
+ last-tested: 2026-05-05
482
559
  - name: profiling-godot
483
560
  category: performance
484
561
  status: NEXT
@@ -624,6 +701,13 @@ skills:
624
701
  public: true
625
702
 
626
703
  # ── deployment ──
704
+ - name: export-and-ship
705
+ category: deployment
706
+ status: HAVE
707
+ public: true
708
+ recommended: true
709
+ user-invocable: true
710
+ last-tested: 2026-05-05
627
711
  - name: export-presets
628
712
  category: deployment
629
713
  status: NEXT
@@ -1,133 +1,341 @@
1
1
  ---
2
2
  name: fps-controller
3
- description: First-person character controller with WASD movement, mouse look, jumping, and physics. Use when building an FPS, first-person movement, or a 3D character that walks/jumps. Trigger on "FPS", "first-person", "WASD", "character controller", "mouse look".
3
+ description: First-person character controller with WASD, mouse look, jump, coyote time, jump buffering, air control, and external-velocity handling. Use when building an FPS, first-person movement, or a 3D character that walks, jumps, and gets shoved around. Trigger on "FPS", "first-person", "WASD", "character controller", "mouse look".
4
4
  license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: character-controllers
7
7
  user-invocable: false
8
8
  template-id: template-3d-fps
9
- allowed-tools: Read Grep summer_add_node summer_set_prop summer_set_resource_property summer_input_map_bind summer_save_scene summer_get_script_errors
9
+ allowed-tools: Read Grep summer_get_scene_tree summer_add_node summer_set_prop summer_set_resource_property summer_input_map_bind summer_save_scene summer_get_script_errors
10
10
  paths: ["**/*.gd", "**/*.tscn"]
11
11
  ---
12
12
 
13
13
  # FPS Controller for Summer Engine
14
14
 
15
- Build a first-person character controller using CharacterBody3D, Camera3D, and proper collision. Follow this structure for reliable movement and camera behavior.
15
+ The canonical first-person controller. Coyote time, jump buffering, air control, external-velocity, the works. This is the production minimum for an FPS that doesn't feel like a Unity tutorial — it follows the shipping FPS standard for movement (gravity, grace timers, separate ground/air acceleration, external force accumulator) so the player never says "this feels stiff" or "I jumped right at the edge and died unfairly".
16
16
 
17
17
  ## Scene Structure
18
18
 
19
19
  ```
20
20
  Player (CharacterBody3D)
21
- ├── CollisionShape3D # Capsule for body
22
- ├── Camera3D # At head height
23
- └── RayCast3D # Optional: ground detection
21
+ ├── CollisionShape3D # CapsuleShape3D, body
22
+ ├── Head (Node3D) # yaw is on Player, pitch is on Head — keeps camera roll-free
23
+ │ └── Camera3D # at eye level
24
+ └── (optional) RayCast3D # ground / interaction probe
24
25
  ```
25
26
 
27
+ The Head/Camera split is the industry-standard movement model: yaw rotates the body, pitch rotates only the head. Don't pitch the body or you get tilted capsules and slope-collision bugs.
28
+
26
29
  ## Step-by-Step Setup via MCP
27
30
 
28
- ### 1. Add the Player Root
31
+ ### 1. Verify the scene first
32
+
33
+ Always call `summer_get_scene_tree` before mutating. If `World/Player`, `World/PlayerOld`, or any existing CharacterBody3D is present, ASK the user how to proceed before adding nodes.
34
+
35
+ ### 2. Add the Player root
29
36
 
30
37
  ```
31
38
  summer_add_node(parent="./World", type="CharacterBody3D", name="Player")
32
39
  summer_set_prop(path="./World/Player", key="position", value="Vector3(0, 1, 0)")
33
40
  ```
34
41
 
35
- ### 2. Add Collision Shape
42
+ ### 3. Add the collision shape (standalone resource, NOT inline)
36
43
 
37
44
  ```
38
- summer_add_node(parent="./World/Player", type="CollisionShape3D", name="CollisionShape3D")
39
- summer_set_prop(path="./World/Player/CollisionShape3D", key="shape", value="CapsuleShape3D")
45
+ summer_add_node(parent="./World/Player", type="CollisionShape3D", name="Collision")
46
+ summer_set_prop(path="./World/Player/Collision", key="shape", value="CapsuleShape3D")
47
+ summer_set_resource_property(nodePath="./World/Player/Collision", resourceProperty="shape", subProperty="radius", value="0.4")
48
+ summer_set_resource_property(nodePath="./World/Player/Collision", resourceProperty="shape", subProperty="height", value="1.8")
40
49
  ```
41
50
 
42
- Configure capsule size (radius ~0.4, height ~1.6 for typical human):
51
+ `summer_set_prop` with a class-name string creates a standalone sub-resource. NEVER call `summer_set_resource_property` against an inline `sub_resource` — the value is silently dropped. See `_shared/mcp-tools-reference.md`.
52
+
53
+ ### 4. Add the Head pivot + Camera
43
54
 
44
55
  ```
45
- summer_set_resource_property(nodePath="./World/Player/CollisionShape3D", resourceProperty="shape", subProperty="radius", value="0.4")
46
- summer_set_resource_property(nodePath="./World/Player/CollisionShape3D", resourceProperty="shape", subProperty="height", value="1.6")
56
+ summer_add_node(parent="./World/Player", type="Node3D", name="Head")
57
+ summer_set_prop(path="./World/Player/Head", key="position", value="Vector3(0, 1.6, 0)")
58
+ summer_add_node(parent="./World/Player/Head", type="Camera3D", name="Camera")
47
59
  ```
48
60
 
49
- ### 3. Add Camera
61
+ Eye height of 1.6 sits the camera right at the top of a 1.8-tall capsule (capsule center 0.9, plus 0.7 to the eye). Tweak per art style.
62
+
63
+ ### 5. InputMap actions
50
64
 
51
65
  ```
52
- summer_add_node(parent="./World/Player", type="Camera3D", name="Camera3D")
53
- summer_set_prop(path="./World/Player/Camera3D", key="position", value="Vector3(0, 1.6, 0)")
66
+ summer_input_map_bind(name="move_forward", events=[{type:"key", key:"W"}])
67
+ summer_input_map_bind(name="move_back", events=[{type:"key", key:"S"}])
68
+ summer_input_map_bind(name="move_left", events=[{type:"key", key:"A"}])
69
+ summer_input_map_bind(name="move_right", events=[{type:"key", key:"D"}])
70
+ summer_input_map_bind(name="jump", events=[{type:"key", key:"Space"}])
71
+ summer_input_map_bind(name="sprint", events=[{type:"key", key:"Shift"}])
54
72
  ```
55
73
 
56
- Camera at y=1.6 puts the view at roughly eye level (capsule center is at 0.8, so 0.8 + 0.8 = 1.6 for eyes).
74
+ ### 6. Attach the controller script
57
75
 
58
- ### 4. InputMap Actions
76
+ Ask before writing: "May I create `scripts/player_controller.gd` and attach it to `./World/Player`?" Then `Write` the file with the skeleton in the next section, attach via inspector, and finish with `summer_save_scene` + `summer_get_script_errors`.
59
77
 
60
- Create movement and look actions:
78
+ ## Physics-Process Flow (the order matters)
61
79
 
62
- ```
63
- summer_input_map_bind(name="move_forward", events=[{type:"key", key:"W"}])
64
- summer_input_map_bind(name="move_back", events=[{type:"key", key:"S"}])
65
- summer_input_map_bind(name="move_left", events=[{type:"key", key:"A"}])
66
- summer_input_map_bind(name="move_right", events=[{type:"key", key:"D"}])
67
- summer_input_map_bind(name="jump", events=[{type:"key", key:"Space"}])
68
- ```
80
+ Each frame, `_physics_process` runs these steps in this exact order. Reordering breaks specific edge cases — they are flagged inline.
69
81
 
70
- For mouse look, you need a script. The script will use `Input.get_vector` for movement and `InputEventMouseMotion` for look. Attach a script and implement:
82
+ 1. **Subtract last frame's external velocity contribution.** If you skip this, knockback and explosions accumulate forever.
83
+ 2. **Apply gravity** (asymmetric — heavier when falling).
84
+ 3. **Update coyote timer.** Reset to full when grounded, tick down when airborne.
85
+ 4. **Update jump buffer + fire jump** if both timers are active. Clearing the buffer on press-instead-of-fire is the bug everyone makes.
86
+ 5. **Read input, build wish-direction in worldspace** by rotating the local 2D input through `transform.basis`.
87
+ 6. **Pick acceleration / friction** based on grounded state, then `move_toward` the horizontal velocity toward `wish_dir * target_speed`.
88
+ 7. **Apply external velocity, save it for next-frame subtraction, decay it** via `move_toward(0, damping * delta)`.
89
+ 8. **`move_and_slide()`** — last, after everything else has settled `velocity`.
71
90
 
72
- ### 5. GDScript Pattern
91
+ The skeleton below implements this exactly.
92
+
93
+ ## GDScript Skeleton (drop into `scripts/player_controller.gd`)
73
94
 
74
95
  ```gdscript
96
+ class_name PlayerController
75
97
  extends CharacterBody3D
76
98
 
77
- @export var move_speed: float = 5.0
78
- @export var jump_velocity: float = 4.5
79
- @export var mouse_sensitivity: float = 0.002
80
-
81
- func _physics_process(_delta: float) -> void:
82
- # Movement
83
- var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
84
- var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
85
- if direction:
86
- velocity.x = direction.x * move_speed
87
- velocity.z = direction.z * move_speed
99
+ # Movement tunables — the canonical defaults. Tweak in inspector, don't fork the script.
100
+ @export var walk_speed: float = 5.0
101
+ @export var sprint_speed: float = 8.0
102
+ @export var jump_velocity: float = 5.5 # ~1.5m peak with default gravity
103
+ @export var gravity: float = 20.0 # heavier than Earth — feels better
104
+ @export var fall_gravity_multiplier: float = 1.4 # snappier descent than ascent
105
+
106
+ # Acceleration model — separate ground/air so air control feels real, not on/off.
107
+ @export var ground_accel: float = 80.0
108
+ @export var ground_friction: float = 90.0
109
+ @export var air_accel: float = 25.0 # ~30% of ground — Quake-ish, not Mario
110
+ @export var air_friction: float = 5.0
111
+
112
+ # Grace timers — the difference between "feels tight" and "feels broken".
113
+ @export var coyote_time: float = 0.1 # jump-after-walking-off-ledge window
114
+ @export var jump_buffer_time: float = 0.1 # press-just-before-landing window
115
+
116
+ # Camera.
117
+ @export_range(0.0001, 0.01) var mouse_sensitivity: float = 0.002
118
+ @export var pitch_min: float = -1.4 # ~-80°
119
+ @export var pitch_max: float = 1.4 # ~+80°
120
+
121
+ # External-velocity damping — knockback / explosions / conveyor belts decay over time.
122
+ @export var external_damping: float = 8.0
123
+
124
+ @onready var head: Node3D = $Head
125
+ @onready var camera: Camera3D = $Head/Camera
126
+
127
+ # Internal state.
128
+ var _coyote_timer: float = 0.0
129
+ var _jump_buffer_timer: float = 0.0
130
+ var _was_on_floor: bool = false
131
+
132
+ # External velocity accumulator. Any system can push the player by adding to this;
133
+ # we apply it, then decay it. Keeps designer-driven forces from fighting input.
134
+ var external_velocity: Vector3 = Vector3.ZERO
135
+ var _prev_external_applied: Vector3 = Vector3.ZERO
136
+
137
+ func _ready() -> void:
138
+ Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
139
+
140
+ func _unhandled_input(event: InputEvent) -> void:
141
+ if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
142
+ rotate_y(-event.relative.x * mouse_sensitivity)
143
+ head.rotate_x(-event.relative.y * mouse_sensitivity)
144
+ head.rotation.x = clamp(head.rotation.x, pitch_min, pitch_max)
145
+ elif event.is_action_pressed("ui_cancel"):
146
+ Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
147
+
148
+ func _physics_process(delta: float) -> void:
149
+ # 1. Strip last frame's external contribution so it doesn't compound with input.
150
+ velocity -= _prev_external_applied
151
+
152
+ # 2. Gravity (asymmetric — falling faster than rising feels better).
153
+ if not is_on_floor():
154
+ var g := gravity * (fall_gravity_multiplier if velocity.y < 0.0 else 1.0)
155
+ velocity.y -= g * delta
156
+
157
+ # 3. Coyote-time bookkeeping. Reset on landing; tick down when airborne.
158
+ if is_on_floor():
159
+ _coyote_timer = coyote_time
160
+ else:
161
+ _coyote_timer = max(_coyote_timer - delta, 0.0)
162
+
163
+ # 4. Jump buffer. Holding/tapping jump up to `jump_buffer_time` before landing
164
+ # queues the jump — fires the instant we touch ground.
165
+ if Input.is_action_just_pressed("jump"):
166
+ _jump_buffer_timer = jump_buffer_time
88
167
  else:
89
- velocity.x = move_toward(velocity.x, 0, move_speed)
90
- velocity.z = move_toward(velocity.z, 0, move_speed)
168
+ _jump_buffer_timer = max(_jump_buffer_timer - delta, 0.0)
91
169
 
92
- # Jump
93
- if is_on_floor() and Input.is_action_just_pressed("jump"):
170
+ if _jump_buffer_timer > 0.0 and _coyote_timer > 0.0:
94
171
  velocity.y = jump_velocity
172
+ _jump_buffer_timer = 0.0
173
+ _coyote_timer = 0.0
174
+
175
+ # 5. Build wish-direction in worldspace from camera yaw.
176
+ var input_v := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
177
+ var wish_dir := (transform.basis * Vector3(input_v.x, 0.0, input_v.y))
178
+ wish_dir.y = 0.0
179
+ wish_dir = wish_dir.normalized() if wish_dir.length() > 0.001 else Vector3.ZERO
180
+
181
+ # 6. Pick speed + accel/friction based on grounded state.
182
+ var target_speed := sprint_speed if Input.is_action_pressed("sprint") else walk_speed
183
+ var accel := ground_accel if is_on_floor() else air_accel
184
+ var friction := ground_friction if is_on_floor() else air_friction
185
+
186
+ var horiz := Vector3(velocity.x, 0.0, velocity.z)
187
+ if wish_dir.length() > 0.0:
188
+ # Accelerate toward wish_dir at `accel` units/sec — not instant snap.
189
+ horiz = horiz.move_toward(wish_dir * target_speed, accel * delta)
190
+ else:
191
+ # No input — decelerate via friction.
192
+ horiz = horiz.move_toward(Vector3.ZERO, friction * delta)
193
+ velocity.x = horiz.x
194
+ velocity.z = horiz.z
95
195
 
96
- # Gravity
97
- if not is_on_floor():
98
- velocity.y -= 9.8 * _delta
196
+ # 7. Apply external velocity, remember it for next-frame subtraction, decay it.
197
+ velocity += external_velocity
198
+ _prev_external_applied = external_velocity
199
+ external_velocity = external_velocity.move_toward(Vector3.ZERO, external_damping * delta)
99
200
 
201
+ _was_on_floor = is_on_floor()
100
202
  move_and_slide()
203
+
204
+ # Public API for other systems — knockback, explosions, jump pads, conveyor belts, etc.
205
+ func add_external_velocity(impulse: Vector3) -> void:
206
+ external_velocity += impulse
101
207
  ```
102
208
 
103
- For mouse look, add to a script attached to the Player or Camera:
209
+ ## Property Reference
104
210
 
105
- ```gdscript
106
- func _input(event: InputEvent) -> void:
107
- if event is InputEventMouseMotion:
108
- rotate_y(-event.relative.x * mouse_sensitivity)
109
- $Camera3D.rotate_x(-event.relative.y * mouse_sensitivity)
110
- $Camera3D.rotation.x = clamp($Camera3D.rotation.x, -1.2, 1.2)
111
- ```
211
+ | Property | Default | Why |
212
+ |---|---|---|
213
+ | `walk_speed` | `5.0` | Standard FPS pace; feels right at 80–90° FOV. |
214
+ | `sprint_speed` | `8.0` | ~60% faster — noticeable without trivializing distances. |
215
+ | `jump_velocity` | `5.5` | ~1.5m apex — clears typical step + crate heights. |
216
+ | `gravity` | `20.0` | Heavier than 9.8 — game-feel default, not realism. |
217
+ | `fall_gravity_multiplier` | `1.4` | Snappier descent than ascent (variable-jump-feel staple). |
218
+ | `ground_accel` / `ground_friction` | `80 / 90` | Crisp start, near-instant stop. |
219
+ | `air_accel` / `air_friction` | `25 / 5` | Reduced air control + minimal air drag. |
220
+ | `coyote_time` | `0.1` | Standard 0.08–0.15s grace — invisible to players, fixes 60% of "unfair death" complaints. |
221
+ | `jump_buffer_time` | `0.1` | Same window the other direction. |
222
+ | `mouse_sensitivity` | `0.002` | Default raw multiplier; expose via settings. |
223
+ | `external_damping` | `8.0` | Knockback decays over ~0.5s. |
224
+
225
+ ## Why Each Feature Matters
226
+
227
+ **Coyote time (~0.1s)** — Players judge ledges by what they see, not by frame-perfect collision. A small grace period after walking off a ledge where jump still works fixes the "I jumped right at the edge and died unfairly" complaint that breaks 60% of new players' tutorials. Cost: 3 lines. Industry-standard window is 0.08–0.15s — short enough to be invisible, long enough to mask the imprecision of human reaction time.
228
+
229
+ **Jump buffering (~0.1s)** — Pressing jump 50ms before landing should not be punished. Buffer the input for ~0.1s and fire on touchdown. Without this, fast-moving players feel like the controller "ate their input" — they pressed jump, they saw nothing happen, they blame the game. With it, every "near-miss" jump becomes a successful one. Cost: 4 lines.
230
+
231
+ **Air control with reduced acceleration (~30% of ground)** — Zero air control feels like ice; the player commits to a direction at jump-takeoff and can't course-correct. Full air control feels like a Unity tutorial; momentum is meaningless and a strafe-jump cancels itself. The shipping FPS standard sits around 25–30% of ground accel: enough to course-correct mid-jump, not enough to cancel realistic momentum. The `move_toward` formulation lets designers tune this with one number — `air_accel` — without forking code.
232
+
233
+ **External velocity accumulator** — Knockback, explosions, jump pads, conveyor belts, wind volumes, and grapple-yanks must not fight player input. Storing them in a separate `external_velocity`, applying once per frame, and decaying via `move_toward(0, damping * delta)` means an explosion shoves the player and then control returns smoothly — no "stuck at terminal velocity" or "input cancels the punch" bugs. The subtract-previous-contribution trick (line 1 of `_physics_process`) is the production fix that prevents accumulation when the player is also sprinting; it's the kind of detail you only learn by shipping multiplayer. Public API (`add_external_velocity`) means any system — AOE attack, jump pad scene, grapple ability — can shove the player with one call, no coupling.
112
234
 
113
- ## Property Values (Godot Format)
235
+ **Acceleration / friction via `move_toward`** — Setting `velocity.x = direction.x * speed` is the AI-tutorial pattern: instant snap to target, instant snap to zero. Real controllers ramp up and ramp down. `move_toward(target, rate * delta)` gives four separate knobs (`ground_accel`, `ground_friction`, `air_accel`, `air_friction`) so designers can dial in "snappy military shooter" (high both) vs "floaty arena shooter" (high accel, low friction) without rewriting code. This is the single biggest reason a controller built from this skeleton feels like a real shipped FPS instead of a prototype.
114
236
 
115
- When using `summer_set_prop`, use these string formats:
237
+ ## Tuning By Feel — Recipe Cards
116
238
 
117
- | Property | Value |
118
- |----------|-------|
119
- | position | `"Vector3(0, 1, 0)"` |
120
- | rotation_degrees | `"Vector3(0, 0, 0)"` |
121
- | shape | `"CapsuleShape3D"` |
239
+ Once the skeleton is in, designers tune by feel. Common targets:
122
240
 
123
- For `summer_set_resource_property` on CapsuleShape3D:
124
- | subProperty | value |
125
- |-------------|-------|
126
- | radius | `"0.4"` |
127
- | height | `"1.6"` |
241
+ **Snappy military shooter (industry-standard FPS feel):**
242
+ - `walk_speed = 5.5`, `sprint_speed = 8.0`
243
+ - `ground_accel = 90`, `ground_friction = 100`
244
+ - `air_accel = 20`, `air_friction = 4`
245
+ - `jump_velocity = 5.0`, `gravity = 22`, `fall_gravity_multiplier = 1.5`
246
+ - `coyote_time = 0.08`, `jump_buffer_time = 0.08`
247
+
248
+ **Floaty arena shooter (longer airtime, more air control):**
249
+ - `walk_speed = 6.5`, `sprint_speed = 10.0`
250
+ - `ground_accel = 60`, `ground_friction = 60`
251
+ - `air_accel = 35`, `air_friction = 2` (low air friction = momentum preserved)
252
+ - `jump_velocity = 7.0`, `gravity = 16`, `fall_gravity_multiplier = 1.2`
253
+
254
+ **Heavy / tactical (slower, more committed):**
255
+ - `walk_speed = 4.0`, `sprint_speed = 6.0`
256
+ - `ground_accel = 50`, `ground_friction = 60`
257
+ - `air_accel = 12`, `air_friction = 6`
258
+ - `jump_velocity = 4.5`, `gravity = 24`
259
+
260
+ The skeleton doesn't change — only the `@export` defaults. That's the point of typed exports.
128
261
 
129
262
  ## Common Issues
130
263
 
131
- - **Falling through floor:** Ensure the floor has a StaticBody3D or similar with a CollisionShape3D
132
- - **Camera clipping:** If the camera goes inside geometry, enable `near` on the Camera3D or add collision exception
133
- - **Mouse capture:** For FPS, you typically need `Input.mouse_mode = Input.MOUSE_MODE_CAPTURED` in `_ready()` and a way to release (e.g., Escape)
264
+ - **Controller feels stiff or instant** — verify `ground_accel` and `ground_friction` are not absurdly high (>500). If they are, `move_toward` saturates in one frame and you get the snap-to-target behaviour you were trying to avoid.
265
+ - **Controller feels mushy / slidey** — `ground_friction` is too low (under 30). Crank to 80–120 for crisp stops.
266
+ - **Jump occasionally fails near ledges** — coyote isn't being reset on landing. Confirm the `is_on_floor()` branch in step 3 runs before the buffer-fires branch in step 4.
267
+ - **Pressing jump just before landing does nothing** — `_jump_buffer_timer` is being decremented to zero before the player touches floor. The buffer must NOT be cleared by anything except a successful jump.
268
+ - **Knockback compounds forever** — `_prev_external_applied` isn't being subtracted. Step 1 of `_physics_process` MUST run before gravity, or external impulses pile up frame after frame.
269
+ - **Knockback decays too fast / too slow** — tune `external_damping`. `8.0` decays a 5 m/s impulse to near-zero in ~0.6s. Lower = longer hangtime.
270
+ - **Air control feels like ice OR like a Unity tutorial** — `air_accel` is the only knob. Industry-standard is 20–30% of `ground_accel`. Zero = ice. Equal = arcade-y. The shipping FPS standard sits around 25.
271
+ - **Camera tilts on slopes** — pitch is on the body instead of the Head. Pitch only on `Head`; yaw only on `Player`.
272
+ - **Camera rolls when looking around** — you're calling `rotate_x` on something that's already rotated on Y. Fix: only ever rotate Y on Player and X on Head, never combine.
273
+ - **Mouse look doesn't work** — `Input.mouse_mode` isn't set to `MOUSE_MODE_CAPTURED`, or another node is consuming `_input` before this script. Use `_unhandled_input` to be a good citizen.
274
+ - **Mouse look feels laggy at low FPS** — sensitivity is being applied in `_physics_process`. Mouse-motion handling MUST stay in `_input` or `_unhandled_input` — those fire per-event, not per-physics-tick.
275
+ - **Falling through floor** — floor needs a StaticBody3D + CollisionShape3D on the same physics layer as the player.
276
+ - **Camera clipping geometry** — push `Camera.near` to ~0.05 or shrink the capsule radius slightly.
277
+ - **Player jitters on slopes / stairs** — enable `floor_snap_length = 0.5` and `floor_max_angle = 0.785` (~45°) on the CharacterBody3D. Out of scope for this skeleton; see the character-body-tuning skill when it ships.
278
+
279
+ ## Networking Note
280
+
281
+ For multiplayer, two viable patterns:
282
+
283
+ 1. **Server-authoritative (the open-source-friendly path):** gate physics with `if not multiplayer.is_server(): return` for non-local players, sync `global_position` + `velocity` via `MultiplayerSynchronizer`, send input via RPC. This skeleton is compatible — wrap the `_physics_process` body in a `is_local_player` check and add a separate `apply_remote_player_physics` that interpolates from network snapshots.
284
+ 2. **Custom action protocol (the shipped-game path):** the same skeleton runs on every client; one peer is "host" and broadcasts authoritative state at a fixed interval. Out of scope for this skill — see the multiplayer-and-networking category.
285
+
286
+ The external-velocity accumulator pattern is a multiplayer-correctness gift either way: explosions and knockbacks happen on the host, the impulse is RPC'd to the affected player, and the client's controller absorbs it without fighting local input. That's why the subtract-previous-contribution trick at the top of `_physics_process` exists — it is the single fix that prevents knockback drift in netcode.
287
+
288
+ ## Beyond This Skeleton
289
+
290
+ The skeleton is intentionally ~90 lines. When you outgrow it, add features by *layering*, not by stuffing more branches into `_physics_process`:
291
+
292
+ - **Headbob.** Drive `Head.position.y` from a sine of `accumulated_walk_distance`, not of time — bob disappears when you stop without a fade-out timer. Amplitude scales with current horizontal speed / `walk_speed`. Disable while airborne.
293
+ - **Crouch.** Lerp the capsule height (e.g., 1.8 → 1.0) and the Head Y (1.6 → 0.9) over ~0.15s. Halve `walk_speed` while crouched. Block standing if a short upward shapecast hits ceiling.
294
+ - **Sprint FOV punch.** Tween `Camera.fov` from 75 → 82 over 0.2s when sprint starts, back when it ends. The single cheapest "feel of speed" effect available.
295
+ - **Slide.** When sprint + crouch are pressed together with horizontal speed above a threshold, lock direction to current velocity, set a `slide_timer`, and skip the wish-direction step. Apply slide-specific friction. Restore on timer expiry, ceiling clearance, or speed-below-threshold.
296
+ - **Wallrun, dash, double-jump.** Each is its own override state. Build a state machine (see `scripting-patterns/state-machine-patterns/SKILL.md`) and let each state run its own `_physics_process` branch with an early `move_and_slide()` return. Don't pile them as `if/elif` in a single function.
297
+ - **Camera shake.** Add a `trauma` float on the camera, accumulate via `add_trauma(amount)`, decay each frame, apply a randomized rotation offset proportional to `trauma * trauma`. Subtract the previous frame's shake offset before applying the new one to prevent drift. See the post-processing/screen-shake skill.
298
+
299
+ Each of these is a separate skill or a separate function, not a fork of this script.
300
+
301
+ ## Fallback (no MCP — edit `.tscn` directly)
302
+
303
+ Save as `res://scenes/player.tscn`:
304
+
305
+ ```
306
+ [gd_scene load_steps=3 format=3]
307
+
308
+ [ext_resource type="Script" path="res://scripts/player_controller.gd" id="1"]
309
+
310
+ [sub_resource type="CapsuleShape3D" id="caps"]
311
+ radius = 0.4
312
+ height = 1.8
313
+
314
+ [node name="Player" type="CharacterBody3D"]
315
+ position = Vector3(0, 1, 0)
316
+ script = ExtResource("1")
317
+
318
+ [node name="Collision" type="CollisionShape3D" parent="."]
319
+ shape = SubResource("caps")
320
+
321
+ [node name="Head" type="Node3D" parent="."]
322
+ position = Vector3(0, 1.6, 0)
323
+
324
+ [node name="Camera" type="Camera3D" parent="Head"]
325
+ ```
326
+
327
+ Then patch `project.godot` with the InputMap actions (`input/move_forward = { ... }` etc.) and `Write` the script body from the section above.
328
+
329
+ ## Collaborative Protocol
330
+
331
+ This skill writes scene nodes, an InputMap, and a `.gd` script. Always ask before applying. Group writes per phase: "May I add the Player + Collision + Head + Camera, bind WASD/jump/sprint, and attach `player_controller.gd`?" See `../../_shared/collaborative-protocol.md`.
332
+
333
+ If the user asks for a "third-person FPS", flag the contradiction: FPS = first-person. Either clarify, or hand off to the `tps-controller` skill when it ships.
334
+
335
+ ## See Also
336
+
337
+ - `_shared/gd-style.md` — typed GDScript conventions used in the skeleton.
338
+ - `_shared/mcp-tools-reference.md` — `summer_set_resource_property` rules + the inline-sub-resource silent-fail trap.
339
+ - `_shared/collaborative-protocol.md` — when to ask before writing.
340
+ - `scripting-patterns/state-machine-patterns/SKILL.md` — once movement grows past this skeleton (slide, grapple, hover), refactor into a state machine instead of stuffing more branches into `_physics_process`.
341
+ - `physics/character-body-tuning/SKILL.md` (when shipped) — slope handling, step-up, floor-snap.