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,137 +1,157 @@
1
1
  ---
2
2
  name: debug
3
- description: Use when the user reports their game is broken, crashing, behaving unexpectedly, or says "debug", "fix", "something's wrong", or "it's not working". Triages the bug end-to-end via Summer Engine's diagnostic tools (console, debugger errors, script errors), locates the offending code, proposes a fix, and verifies the fix.
3
+ description: Use when the user reports a bug, crash, error, or unexpected behavior in a Godot/Summer project, before making any code or scene changes — runs a disciplined script-errors → console → debugger → hypothesis → fix → verify loop. Trigger on "debug", "crash", "error", "broken", "not working", "freezes", "wrong".
4
4
  license: MIT
5
- compatibility: [Cursor, Claude Code, Windsurf, Codex]
5
+ compatibility: [Cursor, Claude Code, Codex, Windsurf, Gemini, OpenCode]
6
6
  category: debugging
7
7
  user-invocable: true
8
- allowed-tools: Read Grep Glob Edit summer_get_console summer_get_debugger_errors summer_get_script_errors summer_get_diagnostics summer_clear_console summer_play summer_stop summer_is_running summer_get_scene_tree summer_inspect_node
9
- paths: ["**/*.gd", "**/*.tscn", "**/*.cs"]
8
+ allowed-tools: Read Edit Grep summer_get_diagnostics summer_get_script_errors summer_get_console summer_clear_console summer_get_debugger_errors summer_play summer_stop summer_is_running summer_inspect_node summer_inspect_resource summer_get_scene_tree
9
+ paths: ["**/*.gd", "**/*.cs", "**/*.tscn", "**/*.tres", "project.godot"]
10
10
  ---
11
11
 
12
- # /debug — Triage and Fix a Bug
12
+ # /debug — Triage and Fix a Bug End-to-End
13
13
 
14
- ## Overview
14
+ The disciplined debugging loop for Godot/Summer projects. Read the error before guessing. Form a hypothesis before editing. Verify the fix before declaring victory. Never grep the codebase before reading the actual error.
15
15
 
