summer-engine 3.1.0 → 3.2.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +27 -5
- package/.codex-plugin/plugin.json +28 -6
- package/.cursor-plugin/plugin.json +27 -5
- package/.factory-plugin/plugin.json +27 -5
- package/.opencode/plugins/summer.js +1 -1
- package/CHANGELOG.md +102 -0
- package/README.md +18 -3
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/skills.js +20 -2
- package/dist/core/api-client.d.ts +9 -1
- package/dist/core/api-client.js +21 -4
- package/dist/core/capabilities/agent-playbook.js +46 -0
- package/dist/core/capabilities/audit/accept.d.ts +55 -0
- package/dist/core/capabilities/audit/accept.js +147 -0
- package/dist/core/capabilities/audit/args.d.ts +95 -0
- package/dist/core/capabilities/audit/args.js +89 -0
- package/dist/core/capabilities/audit/audit.d.ts +56 -0
- package/dist/core/capabilities/audit/audit.js +287 -0
- package/dist/core/capabilities/audit/frame.d.ts +14 -0
- package/dist/core/capabilities/audit/frame.js +35 -0
- package/dist/core/capabilities/audit/gaps.d.ts +96 -0
- package/dist/core/capabilities/audit/gaps.js +257 -0
- package/dist/core/capabilities/audit/judge.d.ts +162 -0
- package/dist/core/capabilities/audit/judge.js +1134 -0
- package/dist/core/capabilities/audit/math.d.ts +110 -0
- package/dist/core/capabilities/audit/math.js +263 -0
- package/dist/core/capabilities/audit/summary.d.ts +74 -0
- package/dist/core/capabilities/audit/summary.js +212 -0
- package/dist/core/capabilities/board.d.ts +31 -0
- package/dist/core/capabilities/board.js +43 -0
- package/dist/core/capabilities/camera-view.d.ts +4 -2
- package/dist/core/capabilities/camera-view.js +6 -3
- package/dist/core/capabilities/connect-signal.d.ts +85 -0
- package/dist/core/capabilities/connect-signal.js +285 -0
- package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
- package/dist/core/capabilities/engine-fallbacks.js +5 -0
- package/dist/core/capabilities/engine-ops.d.ts +5 -1
- package/dist/core/capabilities/engine-ops.js +0 -3
- package/dist/core/capabilities/engine-receipt.d.ts +12 -0
- package/dist/core/capabilities/engine-receipt.js +26 -0
- package/dist/core/capabilities/file-read.d.ts +64 -0
- package/dist/core/capabilities/file-read.js +325 -0
- package/dist/core/capabilities/game-task-plan.js +4 -2
- package/dist/core/capabilities/grep.d.ts +60 -0
- package/dist/core/capabilities/grep.js +190 -0
- package/dist/core/capabilities/inspect-node.d.ts +56 -0
- package/dist/core/capabilities/inspect-node.js +225 -0
- package/dist/core/capabilities/inspect-resource.d.ts +61 -0
- package/dist/core/capabilities/inspect-resource.js +376 -0
- package/dist/core/capabilities/math3d.d.ts +28 -0
- package/dist/core/capabilities/math3d.js +88 -0
- package/dist/core/capabilities/motion-generation.d.ts +62 -0
- package/dist/core/capabilities/motion-generation.js +138 -0
- package/dist/core/capabilities/placement-batch.d.ts +102 -0
- package/dist/core/capabilities/placement-batch.js +483 -0
- package/dist/core/capabilities/placement-math.d.ts +70 -0
- package/dist/core/capabilities/placement-math.js +209 -0
- package/dist/core/capabilities/placement-script.d.ts +42 -0
- package/dist/core/capabilities/placement-script.js +1096 -0
- package/dist/core/capabilities/placement.d.ts +485 -0
- package/dist/core/capabilities/placement.js +1731 -0
- package/dist/core/capabilities/project-context.d.ts +33 -2
- package/dist/core/capabilities/project-context.js +87 -4
- package/dist/core/capabilities/replace-node.d.ts +91 -0
- package/dist/core/capabilities/replace-node.js +423 -0
- package/dist/core/capabilities/runtime-control.d.ts +18 -2
- package/dist/core/capabilities/runtime-control.js +67 -2
- package/dist/core/capabilities/scene-batch.d.ts +63 -0
- package/dist/core/capabilities/scene-batch.js +394 -0
- package/dist/core/capabilities/scene-readback.d.ts +36 -0
- package/dist/core/capabilities/scene-readback.js +54 -0
- package/dist/core/capabilities/seeing/args.d.ts +348 -0
- package/dist/core/capabilities/seeing/args.js +178 -0
- package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
- package/dist/core/capabilities/seeing/candidates.js +282 -0
- package/dist/core/capabilities/seeing/math.d.ts +134 -0
- package/dist/core/capabilities/seeing/math.js +269 -0
- package/dist/core/capabilities/seeing/probe.d.ts +62 -0
- package/dist/core/capabilities/seeing/probe.js +188 -0
- package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
- package/dist/core/capabilities/seeing/scoring.js +639 -0
- package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
- package/dist/core/capabilities/seeing/seeing.js +1341 -0
- package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
- package/dist/core/capabilities/seeing/shot-store.js +204 -0
- package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
- package/dist/core/capabilities/surface-snap-mesh.js +425 -0
- package/dist/core/capabilities/surface-snap.d.ts +77 -0
- package/dist/core/capabilities/surface-snap.js +461 -0
- package/dist/core/capabilities/tool-dispatch.js +231 -72
- package/dist/core/capabilities/tscn.d.ts +85 -0
- package/dist/core/capabilities/tscn.js +391 -0
- package/dist/core/capabilities/world-snapshot.d.ts +58 -0
- package/dist/core/capabilities/world-snapshot.js +150 -0
- package/dist/core/capability-skew.d.ts +5 -5
- package/dist/core/capability-skew.js +14 -6
- package/dist/core/headless/worker-engine-client.d.ts +1 -1
- package/dist/core/headless/worker-engine-client.js +2 -2
- package/dist/core/library-read.d.ts +37 -5
- package/dist/core/library-read.js +264 -33
- package/dist/core/library-search.d.ts +2 -2
- package/dist/installer/agent-config.js +5 -3
- package/dist/installer/agent-table.d.ts +16 -0
- package/dist/installer/agent-table.js +52 -17
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/tools/asset-tools.js +21 -3
- package/dist/mcp/tools/audit-tools.d.ts +23 -0
- package/dist/mcp/tools/audit-tools.js +60 -0
- package/dist/mcp/tools/debug-tools.js +3 -1
- package/dist/mcp/tools/file-tools.js +17 -4
- package/dist/mcp/tools/generate-tools.js +74 -66
- package/dist/mcp/tools/library-tools.js +2 -0
- package/dist/mcp/tools/perception-tools.js +6 -19
- package/dist/mcp/tools/placement-tools.d.ts +24 -0
- package/dist/mcp/tools/placement-tools.js +91 -0
- package/dist/mcp/tools/project-tools.js +18 -15
- package/dist/mcp/tools/scene-tools.js +71 -83
- package/dist/mcp/tools/seeing-tools.d.ts +24 -0
- package/dist/mcp/tools/seeing-tools.js +64 -0
- package/dist/mcp/tools/spatial-tools.js +17 -9
- package/dist/mcp/tools/visual-tools.js +59 -7
- package/gemini-extension.json +26 -4
- package/integrations/amp/README.md +1 -1
- package/integrations/crush/README.md +1 -1
- package/integrations/grok-build/README.md +1 -1
- package/integrations/hermes/README.md +1 -1
- package/integrations/kimi-code/README.md +1 -1
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/vscode-copilot/README.md +1 -1
- package/integrations/warp/README.md +1 -1
- package/integrations/windsurf/README.md +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/references/summer-folder/resource.yaml +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +1 -1
- package/library/skills/README.md +36 -14
- package/library/skills/adaptive-music/SKILL.md +1 -1
- package/library/skills/agent-playtesting/SKILL.md +1 -1
- package/library/skills/ai-characters-in-games/SKILL.md +220 -0
- package/library/skills/ai-characters-in-games/resource.yaml +30 -0
- package/library/skills/ambient-bed/SKILL.md +1 -1
- package/library/skills/animated-loop/SKILL.md +1 -1
- package/library/skills/animation-tree/SKILL.md +1 -1
- package/library/skills/art-direction/SKILL.md +1 -1
- package/library/skills/asset-strategy/SKILL.md +2 -1
- package/library/skills/asset-strategy/resource.yaml +2 -1
- package/library/skills/audio-direction/SKILL.md +1 -1
- package/library/skills/auto-fire-targeting/SKILL.md +1 -1
- package/library/skills/brainstorm-game/SKILL.md +1 -1
- package/library/skills/brainstorming/SKILL.md +1 -1
- package/library/skills/browse-templates/SKILL.md +3 -3
- package/library/skills/browse-templates/resource.yaml +1 -1
- package/library/skills/camera-rigs/SKILL.md +128 -0
- package/library/skills/camera-rigs/resource.yaml +29 -0
- package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +1 -1
- package/library/skills/character-model/SKILL.md +1 -1
- package/library/skills/character-movement/SKILL.md +326 -0
- package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
- package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
- package/library/skills/character-movement/references/movement-3d.md +136 -0
- package/library/skills/character-movement/references/platformer-feel.md +83 -0
- package/library/skills/character-movement/resource.yaml +40 -0
- package/library/skills/character-portrait/SKILL.md +1 -1
- package/library/skills/cinematic-cutscene/SKILL.md +1 -1
- package/library/skills/combat-basics/SKILL.md +159 -0
- package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
- package/library/skills/combat-basics/resource.yaml +34 -0
- package/library/skills/concept-art/SKILL.md +1 -1
- package/library/skills/create-asset-sheet/SKILL.md +1 -1
- package/library/skills/debug/SKILL.md +1 -1
- package/library/skills/debugging-game-feel/SKILL.md +1 -1
- package/library/skills/design-level/SKILL.md +1 -1
- package/library/skills/design-mechanic/SKILL.md +1 -1
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
- package/library/skills/environment-kit/SKILL.md +1 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +1 -1
- package/library/skills/fps-controller/SKILL.md +9 -7
- package/library/skills/fps-controller/resource.yaml +4 -1
- package/library/skills/game-feel/SKILL.md +1 -1
- package/library/skills/gameskill/SKILL.md +1 -1
- package/library/skills/gdscript-patterns/SKILL.md +1 -1
- package/library/skills/generate-motion/SKILL.md +9 -9
- package/library/skills/generate-motion/resource.yaml +2 -1
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
- package/library/skills/headless-scripting/SKILL.md +1 -1
- package/library/skills/in-game-purchases/SKILL.md +254 -0
- package/library/skills/in-game-purchases/resource.yaml +29 -0
- package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
- package/library/skills/investigating-bugs/SKILL.md +1 -1
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
- package/library/skills/make-game/SKILL.md +25 -3
- package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
- package/library/skills/make-game/references/2d-platformer.md +117 -0
- package/library/skills/make-game/references/2d-rpg-content.md +234 -0
- package/library/skills/make-game/references/2d-rpg.md +241 -0
- package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
- package/library/skills/make-game/references/2d-survivors.md +237 -0
- package/library/skills/make-game/references/3d-fps-systems.md +216 -0
- package/library/skills/make-game/references/3d-fps.md +177 -0
- package/library/skills/make-game/references/3d-horror-systems.md +186 -0
- package/library/skills/make-game/references/3d-horror.md +201 -0
- package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
- package/library/skills/make-game/references/3d-third-person.md +215 -0
- package/library/skills/make-game/resource.yaml +6 -1
- package/library/skills/multiplayer/SKILL.md +150 -0
- package/library/skills/multiplayer/resource.yaml +33 -0
- package/library/skills/multiplayer-movement/SKILL.md +510 -0
- package/library/skills/multiplayer-movement/files.md +501 -0
- package/library/skills/multiplayer-movement/resource.yaml +30 -0
- package/library/skills/multiplayer-project/SKILL.md +521 -0
- package/library/skills/multiplayer-project/files.md +448 -0
- package/library/skills/multiplayer-project/resource.yaml +36 -0
- package/library/skills/multiplayer-publish/SKILL.md +229 -0
- package/library/skills/multiplayer-publish/export-errors.md +89 -0
- package/library/skills/multiplayer-publish/resource.yaml +28 -0
- package/library/skills/multiplayer-state/SKILL.md +438 -0
- package/library/skills/multiplayer-state/files.md +592 -0
- package/library/skills/multiplayer-state/resource.yaml +35 -0
- package/library/skills/multiplayer-testing/SKILL.md +236 -0
- package/library/skills/multiplayer-testing/resource.yaml +39 -0
- package/library/skills/music-track/SKILL.md +1 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/new-project/SKILL.md +1 -1
- package/library/skills/organic-model/SKILL.md +1 -1
- package/library/skills/pixel-art/SKILL.md +1 -1
- package/library/skills/play/SKILL.md +1 -1
- package/library/skills/playtesting-a-feature/SKILL.md +1 -1
- package/library/skills/procedural-animation/SKILL.md +2 -2
- package/library/skills/procedural-animation/resource.yaml +1 -1
- package/library/skills/prop-model/SKILL.md +1 -1
- package/library/skills/psx-retro-rendering/SKILL.md +1 -1
- package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
- package/library/skills/remote-deploy/SKILL.md +1 -1
- package/library/skills/retarget/SKILL.md +1 -1
- package/library/skills/running-in-the-cloud/SKILL.md +1 -1
- package/library/skills/save-load/SKILL.md +156 -0
- package/library/skills/save-load/resource.yaml +27 -0
- package/library/skills/scene-composition/SKILL.md +1 -1
- package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
- package/library/skills/scene-scripting/SKILL.md +1 -1
- package/library/skills/scene-to-level/SKILL.md +1 -1
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +1 -1
- package/library/skills/skill-test/SKILL.md +1 -1
- package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
- package/library/skills/skybox-panorama/SKILL.md +1 -1
- package/library/skills/sound-effect/SKILL.md +1 -1
- package/library/skills/spatial-placement/SKILL.md +134 -3
- package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
- package/library/skills/spatial-placement/resource.yaml +16 -2
- package/library/skills/sprite-sheet/SKILL.md +1 -1
- package/library/skills/summer-analytics/SKILL.md +147 -0
- package/library/skills/summer-analytics/resource.yaml +28 -0
- package/library/skills/summer-friends/SKILL.md +226 -0
- package/library/skills/summer-friends/resource.yaml +28 -0
- package/library/skills/summer-leaderboards/SKILL.md +231 -0
- package/library/skills/summer-leaderboards/resource.yaml +30 -0
- package/library/skills/summer-match-results/SKILL.md +286 -0
- package/library/skills/summer-match-results/resource.yaml +32 -0
- package/library/skills/summer-matchmaking/SKILL.md +301 -0
- package/library/skills/summer-matchmaking/resource.yaml +32 -0
- package/library/skills/summer-parties/SKILL.md +253 -0
- package/library/skills/summer-parties/resource.yaml +30 -0
- package/library/skills/summer-player-data/SKILL.md +322 -0
- package/library/skills/summer-player-data/resource.yaml +30 -0
- package/library/skills/summer-store/SKILL.md +271 -0
- package/library/skills/summer-store/resource.yaml +29 -0
- package/library/skills/summer-world-chat/SKILL.md +195 -0
- package/library/skills/summer-world-chat/resource.yaml +30 -0
- package/library/skills/summer-world-saves/SKILL.md +284 -0
- package/library/skills/summer-world-saves/resource.yaml +29 -0
- package/library/skills/text-to-motion/SKILL.md +99 -0
- package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
- package/library/skills/text-to-motion/resource.yaml +41 -0
- package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +1 -1
- package/library/skills/tune-performance/SKILL.md +1 -1
- package/library/skills/ui-basics/SKILL.md +10 -1
- package/library/skills/ui-graphics/SKILL.md +1 -1
- package/library/skills/use-widget-asset/SKILL.md +1 -1
- package/library/skills/using-summer/SKILL.md +2 -2
- package/library/skills/using-summer/resource.yaml +2 -2
- package/library/skills/vehicle-model/SKILL.md +1 -1
- package/library/skills/verification-before-completion/SKILL.md +1 -1
- package/library/skills/verifying-scenes/SKILL.md +76 -2
- package/library/skills/verifying-scenes/resource.yaml +13 -2
- package/library/skills/vfx-dissolve/SKILL.md +1 -1
- package/library/skills/vfx-fire/SKILL.md +1 -1
- package/library/skills/vfx-hit-spark/SKILL.md +1 -1
- package/library/skills/vfx-lightning/SKILL.md +1 -1
- package/library/skills/vfx-magic-glow/SKILL.md +1 -1
- package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
- package/library/skills/vfx-smoke/SKILL.md +1 -1
- package/library/skills/vfx-water-ripple/SKILL.md +1 -1
- package/library/skills/voice-line/SKILL.md +1 -1
- package/library/skills/web-and-touch-input/SKILL.md +38 -0
- package/library/skills/web-and-touch-input/resource.yaml +28 -0
- package/library/skills/web-compatibility-look/SKILL.md +62 -0
- package/library/skills/web-compatibility-look/resource.yaml +30 -0
- package/library/skills/world-building-3d/SKILL.md +8 -1
- package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
- package/library/skills/world-building-3d/resource.yaml +2 -1
- package/library/skills/writing-plans/SKILL.md +1 -1
- package/library/skills/writing-skills/SKILL.md +1 -1
- package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
- package/library/tools/attach-to-surface/resource.yaml +142 -0
- package/library/tools/batch/resource.yaml +9 -3
- package/library/tools/connect-ports/resource.yaml +120 -0
- package/library/tools/connect-signal/resource.yaml +3 -2
- package/library/tools/debug-views/resource.yaml +94 -0
- package/library/tools/frame-nodes/resource.yaml +104 -0
- package/library/tools/frame-shot/resource.yaml +134 -0
- package/library/tools/generate-motion/resource.yaml +44 -9
- package/library/tools/get-board/resource.yaml +50 -0
- package/library/tools/get-project-context/resource.yaml +13 -2
- package/library/tools/grep/resource.yaml +77 -0
- package/library/tools/inspect-asset/resource.yaml +71 -0
- package/library/tools/inspect-node/resource.yaml +9 -2
- package/library/tools/inspect-resource/resource.yaml +16 -5
- package/library/tools/instantiate-scene/resource.yaml +27 -2
- package/library/tools/measure/resource.yaml +96 -0
- package/library/tools/place-adjacent/resource.yaml +113 -0
- package/library/tools/play/resource.yaml +15 -2
- package/library/tools/raycast/resource.yaml +101 -0
- package/library/tools/read-file/resource.yaml +36 -2
- package/library/tools/read-library/resource.yaml +2 -2
- package/library/tools/repeat-along/resource.yaml +129 -0
- package/library/tools/replace-node/resource.yaml +11 -5
- package/library/tools/scene-audit/resource.yaml +144 -0
- package/library/tools/screenshot/resource.yaml +13 -2
- package/library/tools/search-assets/resource.yaml +18 -0
- package/library/tools/shot-sheet/resource.yaml +110 -0
- package/library/tools/snap-to-surface/resource.yaml +4 -1
- package/library/tools/world-snapshot/resource.yaml +24 -3
- package/library/tools/zoom/resource.yaml +105 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +17 -3
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +26 -4
- package/registry/generated/index.json +2154 -469
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +27 -5
- package/registry/generated/plugin.codex.json +28 -6
- package/registry/generated/plugin.cursor.json +27 -5
- package/registry/generated/plugin.factory.json +27 -5
- package/registry/generated/skills-index.md +36 -14
- package/registry/generated/skills-registry.json +421 -124
- package/registry/generated/templates-registry.json +2 -2
- package/library/skills/host-authoritative-state/SKILL.md +0 -332
- package/library/skills/host-authoritative-state/resource.yaml +0 -28
- package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
- package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
- package/library/skills/setup-multiplayer/SKILL.md +0 -336
- package/library/skills/setup-multiplayer/resource.yaml +0 -28
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# 2D Survivors - Core
|
|
2
|
+
|
|
3
|
+
Vampire Survivors-style auto-battler blueprint: arena, wave state machine, player,
|
|
4
|
+
and auto-firing weapons. The player only moves and dashes; weapons aim and fire
|
|
5
|
+
themselves. Waves, enemies, upgrades, shop, and evolution live in
|
|
6
|
+
`2d-survivors-progression.md`.
|
|
7
|
+
|
|
8
|
+
## Minimum playable base (build this first)
|
|
9
|
+
|
|
10
|
+
One arena scene: player CharacterBody2D with WASD movement, one auto-firing
|
|
11
|
+
projectile weapon, a handful of chasing enemies, and visible feedback on every
|
|
12
|
+
hit (flash + damage number). Prove "move, enemies chase, weapon kills them"
|
|
13
|
+
before waves, shop, or characters exist.
|
|
14
|
+
|
|
15
|
+
Two rules for every scene:
|
|
16
|
+
|
|
17
|
+
- Dynamic viewport: `display/window/stretch/mode = "canvas_items"`,
|
|
18
|
+
`stretch/aspect = "expand"`. All UI uses Control anchors, never hardcoded
|
|
19
|
+
pixel positions. Spawn enemies relative to the player (650-900px out), never
|
|
20
|
+
from screen coordinates.
|
|
21
|
+
- Playable slice, not tech demo: every hit needs shake/flash/particles/damage
|
|
22
|
+
number. A silent wave where numbers move but nothing feels alive fails.
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
res://
|
|
28
|
+
scenes/
|
|
29
|
+
main.tscn # main menu (run entry point)
|
|
30
|
+
levels/arena.tscn # THE gameplay scene (owns everything)
|
|
31
|
+
player/player.tscn
|
|
32
|
+
enemies/enemy.tscn # one scene reused for ALL enemy types
|
|
33
|
+
enemies/enemy_projectile.tscn
|
|
34
|
+
pickups/pickup.tscn # gold coin / health orb
|
|
35
|
+
weapons/projectile.tscn
|
|
36
|
+
ui/character_select.tscn
|
|
37
|
+
scripts/
|
|
38
|
+
autoloads/{game_manager,audio_manager}.gd
|
|
39
|
+
player/player.gd
|
|
40
|
+
enemies/{enemy_base,enemy_spawner,enemy_projectile,elite_system}.gd
|
|
41
|
+
weapons/{projectile_weapon,projectile,orbital_weapon,area_weapon}.gd
|
|
42
|
+
weapons/evolved/{piercing_projectile_weapon,radiant_orbital_weapon,supernova_weapon}.gd
|
|
43
|
+
systems/{wave_data,upgrade_data,upgrade_manager,weapon_evolution,
|
|
44
|
+
item_data,item_manager,item_registry,character_data,
|
|
45
|
+
character_registry,pickup,camera_effects,arena_hazard}.gd
|
|
46
|
+
ui/{main_menu,character_select_ui,hud,level_up_ui,shop_ui,
|
|
47
|
+
pause_menu,game_over_ui}.gd
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Two autoloads only: `GameManager` (single source of truth for ALL run state -
|
|
51
|
+
stats, gold, XP, health, wave, signals) and `AudioManager` (pooled SFX + music).
|
|
52
|
+
Every system reads and writes GameManager; UI listens to its signals.
|
|
53
|
+
|
|
54
|
+
## Physics layers
|
|
55
|
+
|
|
56
|
+
| Layer | Bit | Name |
|
|
57
|
+
|-------|-----|------|
|
|
58
|
+
| 1 | 1 | Player |
|
|
59
|
+
| 2 | 2 | Enemies |
|
|
60
|
+
| 3 | 4 | Pickups |
|
|
61
|
+
| 4 | 8 | PlayerProjectiles |
|
|
62
|
+
| 5 | 16 | Walls |
|
|
63
|
+
| 6 | 32 | EnemyProjectiles |
|
|
64
|
+
|
|
65
|
+
Wiring: Player body layer 1 mask 16 (walls only). Player Hurtbox layer 0 mask 34
|
|
66
|
+
(Enemies + EnemyProjectiles). PickupRange layer 0 mask 4. Enemies layer 2 mask 0.
|
|
67
|
+
Player projectiles layer 8 mask 2. Enemy projectiles layer 32 mask 1.
|
|
68
|
+
|
|
69
|
+
Input map: move_up/down/left/right (WASD + left stick), dash (Space + R2),
|
|
70
|
+
pause (Escape + Start).
|
|
71
|
+
|
|
72
|
+
## Wave state machine (arena.gd)
|
|
73
|
+
|
|
74
|
+
The arena scene owns the player, spawner, all managers, and all UI CanvasLayers.
|
|
75
|
+
Everything wires together via signals in `_ready()`. Per wave, 4 phases:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
PRE_WAVE (1.5s announce) -> COMBAT (timed, 20-30s) ->
|
|
79
|
+
WAVE_CLEAR (2.5s: kill all, vacuum gold, slow-mo) ->
|
|
80
|
+
SHOPPING (buy/reroll/next) -> next PRE_WAVE
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
20 waves total, bosses at 10 and 20, victory after 20, death = game over.
|
|
84
|
+
|
|
85
|
+
```gdscript
|
|
86
|
+
# arena.gd core
|
|
87
|
+
enum WaveState { PRE_WAVE, COMBAT, WAVE_CLEAR, SHOPPING }
|
|
88
|
+
|
|
89
|
+
func _physics_process(delta: float) -> void:
|
|
90
|
+
if GameManager.current_state != GameManager.State.PLAYING:
|
|
91
|
+
return
|
|
92
|
+
match _wave_state:
|
|
93
|
+
WaveState.PRE_WAVE:
|
|
94
|
+
_state_timer -= delta
|
|
95
|
+
if _state_timer <= 0.0: _start_combat()
|
|
96
|
+
WaveState.COMBAT:
|
|
97
|
+
_combat_time_remaining -= delta
|
|
98
|
+
hud.update_wave_timer(_combat_time_remaining)
|
|
99
|
+
if _combat_time_remaining <= 0.0: _start_wave_clear()
|
|
100
|
+
WaveState.WAVE_CLEAR:
|
|
101
|
+
_state_timer -= delta
|
|
102
|
+
if _state_timer <= 0.0: _start_shopping()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Wave clear = spawner off, `kill_all_enemies()`, gold bonus (5 + wave*2),
|
|
106
|
+
flash + slow_motion(0.4, 0.8) + zoom pulse. GameManager pauses the tree for
|
|
107
|
+
LEVEL_UP / SHOPPING / PAUSED / GAME_OVER states and unpauses on PLAYING:
|
|
108
|
+
|
|
109
|
+
```gdscript
|
|
110
|
+
func change_state(new_state: State) -> void:
|
|
111
|
+
current_state = new_state
|
|
112
|
+
state_changed.emit(new_state)
|
|
113
|
+
match new_state:
|
|
114
|
+
State.PLAYING: get_tree().paused = false
|
|
115
|
+
State.PAUSED, State.LEVEL_UP, State.SHOPPING, State.GAME_OVER:
|
|
116
|
+
get_tree().paused = true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Arena is a 6000x6000 field: background ColorRect, StaticBody2D walls on layer 5
|
|
120
|
+
at +-3020, Line2D border for visibility. Player collides with walls; enemies
|
|
121
|
+
do not (spawn logic clamps them).
|
|
122
|
+
|
|
123
|
+
## Player
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
Player (CharacterBody2D, layer 1, mask 16/Walls)
|
|
127
|
+
CollisionShape2D (CircleShape2D radius 14)
|
|
128
|
+
Body (Polygon2D, arrow shape, character color)
|
|
129
|
+
Hurtbox (Area2D, layer 0, mask 34)
|
|
130
|
+
CollisionShape2D (CircleShape2D radius 12)
|
|
131
|
+
PickupRange (Area2D, layer 0, mask 4)
|
|
132
|
+
CollisionShape2D (CircleShape2D radius 140, driven by GameManager stat)
|
|
133
|
+
WeaponMount (Node2D) # weapons attach here
|
|
134
|
+
Camera2D (smoothing on, speed 4.0)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Movement is trivial by design - all depth lives in the build:
|
|
138
|
+
|
|
139
|
+
```gdscript
|
|
140
|
+
func _move() -> void:
|
|
141
|
+
var input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
|
142
|
+
velocity = input * GameManager.player_speed
|
|
143
|
+
move_and_slide()
|
|
144
|
+
if input.length_squared() > 0.01:
|
|
145
|
+
body.rotation = input.angle()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Dash: 0.14s invincible burst at 650px/s, 1.2s cooldown, afterimage ghost
|
|
149
|
+
(Polygon2D copy, tween alpha to 0 over 0.2s) every 0.025s. Invincibility timer =
|
|
150
|
+
dash duration + 0.08. HUD shows the cooldown bar.
|
|
151
|
+
|
|
152
|
+
Contact damage: do NOT use `area_entered` signals. Check
|
|
153
|
+
`hurtbox.get_overlapping_areas()` manually each `_physics_process` and apply
|
|
154
|
+
damage with a 0.6s invincibility window. Signals miss continuous contact;
|
|
155
|
+
polling + i-frames is reliable.
|
|
156
|
+
|
|
157
|
+
Player also handles: regen tick (1s interval, `GameManager.player_regen` HP),
|
|
158
|
+
pickup magnet (attract pickups inside PickupRange), and a `_weapons` Dictionary
|
|
159
|
+
keyed by weapon id - `add_or_upgrade_weapon(id)` upgrades if owned, else
|
|
160
|
+
instantiates the weapon script onto WeaponMount.
|
|
161
|
+
|
|
162
|
+
## Weapons (auto-fire, no aiming)
|
|
163
|
+
|
|
164
|
+
All weapons are Node2D scripts on WeaponMount, firing from `_physics_process`.
|
|
165
|
+
Damage scales with `GameManager.player_damage_mult`, cooldowns divide by
|
|
166
|
+
`player_attack_speed_mult`, crits roll `player_crit_chance` for 2x. Three bases:
|
|
167
|
+
|
|
168
|
+
- Projectile: fires at nearest enemy within 800px (compare
|
|
169
|
+
`distance_squared_to` against 640000, never sqrt). Extra projectiles fan out
|
|
170
|
+
at 0.15 rad spread. If no target, retry in 0.1s instead of a full cooldown.
|
|
171
|
+
- Orbital: N shields orbit at radius 80, speed 3.5 rad/s. Per-enemy hit
|
|
172
|
+
cooldown Dictionary (0.4s keyed by instance id) so one orbit does not
|
|
173
|
+
multi-hit the same enemy every frame.
|
|
174
|
+
- Area (nova): every 3.0s damage all enemies within radius 120 (group scan +
|
|
175
|
+
distance_squared), show an expanding ring tween.
|
|
176
|
+
|
|
177
|
+
```gdscript
|
|
178
|
+
func _find_nearest_enemy() -> Node2D:
|
|
179
|
+
var nearest: Node2D = null
|
|
180
|
+
var best := MAX_TARGET_RANGE_SQ
|
|
181
|
+
for enemy in get_tree().get_nodes_in_group("enemies"):
|
|
182
|
+
if not is_instance_valid(enemy): continue
|
|
183
|
+
var d := global_position.distance_squared_to(enemy.global_position)
|
|
184
|
+
if d < best:
|
|
185
|
+
best = d
|
|
186
|
+
nearest = enemy
|
|
187
|
+
return nearest
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Group scan is fine to ~250 enemies (the max_alive cap); past that use spatial
|
|
191
|
+
hashing.
|
|
192
|
+
|
|
193
|
+
## Pickups and XP
|
|
194
|
+
|
|
195
|
+
XP is NOT a physical pickup - it auto-earns on kill (`GameManager.add_xp`,
|
|
196
|
+
amount = enemy xp_value). Gold and health orbs ARE physical Area2D pickups in
|
|
197
|
+
the "pickups" group with magnet behavior: once `attract()` is called (player
|
|
198
|
+
range overlap, or level-up vacuums all), accelerate toward the player at up to
|
|
199
|
+
650px/s and collect within 12px. Level-up curve:
|
|
200
|
+
`xp_needed = 5 + level*level + level*3`.
|
|
201
|
+
|
|
202
|
+
## Camera effects (the juice layer)
|
|
203
|
+
|
|
204
|
+
`camera_effects.gd` attaches as a child of the player's Camera2D. Provides
|
|
205
|
+
`screen_shake(intensity, duration)` (tween_method randomized offset decaying to
|
|
206
|
+
zero), `screen_flash(color, duration)`, `slow_motion(scale, duration)` (set
|
|
207
|
+
`Engine.time_scale`, restore via tween with `set_ignore_time_scale(true)`),
|
|
208
|
+
`zoom_pulse`, and a low-HP vignette shader that fades in below 35% health.
|
|
209
|
+
Register it on GameManager (`GameManager.camera_fx`) so every system can call it.
|
|
210
|
+
|
|
211
|
+
## Build order
|
|
212
|
+
|
|
213
|
+
1. project.godot: autoloads, input map, layer names, main scene, 1920x1080
|
|
214
|
+
canvas_items/expand stretch. Audio buses: Master, Music, SFX.
|
|
215
|
+
2. Scenes smallest-first: pickup, projectile, enemy_projectile, enemy, player,
|
|
216
|
+
character_select, main, arena.
|
|
217
|
+
3. Base loop: player move + dash, projectile weapon, walker enemies chasing.
|
|
218
|
+
4. GameManager stats + HUD (health, gold, XP bar, wave timer, kills, dash bar).
|
|
219
|
+
5. Wave state machine + spawner + wave data (progression file).
|
|
220
|
+
6. Level-up upgrades, shop, elites, evolution, characters (progression file).
|
|
221
|
+
|
|
222
|
+
## Traps
|
|
223
|
+
|
|
224
|
+
- Physics-based enemies. Never RigidBody2D or move_and_slide for enemies -
|
|
225
|
+
direct `position +=` on Area2D. Hundreds of colliding bodies kill perf;
|
|
226
|
+
enemies only need to chase, not collide with each other.
|
|
227
|
+
- Heavy enemy scenes. One Area2D + CollisionShape2D + Polygon2D + ProgressBar.
|
|
228
|
+
No AnimationPlayer, no particles, no per-enemy shader materials - you will
|
|
229
|
+
have 200+ alive.
|
|
230
|
+
- UI without `process_mode = ALWAYS`. The tree pauses during level-up, shop,
|
|
231
|
+
and game over; without ALWAYS, no button responds and the game soft-locks.
|
|
232
|
+
- Manual aim. The entire input surface is move + dash. Weapons auto-target;
|
|
233
|
+
adding aim breaks the genre.
|
|
234
|
+
- Editor-connected signals on runtime-instantiated scenes. Connect in `_ready()`
|
|
235
|
+
via code for enemies/projectiles/pickups - editor connections on templates
|
|
236
|
+
are fragile and undebuggable.
|
|
237
|
+
- On-screen spawns. Always 650-900px from the player, clamped to arena bounds.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# 3D FPS - Systems
|
|
2
|
+
|
|
3
|
+
The combat and interaction layer on top of `3d-fps.md`: hitscan and
|
|
4
|
+
projectile shooting, the data-driven weapon system, raycast interaction, and
|
|
5
|
+
the HUD.
|
|
6
|
+
|
|
7
|
+
## Shooting: hitscan vs projectile
|
|
8
|
+
|
|
9
|
+
Hitscan (raycast) for rifles/pistols, projectiles for rockets/grenades.
|
|
10
|
+
Most weapons should be hitscan - instant, cheap, reliable.
|
|
11
|
+
|
|
12
|
+
### Hitscan
|
|
13
|
+
|
|
14
|
+
Uses the player's ShootRay (RayCast3D under Camera3D, target (0,0,-100),
|
|
15
|
+
mask 6 = Environment + Enemy):
|
|
16
|
+
|
|
17
|
+
```gdscript
|
|
18
|
+
func _shoot() -> void:
|
|
19
|
+
if not can_shoot:
|
|
20
|
+
return
|
|
21
|
+
can_shoot = false
|
|
22
|
+
shoot_cooldown = 0.1 # fire rate
|
|
23
|
+
shoot_ray.force_raycast_update()
|
|
24
|
+
if shoot_ray.is_colliding():
|
|
25
|
+
var collider := shoot_ray.get_collider()
|
|
26
|
+
_spawn_hit_effect(shoot_ray.get_collision_point(), shoot_ray.get_collision_normal())
|
|
27
|
+
if collider.has_method("take_damage"):
|
|
28
|
+
collider.take_damage(25.0)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`force_raycast_update()` matters: the ray otherwise reports last frame's
|
|
32
|
+
state and fast turns produce misses. Cooldown ticks down in `_process` and
|
|
33
|
+
re-arms `can_shoot`.
|
|
34
|
+
|
|
35
|
+
### Projectile
|
|
36
|
+
|
|
37
|
+
CharacterBody3D moved with `move_and_collide` (not RigidBody - you want
|
|
38
|
+
deterministic flight):
|
|
39
|
+
|
|
40
|
+
```gdscript
|
|
41
|
+
func _physics_process(delta: float) -> void:
|
|
42
|
+
time_alive += delta
|
|
43
|
+
if time_alive >= lifetime: # 5s despawn, always
|
|
44
|
+
queue_free()
|
|
45
|
+
return
|
|
46
|
+
velocity = direction * speed
|
|
47
|
+
if gravity_affected:
|
|
48
|
+
velocity.y -= 9.8 * delta
|
|
49
|
+
var collision := move_and_collide(velocity * delta)
|
|
50
|
+
if collision:
|
|
51
|
+
var collider := collision.get_collider()
|
|
52
|
+
if collider and collider.has_method("take_damage"):
|
|
53
|
+
collider.take_damage(damage)
|
|
54
|
+
_explode()
|
|
55
|
+
queue_free()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Spawn from the camera, not the weapon model, so shots go where the
|
|
59
|
+
crosshair points:
|
|
60
|
+
|
|
61
|
+
```gdscript
|
|
62
|
+
proj.global_position = camera.global_position
|
|
63
|
+
proj.direction = -camera.global_transform.basis.z
|
|
64
|
+
get_tree().current_scene.add_child(proj)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Weapon system (data-driven)
|
|
68
|
+
|
|
69
|
+
WeaponData is a Resource; one .tres (or in-code instance) per weapon:
|
|
70
|
+
|
|
71
|
+
```gdscript
|
|
72
|
+
class_name WeaponData
|
|
73
|
+
extends Resource
|
|
74
|
+
|
|
75
|
+
@export var weapon_name: String = "Pistol"
|
|
76
|
+
@export var damage: float = 25.0
|
|
77
|
+
@export var fire_rate: float = 0.15 # seconds between shots
|
|
78
|
+
@export var reload_time: float = 1.5
|
|
79
|
+
@export var magazine_size: int = 12
|
|
80
|
+
@export var max_ammo: int = 120
|
|
81
|
+
@export var is_automatic: bool = false
|
|
82
|
+
@export var is_hitscan: bool = true
|
|
83
|
+
@export var spread: float = 0.0 # radians of random spread
|
|
84
|
+
@export var projectile_scene: PackedScene = null
|
|
85
|
+
@export var weapon_model_scene: PackedScene = null
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
WeaponManager (Node3D child of Player) owns `weapons: Array[WeaponData]`,
|
|
89
|
+
current index, magazine/reserve ammo, and these signals for the HUD:
|
|
90
|
+
`weapon_changed`, `ammo_changed(current, reserve)`,
|
|
91
|
+
`reload_started(time)`, `reload_finished`.
|
|
92
|
+
|
|
93
|
+
Core rules learned the hard way:
|
|
94
|
+
|
|
95
|
+
- Automatic vs semi-auto is one branch: automatic polls
|
|
96
|
+
`Input.is_action_pressed("shoot")`, semi-auto uses
|
|
97
|
+
`is_action_just_pressed`.
|
|
98
|
+
- Fire path: refuse if `not can_shoot or is_reloading`; if magazine empty,
|
|
99
|
+
auto-start reload instead of firing; decrement ammo and emit
|
|
100
|
+
`ammo_changed` BEFORE resolving the hit.
|
|
101
|
+
- Spread: randomize `shoot_ray.target_position` x/y within +-spread before
|
|
102
|
+
the raycast, then RESET it to (0,0,-100) after - a forgotten reset makes
|
|
103
|
+
every later weapon inherit the spread.
|
|
104
|
+
- Switching: clear WeaponHolder children (`queue_free`), instantiate the
|
|
105
|
+
new `weapon_model_scene`, reset `is_reloading`/`can_shoot`/`shoot_timer`.
|
|
106
|
+
Guard against re-equipping the same index.
|
|
107
|
+
- Reload:
|
|
108
|
+
|
|
109
|
+
```gdscript
|
|
110
|
+
func _start_reload() -> void:
|
|
111
|
+
if is_reloading or magazine_ammo == current_weapon.magazine_size:
|
|
112
|
+
return
|
|
113
|
+
if reserve_ammo <= 0:
|
|
114
|
+
return
|
|
115
|
+
is_reloading = true
|
|
116
|
+
can_shoot = false
|
|
117
|
+
reload_started.emit(current_weapon.reload_time)
|
|
118
|
+
var tween := create_tween()
|
|
119
|
+
tween.tween_interval(current_weapon.reload_time)
|
|
120
|
+
tween.tween_callback(_finish_reload)
|
|
121
|
+
|
|
122
|
+
func _finish_reload() -> void:
|
|
123
|
+
var available := mini(current_weapon.magazine_size - magazine_ammo, reserve_ammo)
|
|
124
|
+
magazine_ammo += available
|
|
125
|
+
reserve_ammo -= available
|
|
126
|
+
is_reloading = false
|
|
127
|
+
can_shoot = true
|
|
128
|
+
ammo_changed.emit(magazine_ammo, reserve_ammo)
|
|
129
|
+
reload_finished.emit()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The `is_reloading` guard is not optional - without it players stack reload
|
|
133
|
+
timers. Partial reloads take `mini(needed, reserve)`, never a full magazine.
|
|
134
|
+
|
|
135
|
+
Weapon switching by number keys: loop `weapons.size()` checking
|
|
136
|
+
`Input.is_action_just_pressed("weapon_%d" % (i + 1))`.
|
|
137
|
+
|
|
138
|
+
## Interaction system
|
|
139
|
+
|
|
140
|
+
Duck-typed interface - any node is interactable if it implements:
|
|
141
|
+
|
|
142
|
+
```gdscript
|
|
143
|
+
func interact(player: Node3D) -> void
|
|
144
|
+
func get_interaction_prompt() -> String
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The player polls its InteractionRay every physics frame and caches
|
|
148
|
+
`look_target`. Check the collider AND its parent for the methods (the
|
|
149
|
+
collider is often a StaticBody3D child of the actual object):
|
|
150
|
+
|
|
151
|
+
```gdscript
|
|
152
|
+
func _update_interaction_target() -> void:
|
|
153
|
+
if interaction_ray.is_colliding():
|
|
154
|
+
var target: Node = interaction_ray.get_collider()
|
|
155
|
+
if target and not target.has_method("get_interaction_prompt"):
|
|
156
|
+
target = target.get_parent()
|
|
157
|
+
if target and target.has_method("get_interaction_prompt"):
|
|
158
|
+
look_target = target
|
|
159
|
+
return
|
|
160
|
+
look_target = null
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Door pattern
|
|
164
|
+
|
|
165
|
+
Node3D root that rotates; mesh + StaticBody3D child positioned at
|
|
166
|
+
(0.5, 1.4, 0) so the hinge is the parent origin, not the door center.
|
|
167
|
+
`_process` lerps `rotation.y` toward `target_rotation`. Open away from the
|
|
168
|
+
player by checking which side they are on:
|
|
169
|
+
|
|
170
|
+
```gdscript
|
|
171
|
+
var to_door := global_position - player.global_position
|
|
172
|
+
var local_dir := global_transform.basis.inverse() * to_door
|
|
173
|
+
target_rotation = deg_to_rad(open_angle if local_dir.z > 0 else -open_angle)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Prompt reflects state: "Locked" / "Open" / "Close". The door body's
|
|
177
|
+
collision_layer is 6 (Environment + Interactable) so it both blocks
|
|
178
|
+
movement and answers the interaction ray.
|
|
179
|
+
|
|
180
|
+
### Pickup pattern
|
|
181
|
+
|
|
182
|
+
RigidBody3D with `freeze = true` (physics off, still hit by rays), idle bob
|
|
183
|
+
(`base_y + sin(bob_time) * 0.05`) and slow rotate, randomized starting
|
|
184
|
+
phase (`bob_time = randf() * TAU`) so a row of pickups does not bob in
|
|
185
|
+
lockstep. `interact()` applies the effect and `queue_free()`s.
|
|
186
|
+
|
|
187
|
+
## HUD
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
HUD (CanvasLayer)
|
|
191
|
+
Crosshair (TextureRect, centered 16x16)
|
|
192
|
+
InteractionPrompt (Label, bottom center, "[E] Open")
|
|
193
|
+
AmmoLabel (Label, bottom right, "12 / 120")
|
|
194
|
+
HealthBar (ProgressBar, bottom left)
|
|
195
|
+
DamageOverlay (ColorRect, full rect, red, alpha 0)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Wiring pattern: the HUD finds the player via the "player" group in
|
|
199
|
+
`_process` (retry until found - spawn order is not guaranteed), then
|
|
200
|
+
connects `weapon_manager.ammo_changed`. Interaction prompt shows
|
|
201
|
+
`"[E] " + player.look_target.get_interaction_prompt()` while `look_target`
|
|
202
|
+
is set, hides otherwise.
|
|
203
|
+
|
|
204
|
+
Damage feedback: on hit, set the overlay to `Color(1, 0, 0, 0.3)` and fade
|
|
205
|
+
alpha down at 2.0/s in `_process`. Cheap and effective.
|
|
206
|
+
|
|
207
|
+
## Order of work
|
|
208
|
+
|
|
209
|
+
1. Hitscan fire on the base controller (fixed 25 damage, no weapon data).
|
|
210
|
+
2. Hit effects + damage overlay - shooting must feel real before it is
|
|
211
|
+
configurable.
|
|
212
|
+
3. WeaponData + WeaponManager, migrate the hardcoded fire into it, add
|
|
213
|
+
ammo/reload + HUD ammo label.
|
|
214
|
+
4. Second and third weapons (one automatic with spread, one projectile) to
|
|
215
|
+
prove the data path.
|
|
216
|
+
5. Interaction ray + door + pickup + prompt label.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# 3D FPS - Core
|
|
2
|
+
|
|
3
|
+
First-person shooter blueprint: player hierarchy, mouse look, movement feel,
|
|
4
|
+
and level setup. Shooting, weapons, interaction, and HUD live in
|
|
5
|
+
`3d-fps-systems.md`.
|
|
6
|
+
|
|
7
|
+
Rule for Summer Engine builds: scene operations (.tscn) go through the scene
|
|
8
|
+
tools (`summer_add_node` / `summer_set_prop` / `summer_set_resource_property`,
|
|
9
|
+
or one `summer_batch`, always with `scenePath`); script files (.gd) go through
|
|
10
|
+
`summer_write_file` / `summer_replace_text`. Never cross the streams. For a
|
|
11
|
+
production controller with air control and external velocity, see the
|
|
12
|
+
`fps-controller` skill.
|
|
13
|
+
|
|
14
|
+
## Minimum playable base (build this first)
|
|
15
|
+
|
|
16
|
+
Level with ground + light + WorldEnvironment, the player scene below, mouse
|
|
17
|
+
look + WASD + jump. Prove "walk around and look" before any weapon exists.
|
|
18
|
+
|
|
19
|
+
## Player hierarchy (non-negotiable)
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Player (CharacterBody3D, group "player", collision_mask 6)
|
|
23
|
+
StandingShape (CollisionShape3D, capsule r 0.35 h 1.8, at y 0.9)
|
|
24
|
+
CrouchingShape (CollisionShape3D, capsule r 0.35 h 1.0, at y 0.5, disabled)
|
|
25
|
+
Head (Node3D, y 1.5) # receives PITCH
|
|
26
|
+
Camera3D (fov 70)
|
|
27
|
+
WeaponHolder (Node3D, (0.2, -0.15, -0.4))
|
|
28
|
+
InteractionRay (RayCast3D, target (0,0,-2.5), mask 4)
|
|
29
|
+
ShootRay (RayCast3D, target (0,0,-100), mask 6)
|
|
30
|
+
CeilingCheck (RayCast3D, at y 1, target (0,1,0), mask 2)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Why this exact shape:
|
|
34
|
+
|
|
35
|
+
- Yaw rotates the Player ROOT, pitch rotates Head. Rotating the camera on
|
|
36
|
+
both axes causes gimbal lock and drift.
|
|
37
|
+
- Camera under Head inherits both rotations; WeaponHolder under Camera makes
|
|
38
|
+
weapons track the view exactly.
|
|
39
|
+
- Two collision shapes for crouch, toggled via `disabled`. Resizing one
|
|
40
|
+
shape at runtime glitches physics.
|
|
41
|
+
- Short InteractionRay (2.5m) and long ShootRay (100m) are separate rays with
|
|
42
|
+
separate masks.
|
|
43
|
+
|
|
44
|
+
Collision layers: 1 Player, 2 Environment, 3 Interactable, 4 Enemy. ShootRay
|
|
45
|
+
mask 6 = Environment + Enemy (never the player). InteractionRay mask 4 =
|
|
46
|
+
Interactable only.
|
|
47
|
+
|
|
48
|
+
Input map before any scripts: move_forward/back/left/right, jump (Space),
|
|
49
|
+
sprint (Shift), crouch (C), shoot (LMB), aim (RMB), reload (R), interact (E),
|
|
50
|
+
weapon_1/2/3, pause (Escape).
|
|
51
|
+
|
|
52
|
+
## Controller script
|
|
53
|
+
|
|
54
|
+
Tuning baseline: walk 5.0, sprint 8.0, crouch 2.5, acceleration 10.0, jump
|
|
55
|
+
4.5, mouse sensitivity 0.002, FOV 70 default / 80 sprint / 50 aim.
|
|
56
|
+
|
|
57
|
+
```gdscript
|
|
58
|
+
func _ready() -> void:
|
|
59
|
+
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
60
|
+
|
|
61
|
+
func _unhandled_input(event: InputEvent) -> void:
|
|
62
|
+
if event.is_action_pressed("ui_cancel"):
|
|
63
|
+
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
64
|
+
return
|
|
65
|
+
if event is InputEventMouseButton and event.pressed \
|
|
66
|
+
and Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
|
|
67
|
+
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
68
|
+
get_viewport().set_input_as_handled()
|
|
69
|
+
return
|
|
70
|
+
if event is InputEventMouseMotion:
|
|
71
|
+
rotate_y(-event.relative.x * mouse_sensitivity)
|
|
72
|
+
head.rotate_x(-event.relative.y * mouse_sensitivity)
|
|
73
|
+
head.rotation.x = clampf(head.rotation.x, deg_to_rad(-85.0), deg_to_rad(85.0))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Mouse capture is mandatory or look does not work; the Escape release +
|
|
77
|
+
click-to-recapture pair is mandatory or the player cannot quit.
|
|
78
|
+
|
|
79
|
+
Movement (in `_physics_process`, never `_process`):
|
|
80
|
+
|
|
81
|
+
```gdscript
|
|
82
|
+
func _handle_movement(delta: float) -> void:
|
|
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.0, input_dir.y)).normalized()
|
|
85
|
+
if direction:
|
|
86
|
+
velocity.x = lerpf(velocity.x, direction.x * current_speed, acceleration * delta)
|
|
87
|
+
velocity.z = lerpf(velocity.z, direction.z * current_speed, acceleration * delta)
|
|
88
|
+
else:
|
|
89
|
+
velocity.x = lerpf(velocity.x, 0.0, acceleration * delta)
|
|
90
|
+
velocity.z = lerpf(velocity.z, 0.0, acceleration * delta)
|
|
91
|
+
if is_on_floor() and Input.is_action_just_pressed("jump") and not is_crouching:
|
|
92
|
+
velocity.y = jump_velocity
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Gravity from project settings, not a magic number:
|
|
96
|
+
`velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta`
|
|
97
|
+
when not on floor. Then `move_and_slide()`.
|
|
98
|
+
|
|
99
|
+
### Sprint + stamina
|
|
100
|
+
|
|
101
|
+
Sprint only while pressing sprint, moving (velocity > 0.5), not crouching,
|
|
102
|
+
and stamina > 0. Drain 20/s, regen 15/s to 100. Sprinting also pushes FOV to
|
|
103
|
+
80 (lerp at 5.0/delta), aim pulls to 50 - the FOV lerp is a big part of feel.
|
|
104
|
+
|
|
105
|
+
### Crouch
|
|
106
|
+
|
|
107
|
+
Toggle: swap which CollisionShape3D is disabled, lerp `head.position.y`
|
|
108
|
+
between 1.5 and 0.8 at 8.0/delta. Standing up first checks
|
|
109
|
+
`ceiling_check.is_colliding()` - refuse to stand under low geometry.
|
|
110
|
+
Crouching cancels sprint and blocks jump.
|
|
111
|
+
|
|
112
|
+
### Headbob + footsteps
|
|
113
|
+
|
|
114
|
+
```gdscript
|
|
115
|
+
func _handle_headbob(delta: float) -> void:
|
|
116
|
+
var speed := Vector2(velocity.x, velocity.z).length()
|
|
117
|
+
if speed < 0.5 or not is_on_floor():
|
|
118
|
+
camera.position.y = lerpf(camera.position.y, 0.0, 5.0 * delta)
|
|
119
|
+
camera.position.x = lerpf(camera.position.x, 0.0, 5.0 * delta)
|
|
120
|
+
return
|
|
121
|
+
headbob_time += delta * (14.0 if is_sprinting else 10.0)
|
|
122
|
+
var intensity := 0.05 if is_sprinting else 0.03
|
|
123
|
+
var bob_y := sin(headbob_time) * intensity
|
|
124
|
+
camera.position.y = bob_y
|
|
125
|
+
camera.position.x = sin(headbob_time * 0.5) * intensity * 0.5
|
|
126
|
+
if headbob_last_y >= 0.0 and bob_y < 0.0:
|
|
127
|
+
_play_footstep() # sine crossing zero downward = foot plants
|
|
128
|
+
headbob_last_y = bob_y
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The lerp-to-zero branch when stopped is required - without it the camera
|
|
132
|
+
freezes mid-bob. The zero-crossing footstep trigger syncs audio to motion
|
|
133
|
+
for free.
|
|
134
|
+
|
|
135
|
+
## Level scene
|
|
136
|
+
|
|
137
|
+
The real level comes from real assets, not primitives: route the need through
|
|
138
|
+
the `asset-strategy` skill (`summer_search_assets`, then `summer_import_asset` /
|
|
139
|
+
`summer_import_asset_by_id`), import a ready location or kit that fits, and build
|
|
140
|
+
gameplay inside it; weapons and props should share its style. A PlaneMesh ground
|
|
141
|
+
is only the walk-test blockout and for pieces no asset covers — never the
|
|
142
|
+
shipped level when a real one is available.
|
|
143
|
+
|
|
144
|
+
Blockout minimum: Node3D root + WorldEnvironment + ground (StaticBody3D with
|
|
145
|
+
BoxShape3D collision AND a PlaneMesh visual - a MeshInstance3D alone has no
|
|
146
|
+
collision) + DirectionalLight3D at (-45, 30, 0) with shadows + the player
|
|
147
|
+
instance at (0, 1, 0).
|
|
148
|
+
|
|
149
|
+
Environment presets:
|
|
150
|
+
|
|
151
|
+
- Outdoor: ProceduralSkyMaterial sky, angled DirectionalLight3D with
|
|
152
|
+
shadows, ambient from sky.
|
|
153
|
+
- Indoor: no sky (custom color background), Omni/SpotLights per room, low
|
|
154
|
+
ambient, SSAO on, optional fog.
|
|
155
|
+
|
|
156
|
+
## Build order
|
|
157
|
+
|
|
158
|
+
1. Input map + collision layer names.
|
|
159
|
+
2. Player scene per the hierarchy above (MCP), controller script (files).
|
|
160
|
+
3. Level: blockout ground, light, environment. Walk test.
|
|
161
|
+
4. Swap the blockout for an imported location + props (`asset-strategy`).
|
|
162
|
+
5. Hitscan shooting + one weapon + crosshair (`3d-fps-systems.md`).
|
|
163
|
+
6. Interaction (door, pickup), HUD, then weapon roster.
|
|
164
|
+
|
|
165
|
+
## Traps
|
|
166
|
+
|
|
167
|
+
- Mouse look on the Camera directly - split yaw (root) / pitch (Head) or
|
|
168
|
+
you get gimbal lock.
|
|
169
|
+
- Movement in `_process` - variable framerate stutter. CharacterBody3D
|
|
170
|
+
moves in `_physics_process` only.
|
|
171
|
+
- Forgetting `MOUSE_MODE_CAPTURED` (look does nothing) or the Escape
|
|
172
|
+
release (player trapped).
|
|
173
|
+
- Camera at y 0 - you see from your feet. Head height is 1.5-1.7.
|
|
174
|
+
- Level geometry without StaticBody3D collision - the player falls through
|
|
175
|
+
anything that is only a MeshInstance3D.
|
|
176
|
+
- One resized collision shape for crouch instead of two toggled shapes.
|
|
177
|
+
- ShootRay mask including the Player layer - you shoot yourself; mask 6.
|