16
- A bug in a Godot/Summer game can hide in three places: scripts (GDScript/C# compile or runtime errors), the engine console (printed messages, warnings), or the scene/resource state (wrong property, missing collider, broken signal connection). This skill walks all three before guessing.
16
+ **Core principle:** The debugger already knows what's wrong. Your job is to listen to it before doing anything else.
17
17
 
18
- **Core principle:** read what the engine actually saw, not what the code looks like it does.
18
+ ## When to use this skill
19
19
 
20
- ## Steps
20
+ - The user says "it crashes", "it broke", "throws an error", "doesn't work", "freezes", "wrong behavior".
21
+ - The user pastes a stack trace.
22
+ - A previous build or test step failed and the user wants to fix it.
21
23
 
22
- ### 1. Get the user's description first
24
+ ## When NOT to use this skill
23
25
 
24
- Ask one short question:
26
+ - The user is asking how to *prevent* a class of bug — that's a design question. Use the relevant discipline skill (`gdscript-patterns`, `scene-composition`).
27
+ - The user is asking for a feature with a known unfinished spec — that's `make-game` territory.
28
+ - The bug is in a non-Godot file the host agent can debug natively (CI config, npm scripts) — use the host's debugger.
25
29
 
26
- > What's happening? Briefly: when does it go wrong, and what do you expect vs. what you see?
30
+ ## The Loop
27
31
 
28
- Wait for the answer before doing anything else. Don't start grepping code yet — the user's words narrow the search 10x.
32
+ ```
33
+ Listen → Diagnose → Hypothesize → Propose → Fix → Verify
34
+ ```
29
35
 
30
- ### 2. Read the engine's actual error state
36
+ Do not skip steps. Do not loop back to "Hypothesize" without re-running the cheap diagnostic.
31
37
 
32
- Always do this in order. Each tool answers a different question.
38
+ ### 1. Listen
33
39
 
34
- **Preferred (Summer MCP):**
40
+ Ask the user **one** focused question and wait for the answer:
35
41
 
36
- ```
37
- summer_get_script_errors # Compile errors? (cheapest, do first)
38
- summer_get_console # Print() output, warnings, info
39
- summer_get_debugger_errors # Runtime errors with stack traces
40
- summer_get_diagnostics # Aggregated count + summary
41
- ```
42
+ > What's happening, and when does it happen?
42
43
 
43
- If the bug is reproducible only at runtime and the engine isn't currently playing:
44
+ If they already gave a clear symptom in the request, skip the question.
45
+ If they say "something's broken", that one question is your only call until they reply. Don't run tools yet.
44
46
 
45
- ```
46
- summer_clear_console # Clean slate
47
- summer_play # Run it
48
- # (let the user reproduce the bug)
49
- summer_get_debugger_errors # Now read what blew up
50
- summer_stop # When done
51
- ```
47
+ ### 2. Diagnose — cheapest tool first
52
48
 
53
- **Fallback (no MCP — engine isn't connected):**
49
+ In strict order. Stop at the first one that returns useful signal.
54
50
 
55
- Ask the user to copy-paste the engine's Output panel and Debugger errors. Inspect manually.
51
+ | Order | Tool | When |
52
+ |---|---|---|
53
+ | 1 | `summer_get_script_errors` | Always start here. Catches GDScript parse errors, missing identifiers, signature mismatches. Cheapest, no side effects. |
54
+ | 2 | `summer_get_diagnostics` | Aggregate console + debugger error counts. One call, broad picture. |
55
+ | 3 | `summer_get_console` | Read the editor Output panel — print statements, warnings, errors that didn't crash. |
56
+ | 4 | `summer_get_debugger_errors` | Runtime errors caught by the debugger. Use AFTER `summer_play` for runtime-only bugs. |
56
57
 
57
- ### 3. Locate the offending code
58
+ **If `summer_get_script_errors` is clean and the user says it crashes only when running:** it's a runtime bug. Go to step 2b.
58
59
 
59
- For each error, follow the stack trace to the file + line. Use host file tools:
60
+ ### 2b. Runtime-only bugs
60
61
 
61
62
  ```
62
- Read <file>:<line-near-error>
63
+ summer_clear_console
64
+ summer_play
65
+ ─▶ ASK USER: "Reproduce the bug now."
66
+ (wait for confirmation)
67
+ summer_get_debugger_errors
68
+ summer_stop
63
69
  ```
64
70
 
65
- Read 20–40 lines around the error site. Don't open the whole file unless you need to — context budget matters.
71
+ Do not skip the "reproduce now" prompt. Auto-running and grabbing whatever's in the buffer leads you to chase ghosts from previous sessions.
66
72
 
67
- If the error mentions a node path (e.g. `./World/Player/Camera`), inspect it:
73
+ ### 2c. MCP unavailable (engine not running, or no Summer install)
68
74
 
69
- ```
70
- summer_inspect_node "./World/Player/Camera"
71
- ```
75
+ If `summer_get_script_errors` returns "Summer Engine is not running" or the tool isn't available:
72
76
 
73
- ### 4. Form a hypothesis and propose the fix
77
+ 1. Ask the user to copy-paste the Output panel and the Debugger panel from the Godot editor.
78
+ 2. Reason over the pasted text exactly as you would over MCP output.
79
+ 3. Continue with the rest of the loop unchanged.
74
80
 
75
- State it in one sentence:
81
+ Do NOT loop on MCP retry. Do NOT pretend the engine will come back. Use the fallback the moment it fails once.
76
82
 
77
- > The error happens because <root cause>. The fix is <one-line summary>.
83
+ ### 3. Hypothesize — one specific theory
78
84
 
79
- Then ask:
85
+ State exactly one hypothesis, in one sentence, naming the file and line.
80
86
 
81
- > May I edit `<file>` to <do exactly this>?
87
+ > **Good:** "Typo at `scripts/player.gd:14` — `GRAVTY` should be `GRAVITY`."
88
+ > **Bad:** "Could be a typo, missing import, or wrong scope."
82
89
 
83
- Don't guess at multiple fixes at once. Pick one. If wrong, the next iteration is cheap.
90
+ If you have multiple plausible theories, pick the highest-prior-probability one. The user can correct you if you're wrong; chasing all three at once wastes their time.
84
91
 
85
- ### 5. Apply the fix
92
+ **Verify the hypothesis without editing.** If the error mentions a missing node, call `summer_inspect_node` to confirm. If it's a missing resource, `summer_inspect_resource`. If it's a scene-graph issue, `summer_get_scene_tree`. **Confirm the world state matches the error before proposing a fix.**
86
93
 
87
- On user yes, edit the file with host tools (`Edit`/`Write`). Single focused change.
94
+ ### 4. Propose — ask before writing
88
95
 
89
- ### 6. Verify
96
+ Surface the proposed fix in plain language and ask permission. Two patterns:
90
97
 
91
- Re-run the relevant check:
98
+ **Code fix:**
99
+ > May I edit `scripts/player.gd` to rename `GRAVTY` → `GRAVITY` on line 14?
92
100
 
93
- ```
94
- summer_get_script_errors # If it was a compile error
95
- summer_play → summer_get_debugger_errors → summer_stop # If runtime
96
- ```
101
+ **Scene fix vs code fix:**
102
+ > The script calls `audio.play()` but the `AudioStreamPlayer` child is gone. Two options:
103
+ > 1. Re-add the `AudioStreamPlayer` to `./Coin` (preserves the original behavior).
104
+ > 2. Null-check in `coin.gd` (defensive, but the audio is silent).
105
+ >
106
+ > Which one do you want?
97
107
 
98
- Report:
108
+ **Never** unilaterally pick when there are two equally valid fixes (scene vs code, defensive vs strict, fast vs correct). Ask.
99
109
 
100
- > Fixed. <One-line description of what changed.>
110
+ ### 5. Fix — minimal, focused
101
111
 
102
- If still broken: go back to step 4 with the new error state. State explicitly that the first hypothesis was wrong.
112
+ - For GDScript edits: `Read` the 20–40 lines around the error, `Edit` the exact change. Don't read the whole file. Don't reformat. Don't rename other things.
113
+ - For scene edits: use the appropriate `summer_*` tool (`summer_add_node`, `summer_set_prop`, `summer_replace_node`). Group multi-step changes in `summer_batch` for one undo step.
114
+ - **Trap to avoid:** never call `summer_set_resource_property` against an inline `sub_resource` — the value is silently dropped. If the property is on a nested resource, first call `summer_set_prop` with the resource class name to instantiate a standalone resource, then drill in. See `_shared/mcp-tools-reference.md`.
103
115
 
104
- ## Common bug patterns and how to spot them
116
+ ### 6. Verify — re-run the diagnostic that found it
105
117
 
106
- | Symptom | Likely cause | First check |
107
- |---|---|---|
108
- | `Invalid set index 'X' (on base: Nil)` | Node path resolved to null | `summer_inspect_node` the path |
109
- | `Identifier not declared` (script error) | Missing `class_name` or wrong scope | `summer_get_script_errors` |
110
- | Game runs but sprite doesn't appear | Node added but not visible (transform / parent / layer) | `summer_get_scene_tree` + inspect parent |
111
- | Signal handler never fires | Connection not made, or connected to wrong instance | `summer_inspect_node` of emitter, check connections |
112
- | Player falls through floor | CollisionShape3D has no shape, or shape is inline sub_resource | `summer_inspect_resource` on the CollisionShape |
113
- | Mouse capture stuck | `Input.set_mouse_mode` not paired with release on focus loss | `summer_get_console` for warnings |
114
- | Game hangs on start | Infinite loop in `_ready()` or `_process()` | `summer_get_debugger_errors` after a forced stop |
118
+ The fix is not done until the same diagnostic that found the bug returns clean.
119
+
120
+ - Script error → re-run `summer_get_script_errors`.
121
+ - Runtime error → re-run the play/reproduce/check-debugger loop.
122
+ - Console warning → `summer_clear_console`, then `summer_play` and confirm clean output.
115
123
 
116
- ## Anti-patterns (don't do these)
124
+ If the diagnostic is still red after the fix, you formed the wrong hypothesis. Go back to step 3 — do **not** make a second edit on top of the first. Revert if the first edit didn't help, then re-hypothesize.
117
125
 
118
- - **Don't** start by reading the user's whole codebase. The error tells you where to look.
119
- - **Don't** propose a sweeping refactor for a single bug. One file, one change, verify.
120
- - **Don't** call `summer_set_resource_property` on an inline `sub_resource` — it silently fails. See `_shared/mcp-tools-reference.md` § "Trap".
121
- - **Don't** call `summer_play` without `summer_clear_console` first if you want clean output to read.
122
- - **Don't** skip the user's description in step 1. "What were you trying to do" beats "what does the code do" 9/10 times.
126
+ ## Anti-Patterns
123
127
 
124
- ## Collaborative protocol
128
+ | Don't | Why |
129
+ |---|---|
130
+ | Grep the whole project before reading the error | The error already names the file and line. Save the user's tokens. |
131
+ | Read whole files | 20–40 lines around the error is enough 90% of the time. |
132
+ | Run multiple diagnostics in parallel | They mask each other's signal. Cheapest first, escalate. |
133
+ | Edit before asking | The user owns the fix decision, you own the diagnosis. |
134
+ | "Try this, see if it works" | That's not a hypothesis, that's gambling. State the theory or ask another question. |
135
+ | Reformat the file while you're in it | Out-of-scope edits make the diff hostile to review. |
136
+ | Auto-fix linter warnings unrelated to the bug | Same reason. |
137
+ | Declare victory after editing | Re-run the diagnostic. Always. |
125
138
 
126
- This skill writes files. Always ask before each edit step. See `_shared/collaborative-protocol.md`.
139
+ ## Quick reference — common Godot bug families
127
140
 
128
- ## Want a working starter?
141
+ | Symptom | First tool | Common cause |
142
+ |---|---|---|
143
+ | "Identifier X not declared" | `summer_get_script_errors` | Typo, missing import, wrong scope |
144
+ | "Invalid call. Nonexistent function 'X' in base 'Nil'" | `summer_get_debugger_errors` after `summer_play` | Node was deleted in editor, code still references it |
145
+ | "Cannot find type 'X'" | `summer_get_script_errors` | Class name mismatch, missing autoload, missing `class_name` |
146
+ | Game runs but visuals wrong | `summer_get_console` + `summer_inspect_node` | Material/light/camera misconfigured |
147
+ | Game freezes (no crash) | `summer_get_console` after `summer_play` for ~3s | Infinite loop in `_process` or `_ready` |
148
+ | `summer_set_resource_property` "succeeded" but nothing changed | n/a | Inline sub-resource silent-fail. Use `summer_set_prop` with class name first. |
129
149
 
130
- No template — this is a workflow, not a project scaffold. It works against any Godot/Summer project that has Summer MCP wired up.
150
+ ## Closing
131
151
 
132
- ## See also
152
+ A debug session is done when:
153
+ 1. The user-reported symptom no longer reproduces.
154
+ 2. The diagnostic that originally flagged the bug returns clean.
155
+ 3. No new errors or warnings have been introduced.
133
156
 
134
- - `_shared/mcp-tools-reference.md` — full tool list
135
- - `_shared/godot-version.md` — engine version + LLM cutoff risks
136
- - `scripting-patterns/gdscript-patterns/SKILL.md` — what good GDScript looks like
137
- - `scene-and-project/play/SKILL.md` — running the game
157
+ Tell the user one short sentence: "Fixed `<file>:<line>` — `<change>`. Diagnostic clean." Then stop.
@@ -0,0 +1,332 @@
1
+ ---
2
+ name: host-authoritative-state
3
+ description: Use when designing the state layer of a multiplayer Godot game — deciding what's host-owned, how clients request changes, how the host validates and broadcasts. Pairs with `/peer-to-peer-multiplayer`. Trigger on "host authority", "authoritative state", "state ownership", "MP cheating", "client validation", "RPC patterns".
4
+ license: MIT
5
+ compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
+ category: multiplayer-and-networking
7
+ user-invocable: true
8
+ allowed-tools: Read Grep summer_get_scene_tree summer_inspect_node summer_save_scene summer_get_script_errors
9
+ ---
10
+
11
+ # /host-authoritative-state — Designing the State Layer
12
+
13
+ ## Overview
14
+
15
+ This is **Layer 2** of multiplayer architecture. `/peer-to-peer-multiplayer` covers all four layers; this skill zooms in on the one that produces 90% of shipping-quality MP bugs. For every piece of state in your game, decide: **does the host own it, or does the client own it?** Get this wrong and you ship a cheat-vulnerable, desync-prone game where players "teleport," items dupe, and hit-detection lies. Get it right and the rest of multiplayer falls into place.
16
+
17
+ ## The fundamental question
18
+
19
+ For every state field, ask one question:
20
+
21
+ > **If a malicious client lies about this, does it break the game?**
22
+
23
+ | Answer | Owner | Notes |
24
+ |---|---|---|
25
+ | Yes | **Host-authoritative.** | Clients send intent (RPC request); host validates and broadcasts. |
26
+ | No | **Client-owned.** | Cosmetic. Replicated for visual sync only. |
27
+ | Sometimes | **Host-authoritative with client prediction.** | Position is the canonical example: client predicts to feel responsive; host reconciles to prevent teleport-cheats. |
28
+
29
+ This question is the entire decision tree. Apply it to every field, no exceptions.
30
+
31
+ ## The decision matrix
32
+
33
+ The canonical state-ownership table for a typical 3D action game. Use this as your starting point and extend per game.
34
+
35
+ | State field | Owner | Why | What an attacker could do if you got it wrong |
36
+ |---|---|---|---|
37
+ | `health` | host | core combat integrity | edit memory → infinite HP, ignore damage |
38
+ | `mana` / `stamina` | host | gates ability/sprint use | infinite-cast spam, infinite-sprint |
39
+ | `score` / `kills` / `objectives` | host | competitive integrity | self-award kills, fake leaderboard |
40
+ | `inventory` contents | host | items must not dupe | broadcast "I picked it up" twice → duped item |
41
+ | `currency` (gold, gems) | host | trade exploits | mint currency on the client |
42
+ | `ability_cooldowns` | host | gates damage rate | spam-cast every frame |
43
+ | `current_weapon` equipped | host | tied to inventory and damage tables | swap to "best gun" without owning it |
44
+ | `team_assignment` | host | balance + friendly fire | switch sides mid-match |
45
+ | `ready_state` (lobby) | host | match-start gating | force-start with one player |
46
+ | `chat_messages` | host-relayed | moderation hooks, profanity filter | bypass mute, broadcast to muted peers |
47
+ | `position` | client-predicted, host-reconciled | input latency feels awful otherwise | speed-hack, teleport — reconciliation catches both |
48
+ | `velocity` | client-predicted, host-reconciled | same as position | same as position |
49
+ | `look_direction` (yaw/pitch) | **client** | cosmetic; host doesn't need it for hit-reg if you raycast on host | cheating here doesn't help |
50
+ | `animation_state` | **client** | visual; can lead the truth | wrong anim = visual glitch, not exploit |
51
+ | `footstep_audio` | **client** | host doesn't care | spam → mild annoyance, not exploit |
52
+ | `particle_fx` / `muzzle_flash` | **client** | host doesn't care | same |
53
+ | `aim_assist_target` | **client** | local UX | spoofing it only hurts the spoofer |
54
+
55
+ If a row is missing, run the fundamental question on it. When in doubt, host-own it — false positives are cheap, false negatives ship cheats.
56
+
57
+ ## The Manager pattern
58
+
59
+ Every host-authoritative state field lives in a Manager autoload. Naming convention: `{Domain}Manager` — `HealthManager`, `ScoreManager`, `InventoryManager`, `CooldownManager`. This is the canonical state-ownership pattern across shipping-quality MP architectures.
60
+
61
+ Each Manager has a fixed shape:
62
+
63
+ ```gdscript
64
+ class_name HealthManager
65
+ extends Node
66
+
67
+ # ── Signals (UI / floating bars / scoreboard listen here) ──
68
+ signal player_health_updated(peer_id: int, hp: int, max_hp: int)
69
+ signal player_died(peer_id: int, killer_id: int)
70
+
71
+ const DEFAULT_MAX_HEALTH := 100
72
+
73
+ # Host-authoritative dictionary, keyed by peer_id.
74
+ var _player_health: Dictionary = {} # peer_id -> { hp, max_hp, is_dead }
75
+
76
+ func _ready() -> void:
77
+ NetworkManager.peer_joined.connect(_on_peer_joined)
78
+ NetworkManager.peer_left.connect(_on_peer_left)
79
+
80
+ # ── HOST-ONLY MUTATORS (state writes) ──────────────────────────
81
+ func _host_apply_damage(target: int, amount: int, attacker: int) -> void:
82
+ if not NetworkManager.is_host: return
83
+ var entry = _player_health.get(target)
84
+ if entry == null or entry.is_dead: return
85
+ var new_hp = max(0, entry.hp - amount)
86
+ entry.hp = new_hp
87
+ _broadcast_health(target, new_hp, entry.max_hp)
88
+ if new_hp == 0:
89
+ entry.is_dead = true
90
+ _broadcast_death(target, attacker)
91
+
92
+ # ── CLIENT REQUEST HANDLERS (validate then mutate) ─────────────
93
+ @rpc("any_peer", "call_remote", "reliable")
94
+ func _client_request_damage(target: int, amount: int) -> void:
95
+ if not NetworkManager.is_host: return
96
+ var sender := multiplayer.get_remote_sender_id()
97
+ if not _validate_damage_request(sender, target, amount): return
98
+ _host_apply_damage(target, amount, sender)
99
+
100
+ # ── HOST-TO-CLIENT BROADCASTS (state reads) ────────────────────
101
+ @rpc("authority", "call_remote", "reliable")
102
+ func _broadcast_health(peer_id: int, hp: int, max_hp: int) -> void:
103
+ var entry = _player_health.get(peer_id, { "hp": hp, "max_hp": max_hp, "is_dead": false })
104
+ entry.hp = hp
105
+ entry.max_hp = max_hp
106
+ _player_health[peer_id] = entry
107
+ player_health_updated.emit(peer_id, hp, max_hp)
108
+
109
+ @rpc("authority", "call_remote", "reliable")
110
+ func _broadcast_death(peer_id: int, killer_id: int) -> void:
111
+ player_died.emit(peer_id, killer_id)
112
+ ```
113
+
114
+ The shape:
115
+
116
+ 1. **Data** lives in a private `Dictionary` keyed by `peer_id`.
117
+ 2. **`_host_apply_*()` mutators** are the *only* functions that write the dictionary. First line: `if not NetworkManager.is_host: return`.
118
+ 3. **`_client_request_*()` RPC handlers** validate, then call the mutator. Never write directly.
119
+ 4. **`_broadcast_*()` RPC functions** push state to clients (`@rpc("authority", ...)`) so only host can call.
120
+ 5. **Lifecycle** via `NetworkManager.peer_joined` / `peer_left` — host registers and cleans up entries.
121
+
122
+ Build one Manager per state domain. Don't put health and inventory in the same autoload — when one needs a refactor the other gets dragged along.
123
+
124
+ ## The four RPC flavors
125
+
126
+ `/peer-to-peer-multiplayer` covers three; this skill goes deeper on a fourth (query/response). Every networked function maps to one of these. Pick the wrong one and you'll see the bug listed.
127
+
128
+ ### Flavor 1: Host → Client broadcast
129
+
130
+ ```gdscript
131
+ @rpc("authority", "call_remote", "reliable")
132
+ func _broadcast_score(peer: int, value: int) -> void:
133
+ _scores[peer] = value
134
+ score_updated.emit(peer, value)
135
+ ```
136
+
137
+ - `authority` = only host can call this. Anyone else's call is dropped by Godot.
138
+ - `call_remote` = doesn't run on the caller. Host already wrote the dictionary; no need to re-run locally.
139
+ - `reliable` = state must arrive. Lost packets retry.
140
+ - **Bug if wrong:** if you mark this `any_peer`, a client can fake a score broadcast → desync.
141
+
142
+ ### Flavor 2: Client → Host request
143
+
144
+ ```gdscript
145
+ @rpc("any_peer", "call_remote", "reliable")
146
+ func _client_request_use_item(item_id: String) -> void:
147
+ if not NetworkManager.is_host: return
148
+ var sender := multiplayer.get_remote_sender_id()
149
+ if not _validate_item_use(sender, item_id): return
150
+ _host_apply_item_use(sender, item_id)
151
+ ```
152
+
153
+ - `any_peer` = any client can call.
154
+ - First line is **always** `if not NetworkManager.is_host: return`. Defensive — Godot already routes correctly, this guards against misconfiguration.
155
+ - `multiplayer.get_remote_sender_id()` is trustworthy — clients can't lie about who sent the request.
156
+ - **Bug if wrong:** missing the `is_host` guard means peers process each other's requests and corrupt local state.
157
+
158
+ ### Flavor 3: Client → Client cosmetic relay
159
+
160
+ ```gdscript
161
+ @rpc("any_peer", "call_remote", "unreliable")
162
+ func _peer_play_emote(emote_id: String) -> void:
163
+ var sender := multiplayer.get_remote_sender_id()
164
+ _play_emote_visual(sender, emote_id)
165
+ ```
166
+
167
+ - `unreliable` = fire-and-forget, lossy is fine for cosmetics.
168
+ - No validation — worst case a player's emote desyncs by a frame.
169
+ - **Bug if wrong:** marking this `reliable` for high-frequency cosmetic events (footsteps, particle spawns) blows up bandwidth.
170
+
171
+ ### Flavor 4: Query / response (the late-join flavor)
172
+
173
+ The flavor not covered in `/peer-to-peer-multiplayer`. When one specific client needs the host to send them state — e.g. a peer just joined and needs the full game state — use `rpc_id` to target a single peer.
174
+
175
+ ```gdscript
176
+ # Client side — peer asks the host for current state
177
+ func client_request_full_state() -> void:
178
+ rpc_id(1, "_host_send_full_state_to_caller")
179
+
180
+ @rpc("any_peer", "call_remote", "reliable")
181
+ func _host_send_full_state_to_caller() -> void:
182
+ if not NetworkManager.is_host: return
183
+ var caller := multiplayer.get_remote_sender_id()
184
+ # send state ONLY to the caller, not all peers
185
+ for peer_id in _player_health:
186
+ var entry = _player_health[peer_id]
187
+ rpc_id(caller, "_broadcast_health", peer_id, entry.hp, entry.max_hp)
188
+
189
+ # Host side — actually a flavor 1 broadcast, but targeted via rpc_id
190
+ @rpc("authority", "call_remote", "reliable")
191
+ func _broadcast_health(peer_id: int, hp: int, max_hp: int) -> void:
192
+ # same handler as flavor 1, but called via rpc_id(caller, ...) for targeting
193
+ pass
194
+ ```
195
+
196
+ Use this for: late-join state replay, lobby info on connect, post-respawn loadout. **Always reliable** — partial state replay is worse than no replay.
197
+
198
+ ## Validation patterns
199
+
200
+ Every `_client_request_*()` must validate **before** applying. The validation is the only thing standing between you and a cheat.
201
+
202
+ ```gdscript
203
+ func _validate_damage_request(attacker: int, target: int, amount: int) -> bool:
204
+ # 1. Authority check — does this client own the attacker?
205
+ if attacker != target_attacker_owner(attacker): return false
206
+
207
+ # 2. Existence check
208
+ if not _player_health.has(target): return false
209
+ if _player_health[target].is_dead: return false
210
+
211
+ # 3. Resource check — does attacker have a weapon equipped?
212
+ if not InventoryManager.has_weapon_equipped(attacker): return false
213
+
214
+ # 4. Range check — within weapon's max range?
215
+ var weapon := InventoryManager.get_equipped_weapon(attacker)
216
+ var dist := _get_player_distance(attacker, target)
217
+ if dist > weapon.max_range + 0.5: # epsilon for float compare
218
+ return false
219
+
220
+ # 5. Line-of-sight check — physics raycast on the host
221
+ if not _has_line_of_sight(attacker, target): return false
222
+
223
+ # 6. Cooldown check — is the weapon off cooldown?
224
+ if not CooldownManager.is_ready(attacker, weapon.id): return false
225
+
226
+ # 7. Damage cap — clamp `amount` to weapon.max_damage to defeat magnitude inflation
227
+ if amount > weapon.max_damage: return false
228
+
229
+ return true
230
+ ```
231
+
232
+ Six common validators, in priority order:
233
+
234
+ 1. **Authority** — does this client own the entity they're modifying? (You can attack other players, but you can only modify your *own* inventory.)
235
+ 2. **Existence** — does the target still exist on the host? (Race vs. peer disconnect.)
236
+ 3. **Resource** — does the requester have the resources? (Mana, ammo, weapon.)
237
+ 4. **Range / distance** — physics-plausible? (Prevents shooting across the map.)
238
+ 5. **Line-of-sight** — raycast on the host. (Prevents shooting through walls.)
239
+ 6. **Cooldown** — within rate limits? (Prevents spam-cast.)
240
+ 7. **Magnitude** — is the requested amount within game-defined max? (Prevents inflation: clamp damage to weapon's `max_damage`, etc.)
241
+
242
+ When validation fails: **drop silently or log + drop**. Never tell the client they tried to cheat — that just lets them probe. No error response, no friendly "you can't do that" message. The request simply has no effect.
243
+
244
+ **Float epsilon:** range / distance / cooldown checks involve floats. Always allow a small epsilon (`+ 0.5` for distance, `> -0.05` for cooldown remaining). Strict equality on floats fails for legitimate clients due to physics jitter.
245
+
246
+ ## Late-join state sync
247
+
248
+ When a peer joins mid-session, the host must replay current state to them. Without this, the new peer's HUD shows zeros and they think they have full HP.
249
+
250
+ The pattern: every Manager exposes a `_send_full_state(peer_id)` method. Connect it to `NetworkManager.peer_joined`.
251
+
252
+ ```gdscript
253
+ # In each Manager
254
+ func _on_peer_joined(id: int) -> void:
255
+ if not NetworkManager.is_host: return
256
+ # Register the new peer
257
+ _player_health[id] = { "hp": DEFAULT_MAX_HEALTH, "max_hp": DEFAULT_MAX_HEALTH, "is_dead": false }
258
+ # Replay all existing state to them
259
+ _send_full_state(id)
260
+
261
+ func _send_full_state(target_peer: int) -> void:
262
+ for peer_id in _player_health:
263
+ var entry = _player_health[peer_id]
264
+ rpc_id(target_peer, "_broadcast_health", peer_id, entry.hp, entry.max_hp)
265
+ if entry.is_dead:
266
+ rpc_id(target_peer, "_broadcast_death", peer_id, 0)
267
+ ```
268
+
269
+ Use flavor 4 (`rpc_id`) so only the joining peer gets the replay. Broadcasting to all peers re-confirms state they already have — wastes bandwidth and may trigger "received update" hooks on UIs.
270
+
271
+ **Idempotency:** `_send_full_state` may run a fraction of a second after `_broadcast_health` for the same peer. Make sure your Manager's broadcast handlers are idempotent — receiving the same state twice should be a no-op.
272
+
273
+ ## Common mistakes
274
+
275
+ | Mistake | What goes wrong | Fix |
276
+ |---|---|---|
277
+ | Trusting client-supplied damage values | Client edits memory → 99999 damage one-shots everyone | Host computes damage from weapon resource; client supplies only `target` and `weapon_id`. |
278
+ | Validating *after* applying instead of before | Cheat already took effect for a frame; reverting causes visual rubber-banding | Validate first, then mutate. The mutator never runs if validation fails. |
279
+ | Broadcasting state changes via `signal.emit()` on host | Signals fire only on the emitter — clients never see the update | Use `@rpc("authority", ...)` broadcast functions. Signals are for *local* listeners (UI on the host). |
280
+ | Forgetting `peer_left` cleanup | Dictionaries grow forever as players cycle through; eventual memory leak + ghost entries | Every Manager listens to `NetworkManager.peer_left` and `erase()`s its entry. |
281
+ | Using `reliable` for high-frequency cosmetic state (footsteps, particles) | Bandwidth blowup; reliable retransmits on packet loss | `unreliable` for cosmetic; `reliable` only for state that *must* arrive. |
282
+ | Comparing floats without epsilon in validation | Legitimate clients fail validation due to physics-step jitter; their hits get silently dropped | `dist > max_range + 0.5`, never `dist > max_range`. |
283
+ | Writing to host dictionary from a non-host code path | One peer accidentally becomes "second host" → desyncs | First line of every mutator: `if not NetworkManager.is_host: return`. Treat as boilerplate. |
284
+
285
+ ## Anti-pattern: the "synced var" temptation
286
+
287
+ Godot 4 ships a `MultiplayerSynchronizer` node that automatically replicates a list of properties across the network. It's tempting because it removes RPC boilerplate. **It is wrong for anything game-logic-relevant.**
288
+
289
+ `MultiplayerSynchronizer`:
290
+ - Has no validation hook. Writes go through unconditionally.
291
+ - Has no concept of "host owns this, client requests changes."
292
+ - Replicates whatever value the configured authority node sets — and the authority is per-node, not per-property.
293
+ - Provides no audit trail when a value changes (compared to a Manager method you can breakpoint).
294
+
295
+ It is the right tool for **cosmetic state only**:
296
+ - Animation node parameters (`AnimationTree` blend values).
297
+ - Look direction / camera yaw on remote peer's visuals.
298
+ - Particle emission state on a non-gameplay decoration.
299
+
300
+ Game-logic state — health, score, inventory, cooldowns, position-of-truth — goes through Managers with explicit validate-mutate-broadcast. The five extra lines of boilerplate per field are what keeps the game shippable.
301
+
302
+ ## Putting it together
303
+
304
+ When you start a new MP game, the order is:
305
+
306
+ 1. Build `NetworkManager` (covered by `/peer-to-peer-multiplayer` Layer 1).
307
+ 2. List every piece of game state on a sheet.
308
+ 3. Run "if a malicious client lies, does it break?" on each.
309
+ 4. Group host-owned fields by domain → one Manager per domain.
310
+ 5. For each Manager: data dict, `_host_apply_*()` mutators, `_client_request_*()` RPCs with validators, `_broadcast_*()` RPCs, `peer_joined` / `peer_left` lifecycle, `_send_full_state()` for late-join.
311
+ 6. Cosmetic state goes through `MultiplayerSynchronizer` or a flavor-3 RPC.
312
+ 7. Position/velocity get host-reconciled prediction (covered by `/peer-to-peer-multiplayer` Layer 4).
313
+
314
+ ## Collaborative protocol
315
+
316
+ This skill writes new autoload files (one per Manager). Always ask before each:
317
+
318
+ > May I create `autoloads/HealthManager.gd` and register it as an autoload?
319
+ > May I add the validation function to HealthManager? Here's what I'll check: [list].
320
+ > May I wire HealthManager's `peer_joined` lifecycle?
321
+
322
+ Don't bulk-create five Managers in one shot. Walk one Manager end-to-end (data → mutators → requests → broadcasts → lifecycle) so the user can verify the shape, then repeat for the next domain.
323
+
324
+ After each Manager file lands, call `summer_get_script_errors` to confirm clean compile. See [`_shared/collaborative-protocol.md`](../../_shared/collaborative-protocol.md).
325
+
326
+ ## See also
327
+
328
+ - [`peer-to-peer-multiplayer`](../peer-to-peer-multiplayer/SKILL.md) — the four-layer architecture overview. Read this first if you haven't.
329
+ - [`setup-multiplayer`](../setup-multiplayer/SKILL.md) — lighter intro that just gets a session running. Use that one if the user just wants two players to see each other.
330
+ - [`_shared/godot-version.md`](../../_shared/godot-version.md) — Godot 4.5 multiplayer API stability notes.
331
+ - [`_shared/gd-style.md`](../../_shared/gd-style.md) — typed-GDScript conventions used in the examples above.
332
+ - [`_shared/mcp-tools-reference.md`](../../_shared/mcp-tools-reference.md) — `summer_get_script_errors` for compile verification.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: 3d-lighting
3
- description: Lighting setup, WorldEnvironment, sky, and shadows for 3D scenes. Use when setting up a 3D scene, adding lights, configuring environment, or adjusting shadows. Trigger on "lighting", "shadows", "WorldEnvironment", "sun", "ambient".
3
+ description: Use when setting up lighting in a 3D scene — adding lights, configuring WorldEnvironment, sky, or shadows. Covers DirectionalLight3D vs Omni vs Spot, shadow tuning, ambient/sky-driven lighting, and Godot 4.5-era conventions. Trigger on "lighting", "shadows", "WorldEnvironment", "sun", "ambient", "sky", "light".
4
4
  license: MIT
5
5
  compatibility: [Cursor, Claude Code, Windsurf, Codex]
6
6
  category: rendering-and-lighting