summer-engine 2.6.6 → 2.8.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 +1 -1
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/plugin.json +6 -2
- package/.cursor-plugin/plugin.json +5 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +2 -2
- package/AGENTS.md +11 -7
- package/CHANGELOG.md +40 -0
- package/GEMINI.md +9 -7
- package/README.md +63 -21
- package/_persona/summer/SKILL.md +1 -0
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +12 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +123 -36
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +15 -4
- package/dist/lib/api-client.js +138 -13
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +33 -1
- package/dist/lib/skills-registry.js +55 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +10 -3
- package/dist/mcp/server.js +104 -11
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +159 -11
- package/dist/mcp/tools/project-tools.js +37 -27
- package/dist/mcp/tools/scene-tools.js +106 -29
- package/dist/mcp/tools/visual-tools.js +4 -3
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +74 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +140 -0
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +7 -9
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/mcpupdate/SKILL.md +9 -9
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +32 -3
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Autopilot — drives the player through a list of waypoints, saves a frame at each,
|
|
2
|
+
# asserts what it saw, and exits.
|
|
3
|
+
#
|
|
4
|
+
# This is a starting point, not a finished test. Edit CONFIG below to match your game,
|
|
5
|
+
# then add your own assertions in _check_at_waypoint(). Everything here is ordinary
|
|
6
|
+
# GDScript against your real running game — there is no test framework to learn.
|
|
7
|
+
#
|
|
8
|
+
# Run it:
|
|
9
|
+
# bash tests/autopilot/run.sh
|
|
10
|
+
# or from an agent, as a RunVerification probe (see tests/autopilot/README.md).
|
|
11
|
+
|
|
12
|
+
extends "res://tests/autopilot/probe_base.gd"
|
|
13
|
+
|
|
14
|
+
# ── CONFIG — edit this ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
# Node path to the player, from the current scene root.
|
|
17
|
+
const PLAYER_PATH := "Player"
|
|
18
|
+
|
|
19
|
+
# Input action names from your project's Input Map. Leave an entry empty to skip it.
|
|
20
|
+
const ACT_LEFT := "move_left"
|
|
21
|
+
const ACT_RIGHT := "move_right"
|
|
22
|
+
const ACT_UP := "" # "move_forward" in 3D, "move_up" in top-down 2D
|
|
23
|
+
const ACT_DOWN := ""
|
|
24
|
+
const ACT_JUMP := "jump"
|
|
25
|
+
|
|
26
|
+
# Where to walk. 2D games use x/y in pixels; 3D games use x/z in metres (y ignored).
|
|
27
|
+
# Positions are WORLD positions, not offsets.
|
|
28
|
+
const WAYPOINTS := [
|
|
29
|
+
Vector3(300, 0, 0),
|
|
30
|
+
Vector3(600, 0, 0),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# How close counts as arrived, and how long to allow per leg before giving up.
|
|
34
|
+
const ARRIVE_EPSILON := 40.0
|
|
35
|
+
const LEG_TIMEOUT_FRAMES := 240
|
|
36
|
+
|
|
37
|
+
# ── END CONFIG ────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
var _player: Node = null
|
|
40
|
+
var _is_3d := false
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
func _ready() -> void:
|
|
44
|
+
await super._ready()
|
|
45
|
+
|
|
46
|
+
# Let the scene settle. Physics frames, not timers — timers are wall-clock and
|
|
47
|
+
# make results differ between runs and between machines.
|
|
48
|
+
for _i in 30:
|
|
49
|
+
await get_tree().physics_frame
|
|
50
|
+
|
|
51
|
+
if not _bind_player():
|
|
52
|
+
finish()
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
report("player_path", PLAYER_PATH)
|
|
56
|
+
report("player_class", _player.get_class())
|
|
57
|
+
report("mode", "3D" if _is_3d else "2D")
|
|
58
|
+
report("start_pos", str(_pos()))
|
|
59
|
+
dump_tree()
|
|
60
|
+
save_frame("00_start")
|
|
61
|
+
|
|
62
|
+
for i in WAYPOINTS.size():
|
|
63
|
+
var target: Vector3 = WAYPOINTS[i]
|
|
64
|
+
var arrived := await _walk_to(target)
|
|
65
|
+
report("waypoint_%d_reached" % i, arrived)
|
|
66
|
+
report("waypoint_%d_pos" % i, str(_pos()))
|
|
67
|
+
save_frame("%02d_waypoint_%d" % [i + 1, i])
|
|
68
|
+
_check_at_waypoint(i)
|
|
69
|
+
if not arrived:
|
|
70
|
+
report("failed_at_waypoint", i)
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
_check_at_end()
|
|
74
|
+
finish()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── Your assertions go here ───────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
# Called after arriving at (or failing to reach) each waypoint.
|
|
80
|
+
func _check_at_waypoint(index: int) -> void:
|
|
81
|
+
# Assert things that are TRUE OR FALSE, not things that are approximately a number.
|
|
82
|
+
# Hold durations are wall-clock, so exact distances drift between runs.
|
|
83
|
+
#
|
|
84
|
+
# var hud := get_tree().current_scene.get_node_or_null("UI/HUD")
|
|
85
|
+
# report("hud_visible_at_%d" % index, hud != null and hud.visible)
|
|
86
|
+
# report("enemies_alive_at_%d" % index, get_tree().get_nodes_in_group("enemy").size())
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Called once after the last waypoint.
|
|
91
|
+
func _check_at_end() -> void:
|
|
92
|
+
# report("player_alive", _player.has_method("is_alive") and _player.is_alive())
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── Machinery — you should not need to edit below this line ───────────────────────
|
|
97
|
+
|
|
98
|
+
func _bind_player() -> bool:
|
|
99
|
+
var scene := get_tree().current_scene
|
|
100
|
+
if scene == null:
|
|
101
|
+
report("error", "no current scene — is run/main_scene set in project.godot?")
|
|
102
|
+
return false
|
|
103
|
+
_player = scene.get_node_or_null(PLAYER_PATH)
|
|
104
|
+
if _player == null:
|
|
105
|
+
report("error", "player not found at '%s'" % PLAYER_PATH)
|
|
106
|
+
report("scene_children", _child_names(scene))
|
|
107
|
+
dump_tree()
|
|
108
|
+
return false
|
|
109
|
+
_is_3d = _player is Node3D
|
|
110
|
+
if not (_is_3d or _player is Node2D):
|
|
111
|
+
report("error", "player at '%s' is neither Node2D nor Node3D" % PLAYER_PATH)
|
|
112
|
+
return false
|
|
113
|
+
return true
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
func _child_names(n: Node) -> Array:
|
|
117
|
+
var names := []
|
|
118
|
+
for c in n.get_children():
|
|
119
|
+
names.append(c.name)
|
|
120
|
+
return names
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Player position normalised to a Vector3 so one code path handles 2D and 3D.
|
|
124
|
+
# In 2D, x/y are screen pixels and z is unused.
|
|
125
|
+
func _pos() -> Vector3:
|
|
126
|
+
if _is_3d:
|
|
127
|
+
var p: Vector3 = _player.global_position
|
|
128
|
+
return p
|
|
129
|
+
var p2: Vector2 = _player.global_position
|
|
130
|
+
return Vector3(p2.x, p2.y, 0.0)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Distance that matters for "did we arrive", measured ONLY on axes the game can
|
|
134
|
+
# actually steer. A side-on platformer has no up/down actions, so its Y is gravity,
|
|
135
|
+
# not a destination — including it would mean the player never "arrives" while
|
|
136
|
+
# standing on the ground 368 pixels below Y=0. That is a real failure this scaffold
|
|
137
|
+
# hit on its first run.
|
|
138
|
+
func _planar_distance(a: Vector3, b: Vector3) -> float:
|
|
139
|
+
var d := 0.0
|
|
140
|
+
if _has_horizontal_actions():
|
|
141
|
+
d += pow(b.x - a.x, 2.0)
|
|
142
|
+
if _has_secondary_actions():
|
|
143
|
+
var d2 := (b.z - a.z) if _is_3d else (b.y - a.y)
|
|
144
|
+
d += pow(d2, 2.0)
|
|
145
|
+
return sqrt(d)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
func _has_horizontal_actions() -> bool:
|
|
149
|
+
return _action_live(ACT_LEFT) or _action_live(ACT_RIGHT)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
func _has_secondary_actions() -> bool:
|
|
153
|
+
return _action_live(ACT_UP) or _action_live(ACT_DOWN)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
func _action_live(action: String) -> bool:
|
|
157
|
+
return action != "" and InputMap.has_action(action)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
func _walk_to(target: Vector3) -> bool:
|
|
161
|
+
var frames := 0
|
|
162
|
+
var held: Array[String] = []
|
|
163
|
+
|
|
164
|
+
while frames < LEG_TIMEOUT_FRAMES:
|
|
165
|
+
var here := _pos()
|
|
166
|
+
if _planar_distance(here, target) <= ARRIVE_EPSILON:
|
|
167
|
+
_release(held)
|
|
168
|
+
return true
|
|
169
|
+
|
|
170
|
+
var want := _actions_toward(here, target)
|
|
171
|
+
# Only touch actions that changed, so a held key stays held across frames
|
|
172
|
+
# instead of being released and re-pressed every frame.
|
|
173
|
+
for a in held:
|
|
174
|
+
if not want.has(a):
|
|
175
|
+
_set_action(a, false)
|
|
176
|
+
for a in want:
|
|
177
|
+
if not held.has(a):
|
|
178
|
+
_set_action(a, true)
|
|
179
|
+
held = want
|
|
180
|
+
|
|
181
|
+
await get_tree().physics_frame
|
|
182
|
+
frames += 1
|
|
183
|
+
|
|
184
|
+
_release(held)
|
|
185
|
+
return false
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
func _actions_toward(here: Vector3, target: Vector3) -> Array[String]:
|
|
189
|
+
var out: Array[String] = []
|
|
190
|
+
var dx := target.x - here.x
|
|
191
|
+
if absf(dx) > ARRIVE_EPSILON * 0.5:
|
|
192
|
+
var a := ACT_RIGHT if dx > 0.0 else ACT_LEFT
|
|
193
|
+
if _action_live(a):
|
|
194
|
+
out.append(a)
|
|
195
|
+
|
|
196
|
+
# Second axis: z in 3D, y in 2D. Skipped when the game has no such actions
|
|
197
|
+
# (a side-on platformer, for example).
|
|
198
|
+
if _has_secondary_actions():
|
|
199
|
+
var d2 := (target.z - here.z) if _is_3d else (target.y - here.y)
|
|
200
|
+
if absf(d2) > ARRIVE_EPSILON * 0.5:
|
|
201
|
+
var b := ACT_DOWN if d2 > 0.0 else ACT_UP
|
|
202
|
+
if _action_live(b):
|
|
203
|
+
out.append(b)
|
|
204
|
+
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Press and release directly rather than via press(), which awaits its own hold
|
|
209
|
+
# duration on the wall clock. Holding across physics frames is what makes an
|
|
210
|
+
# autopilot run reproducible.
|
|
211
|
+
func _set_action(action: String, pressed: bool) -> void:
|
|
212
|
+
if action == "" or not InputMap.has_action(action):
|
|
213
|
+
return
|
|
214
|
+
var ev := InputEventAction.new()
|
|
215
|
+
ev.action = action
|
|
216
|
+
ev.pressed = pressed
|
|
217
|
+
Input.parse_input_event(ev)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
func _release(held: Array) -> void:
|
|
221
|
+
for a in held:
|
|
222
|
+
_set_action(a, false)
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# SUMMER - Verification Runner v1: probe base class.
|
|
2
|
+
#
|
|
3
|
+
# An AI-authored probe extends this base. The engine injects the probe as a singleton
|
|
4
|
+
# named "SummerProbe" into a disposable, invisible --summer-verify game instance. The
|
|
5
|
+
# base provides the runtime API (report / save_frame / dump_tree / press / key / finish)
|
|
6
|
+
# and guarantees a results.json is written even if the probe hangs (at-exit backstop +
|
|
7
|
+
# the frames-based quit-after ceiling in main.cpp).
|
|
8
|
+
#
|
|
9
|
+
# The engine sets two members on the instance before _ready() via `set()`:
|
|
10
|
+
# summer_out_dir : String - absolute temp dir to write results/frames into
|
|
11
|
+
# summer_max_seconds : int - soft budget; base auto-finishes at this ceiling
|
|
12
|
+
#
|
|
13
|
+
# Contract of <out_dir>/results.json:
|
|
14
|
+
# { "reports": {..}, "frames": [names], "errors_seen": [strings],
|
|
15
|
+
# "frame_warnings": [strings], "duration_ms": int, "finished": bool }
|
|
16
|
+
#
|
|
17
|
+
# PERCEPTION NOTES (measured against the shipped 0.5.55 binary, reproducible 3/3):
|
|
18
|
+
# * This instance is NOT --headless. Headless has no renderer, so
|
|
19
|
+
# viewport.get_texture().get_image() returns null and NO frame can ever be
|
|
20
|
+
# captured. The verify child is a real windowed instance parked offscreen at
|
|
21
|
+
# (-32000,-32000) precisely so capture works. Never add --headless here.
|
|
22
|
+
# * The viewport does not hold your scene yet during _ready(). Measured, with a
|
|
23
|
+
# red box on screen, sampling distinct colours in the captured image:
|
|
24
|
+
# in _ready() frames_drawn=0 -> 1 colour (clear colour only)
|
|
25
|
+
# after 1 process_frame frames_drawn=0 -> 1 colour (still empty)
|
|
26
|
+
# after 2 process_frames frames_drawn=1 -> 2 colours (the box is there)
|
|
27
|
+
# So `await settle()` before your FIRST save_frame(). save_frame() records a
|
|
28
|
+
# frame_warnings entry rather than lying if you skip it.
|
|
29
|
+
# * RenderingServer.force_draw() is NOT a shortcut. Measured: it fills the image
|
|
30
|
+
# with the clear colour (1 colour, all non-black) but the scene geometry is
|
|
31
|
+
# still missing. A plausible-but-wrong frame is worse than a blank one.
|
|
32
|
+
# * Audio IS assertable even though this instance forces the Dummy driver
|
|
33
|
+
# (main.cpp, summer_verify block). AudioServer still mixes, so measured:
|
|
34
|
+
# silent scene -> AudioServer.get_bus_peak_volume_left_db(0,0) == -200.0
|
|
35
|
+
# playing sound -> ... == -1.938
|
|
36
|
+
# * Performance monitors work here and only here. Measured in this instance:
|
|
37
|
+
# draw_calls=1, fps=46, objects=1, primitives=12. The same probe under
|
|
38
|
+
# --headless reports draw_calls=0, fps=1, objects=0 -- all meaningless.
|
|
39
|
+
extends Node
|
|
40
|
+
|
|
41
|
+
var summer_out_dir: String = ""
|
|
42
|
+
var summer_max_seconds: int = 20
|
|
43
|
+
|
|
44
|
+
var _reports: Dictionary = {}
|
|
45
|
+
var _frames: Array = []
|
|
46
|
+
var _frame_warnings: Array = []
|
|
47
|
+
var _start_ms: int = 0
|
|
48
|
+
var _finished: bool = false
|
|
49
|
+
|
|
50
|
+
func _enter_tree() -> void:
|
|
51
|
+
_start_ms = Time.get_ticks_msec()
|
|
52
|
+
|
|
53
|
+
func _ready() -> void:
|
|
54
|
+
# Hard soft-ceiling: if the probe body never calls finish(), auto-finish just before
|
|
55
|
+
# the main.cpp frames ceiling would kill us, so we still flush a partial results.json.
|
|
56
|
+
var t := Timer.new()
|
|
57
|
+
t.one_shot = true
|
|
58
|
+
t.wait_time = float(summer_max_seconds)
|
|
59
|
+
t.timeout.connect(_on_deadline)
|
|
60
|
+
add_child(t)
|
|
61
|
+
t.start()
|
|
62
|
+
|
|
63
|
+
func _on_deadline() -> void:
|
|
64
|
+
if not _finished:
|
|
65
|
+
report("_timeout", true)
|
|
66
|
+
finish()
|
|
67
|
+
|
|
68
|
+
# --- Runtime API for the AI-authored probe --------------------------------------------
|
|
69
|
+
|
|
70
|
+
func report(key: String, value) -> void:
|
|
71
|
+
_reports[key] = value
|
|
72
|
+
|
|
73
|
+
func settle(frames: int = 1) -> void:
|
|
74
|
+
# Await until the renderer has actually PRESENTED `frames` more frames. Call this
|
|
75
|
+
# before your first save_frame(), and after any change you want to see on screen.
|
|
76
|
+
#
|
|
77
|
+
# await settle() # first capture is now trustworthy
|
|
78
|
+
# save_frame("start")
|
|
79
|
+
#
|
|
80
|
+
# Counting presented frames (Engine.get_frames_drawn()) rather than awaiting
|
|
81
|
+
# process_frame is deliberate: a process frame can complete without the renderer
|
|
82
|
+
# having presented anything, which is exactly the blank-capture bug. See the
|
|
83
|
+
# PERCEPTION NOTES at the top for the measurements behind this.
|
|
84
|
+
#
|
|
85
|
+
# But that counter only advances when there IS a renderer. Under --headless it is
|
|
86
|
+
# 0 forever -- measured: 438 process frames awaited over 3005ms, frames_drawn
|
|
87
|
+
# still 0 -- so waiting on it there would spin until the probe budget killed the
|
|
88
|
+
# run. This base is normally injected into the windowed verify child, but the
|
|
89
|
+
# headless-scripting path runs it under `--headless -s`, where that would hang.
|
|
90
|
+
# So: fall back to process frames when there is no renderer, and bound the wait
|
|
91
|
+
# either way so settle() can never be the reason a probe fails to finish.
|
|
92
|
+
if not has_renderer():
|
|
93
|
+
# +1 because content lands a frame later than the naive count suggests.
|
|
94
|
+
for _i in range(maxi(1, frames) + 1):
|
|
95
|
+
await get_tree().process_frame
|
|
96
|
+
return
|
|
97
|
+
var target := Engine.get_frames_drawn() + maxi(1, frames)
|
|
98
|
+
var guard := 0
|
|
99
|
+
while Engine.get_frames_drawn() < target and guard < 600:
|
|
100
|
+
guard += 1
|
|
101
|
+
await get_tree().process_frame
|
|
102
|
+
if guard >= 600:
|
|
103
|
+
report("_settle_timeout", true)
|
|
104
|
+
|
|
105
|
+
func has_renderer() -> bool:
|
|
106
|
+
# True when this instance can actually draw. Use this, NOT OS.has_feature("headless"),
|
|
107
|
+
# which is measurably FALSE under --headless and will mislead you. The rendering
|
|
108
|
+
# method setting lies too -- it still reads "forward_plus" with no renderer behind it.
|
|
109
|
+
# DisplayServer's own name is the honest signal.
|
|
110
|
+
return DisplayServer.get_name() != "headless"
|
|
111
|
+
|
|
112
|
+
func settle_physics(frames: int = 1) -> void:
|
|
113
|
+
# Await N physics frames. Call this after building a scene and BEFORE asserting
|
|
114
|
+
# anything physical. A body added this frame has no space yet, so the natural
|
|
115
|
+
# "build the scene, immediately assert physics" probe throws on frame 0:
|
|
116
|
+
# Parameter "body->get_space()" is null
|
|
117
|
+
# at: godot_physics_server_2d.cpp:997
|
|
118
|
+
# One physics frame after add_child() is the minimum; raycasts and contacts
|
|
119
|
+
# generally want two.
|
|
120
|
+
for _i in range(maxi(1, frames)):
|
|
121
|
+
await get_tree().physics_frame
|
|
122
|
+
|
|
123
|
+
func settle_navigation(max_frames: int = 60) -> bool:
|
|
124
|
+
# Await until the navigation map is actually queryable, and report whether it got
|
|
125
|
+
# there. Returns true on success, false if it timed out.
|
|
126
|
+
#
|
|
127
|
+
# This exists because the failure is SILENT. NavigationServer3D.map_get_path()
|
|
128
|
+
# returns an EMPTY array with no error until the map has synchronized TWICE --
|
|
129
|
+
# iteration_id == 1 is not enough. Measured here, awaiting physics_frame:
|
|
130
|
+
# frame 0-1 iter=0 path_pts=0
|
|
131
|
+
# frame 2-3 iter=1 path_pts=0 <- still empty, still no error
|
|
132
|
+
# frame 4+ iter=2 path_pts=2 <- queryable
|
|
133
|
+
# So the obvious probe returns [] and reads as "navigation is broken" when
|
|
134
|
+
# nothing is wrong. (map_get_closest_point() does error; map_get_path() does not.)
|
|
135
|
+
# process_frame does NOT advance the iteration id as quickly -- await physics.
|
|
136
|
+
var map := get_viewport().world_3d.navigation_map
|
|
137
|
+
for _i in range(maxi(1, max_frames)):
|
|
138
|
+
if NavigationServer3D.map_get_iteration_id(map) >= 2:
|
|
139
|
+
return true
|
|
140
|
+
await get_tree().physics_frame
|
|
141
|
+
report("_navigation_settle_timeout", true)
|
|
142
|
+
return false
|
|
143
|
+
|
|
144
|
+
func save_frame(name: String) -> String:
|
|
145
|
+
# Capture the current viewport framebuffer to <out_dir>/<name> (jpg). Returns the
|
|
146
|
+
# absolute path written, or "" on failure.
|
|
147
|
+
#
|
|
148
|
+
# If no frame has been presented yet, the file still lands (so the web side can look
|
|
149
|
+
# at it) but a frame_warnings entry is recorded, because the image will be the flat
|
|
150
|
+
# clear colour rather than your scene. Fix that by `await settle()` first -- do not
|
|
151
|
+
# ignore the warning, the capture is not evidence of anything.
|
|
152
|
+
if not has_renderer():
|
|
153
|
+
var hw := "save_frame(\"%s\") called under --headless: there is NO renderer, get_image() returns null and no frame can ever be captured. Use the windowed verify path (--summer-verify), not --headless." % name
|
|
154
|
+
if not _frame_warnings.has(hw):
|
|
155
|
+
_frame_warnings.append(hw)
|
|
156
|
+
elif Engine.get_frames_drawn() < 1:
|
|
157
|
+
var w := "save_frame(\"%s\") ran before any frame was presented - the image is the clear colour, not the scene. `await settle()` first." % name
|
|
158
|
+
if not _frame_warnings.has(w):
|
|
159
|
+
_frame_warnings.append(w)
|
|
160
|
+
var vp := get_viewport()
|
|
161
|
+
if vp == null:
|
|
162
|
+
return ""
|
|
163
|
+
var tex := vp.get_texture()
|
|
164
|
+
if tex == null:
|
|
165
|
+
return ""
|
|
166
|
+
var img := tex.get_image()
|
|
167
|
+
if img == null:
|
|
168
|
+
return ""
|
|
169
|
+
var fname := name if name.ends_with(".jpg") else name + ".jpg"
|
|
170
|
+
var path := summer_out_dir.path_join(fname)
|
|
171
|
+
var err := img.save_jpg(path, 0.75)
|
|
172
|
+
if err == OK:
|
|
173
|
+
if not _frames.has(fname):
|
|
174
|
+
_frames.append(fname)
|
|
175
|
+
return path
|
|
176
|
+
return ""
|
|
177
|
+
|
|
178
|
+
func dump_tree(max_depth: int = 6) -> void:
|
|
179
|
+
# Summarize the live scene tree (names + classes) into the report under "tree".
|
|
180
|
+
var root := get_tree().root
|
|
181
|
+
var lines: Array = []
|
|
182
|
+
_dump_node(root, 0, max_depth, lines)
|
|
183
|
+
report("tree", lines)
|
|
184
|
+
|
|
185
|
+
func _dump_node(n: Node, depth: int, max_depth: int, lines: Array) -> void:
|
|
186
|
+
if depth > max_depth:
|
|
187
|
+
return
|
|
188
|
+
var indent := " ".repeat(depth)
|
|
189
|
+
lines.append("%s%s (%s)" % [indent, n.name, n.get_class()])
|
|
190
|
+
for c in n.get_children():
|
|
191
|
+
_dump_node(c, depth + 1, max_depth, lines)
|
|
192
|
+
|
|
193
|
+
func press(action: String, hold_ms: int = 100) -> void:
|
|
194
|
+
# Inject an input action press+release from INSIDE the process (no debugger).
|
|
195
|
+
var ev := InputEventAction.new()
|
|
196
|
+
ev.action = action
|
|
197
|
+
ev.pressed = true
|
|
198
|
+
Input.parse_input_event(ev)
|
|
199
|
+
await get_tree().create_timer(hold_ms / 1000.0).timeout
|
|
200
|
+
var up := InputEventAction.new()
|
|
201
|
+
up.action = action
|
|
202
|
+
up.pressed = false
|
|
203
|
+
Input.parse_input_event(up)
|
|
204
|
+
|
|
205
|
+
func key(keycode: int, hold_ms: int = 100) -> void:
|
|
206
|
+
# Inject a physical key press+release by keycode (e.g. KEY_SPACE).
|
|
207
|
+
var ev := InputEventKey.new()
|
|
208
|
+
ev.keycode = keycode
|
|
209
|
+
ev.physical_keycode = keycode
|
|
210
|
+
ev.pressed = true
|
|
211
|
+
Input.parse_input_event(ev)
|
|
212
|
+
await get_tree().create_timer(hold_ms / 1000.0).timeout
|
|
213
|
+
var up := InputEventKey.new()
|
|
214
|
+
up.keycode = keycode
|
|
215
|
+
up.physical_keycode = keycode
|
|
216
|
+
up.pressed = false
|
|
217
|
+
Input.parse_input_event(up)
|
|
218
|
+
|
|
219
|
+
func finish() -> void:
|
|
220
|
+
# Flush results.json and terminate the disposable instance.
|
|
221
|
+
if _finished:
|
|
222
|
+
return
|
|
223
|
+
_finished = true
|
|
224
|
+
_write_results(true)
|
|
225
|
+
get_tree().quit()
|
|
226
|
+
|
|
227
|
+
# --- Results writing ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
func _collect_errors() -> Array:
|
|
230
|
+
# The C++ SummerVerifyLogger appends error-level engine/script errors to errors.log.
|
|
231
|
+
var errors: Array = []
|
|
232
|
+
var p := summer_out_dir.path_join("errors.log")
|
|
233
|
+
if FileAccess.file_exists(p):
|
|
234
|
+
var f := FileAccess.open(p, FileAccess.READ)
|
|
235
|
+
if f != null:
|
|
236
|
+
while not f.eof_reached():
|
|
237
|
+
var line := f.get_line()
|
|
238
|
+
if line.strip_edges() != "":
|
|
239
|
+
errors.append(line)
|
|
240
|
+
return errors
|
|
241
|
+
|
|
242
|
+
func _write_results(finished: bool) -> void:
|
|
243
|
+
var out := {
|
|
244
|
+
"reports": _reports,
|
|
245
|
+
"frames": _frames,
|
|
246
|
+
"frame_warnings": _frame_warnings,
|
|
247
|
+
"errors_seen": _collect_errors(),
|
|
248
|
+
"duration_ms": Time.get_ticks_msec() - _start_ms,
|
|
249
|
+
"finished": finished,
|
|
250
|
+
}
|
|
251
|
+
var path := summer_out_dir.path_join("results.json")
|
|
252
|
+
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
253
|
+
if f != null:
|
|
254
|
+
f.store_string(JSON.stringify(out, " "))
|
|
255
|
+
f.flush()
|
|
256
|
+
f.close()
|
|
257
|
+
|
|
258
|
+
func _exit_tree() -> void:
|
|
259
|
+
# At-exit backstop: if finish() was never called (quit-after ceiling in main.cpp
|
|
260
|
+
# killed us, or the probe crashed), still leave a partial results.json behind.
|
|
261
|
+
if not _finished:
|
|
262
|
+
_write_results(false)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run the autopilot against this project and print the results.
|
|
3
|
+
#
|
|
4
|
+
# bash tests/autopilot/run.sh # runs autopilot.gd
|
|
5
|
+
# bash tests/autopilot/run.sh my_probe.gd # runs a different probe
|
|
6
|
+
#
|
|
7
|
+
# No editor required. This spawns the engine's offscreen verify instance, which
|
|
8
|
+
# has a REAL renderer — unlike --headless, which produces no pixels at all.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
12
|
+
PROBE="${1:-$PROJECT_DIR/tests/autopilot/autopilot.gd}"
|
|
13
|
+
OUT="$PROJECT_DIR/tests/autopilot/out"
|
|
14
|
+
MAX_SECONDS="${MAX_SECONDS:-40}"
|
|
15
|
+
|
|
16
|
+
# Find the engine. Override with SUMMER_BIN=/path/to/engine if it lives elsewhere.
|
|
17
|
+
# There is no `godot` binary on a Summer install — do not substitute one.
|
|
18
|
+
if [[ -n "${SUMMER_BIN:-}" ]]; then
|
|
19
|
+
ENGINE="$SUMMER_BIN"
|
|
20
|
+
elif [[ -x "/Applications/Summer.app/Contents/MacOS/Summer" ]]; then
|
|
21
|
+
ENGINE="/Applications/Summer.app/Contents/MacOS/Summer"
|
|
22
|
+
elif [[ -x "${LOCALAPPDATA:-}/Summer/current/Summer.exe" ]]; then
|
|
23
|
+
ENGINE="${LOCALAPPDATA}/Summer/current/Summer.exe"
|
|
24
|
+
else
|
|
25
|
+
echo "Could not find the Summer engine binary." >&2
|
|
26
|
+
echo "Set SUMMER_BIN, or read engineBinaryPath from summer_get_project_context." >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
rm -rf "$OUT"
|
|
31
|
+
mkdir -p "$OUT"
|
|
32
|
+
|
|
33
|
+
# --disable-crash-handler: Summer's handler popen()s atos from inside a signal
|
|
34
|
+
# handler, which turns a clean failure into a hang. Never arm it on an
|
|
35
|
+
# agent-driven run. Note there is deliberately no --headless here: headless has
|
|
36
|
+
# no renderer, so save_frame() would produce nothing.
|
|
37
|
+
"$ENGINE" \
|
|
38
|
+
--disable-crash-handler \
|
|
39
|
+
--path "$PROJECT_DIR" \
|
|
40
|
+
--summer-verify "$PROBE" \
|
|
41
|
+
--summer-verify-out "$OUT" \
|
|
42
|
+
--summer-verify-max "$MAX_SECONDS" \
|
|
43
|
+
>"$OUT/engine.log" 2>&1 || true
|
|
44
|
+
|
|
45
|
+
if [[ ! -f "$OUT/results.json" ]]; then
|
|
46
|
+
echo "No results.json — the probe never started. Engine output:" >&2
|
|
47
|
+
tail -30 "$OUT/engine.log" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
cat "$OUT/results.json"
|
|
52
|
+
echo
|
|
53
|
+
echo "Frames and full log: $OUT"
|
|
54
|
+
|
|
55
|
+
# finished:false means the probe hit its time ceiling before calling finish().
|
|
56
|
+
# That is a failure, not a pass — exit non-zero so CI notices.
|
|
57
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
58
|
+
python3 - "$OUT/results.json" <<'PY'
|
|
59
|
+
import json, sys
|
|
60
|
+
d = json.load(open(sys.argv[1]))
|
|
61
|
+
reports = d.get("reports", {})
|
|
62
|
+
if not d.get("finished", False):
|
|
63
|
+
print("FAILED: probe did not finish (hit --summer-verify-max)", file=sys.stderr)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
if d.get("errors_seen"):
|
|
66
|
+
print(f"FAILED: {len(d['errors_seen'])} engine error(s) during the run", file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
if reports.get("error"):
|
|
69
|
+
print(f"FAILED: {reports['error']}", file=sys.stderr)
|
|
70
|
+
print("Edit the CONFIG block at the top of tests/autopilot/autopilot.gd.", file=sys.stderr)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
# Any waypoint the autopilot could not reach is a failure, whatever else passed.
|
|
73
|
+
missed = [k for k, v in reports.items() if k.endswith("_reached") and v is False]
|
|
74
|
+
if missed:
|
|
75
|
+
print(f"FAILED: waypoints not reached: {', '.join(sorted(missed))}", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
PY
|
|
78
|
+
fi
|
package/commands/gameskill.md
CHANGED
|
@@ -6,9 +6,18 @@ This is the meta-skill: the loop that turns ad-hoc fixes into durable, reusable
|
|
|
6
6
|
|
|
7
7
|
Three repos, one ecosystem:
|
|
8
8
|
|
|
9
|
-
1. **`~/development/SummerEngine`** — the
|
|
9
|
+
1. **`~/development/SummerEngine`** — the Summer Engine desktop source and the
|
|
10
|
+
canonical CLI. The C++ engine is maintained against the upstream Godot
|
|
11
|
+
Engine codebase; the Summer-owned module lives at `modules/1summer_engine/`.
|
|
12
|
+
The Summer CLI lives at `tools/summer-cli/` and is normally invoked with
|
|
13
|
+
`npx -y summer-engine@latest`. It installs, signs in, creates, and runs Summer
|
|
14
|
+
projects and can install the current Summer skill bundle for supported
|
|
15
|
+
agents.
|
|
10
16
|
2. **`~/development/PublicSummerEngine`** — the web app at summerengine.com. Has its own AI skill stores at `src/lib/ai/skills/bundled/` and `public/knowledge/summer/skills/` for the in-browser chat agent. ALSO has its own MCP-style consumers of the engine API (`src/lib/bridge/direct-executor.ts`, `src/lib/ai/tools/run-and-verify.ts`) which call the same engine endpoints the CLI does.
|
|
11
|
-
3. **The active game project** — usually
|
|
17
|
+
3. **The active Summer game project** — usually
|
|
18
|
+
`~/development/<game-name>` or whatever game we were just working on. The
|
|
19
|
+
live compatibility line comes from Summer Engine, not this prompt. Real
|
|
20
|
+
working code in this repo is the gold standard for examples.
|
|
12
21
|
|
|
13
22
|
The skill system is the value flywheel: every game we ship teaches lessons → the lessons become skills → the next user (or AI) starts smarter. Your job here is to close that loop for what we just did.
|
|
14
23
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are the Summer Engine command router. The user invoked `/summer` because they want game-development help inside Summer Engine, not generic software advice.
|
|
2
|
+
|
|
3
|
+
Arguments:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ARGUMENTS
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
First, route the request to the closest Summer skill. Use `summer:using-summer` if you need base context before choosing.
|
|
10
|
+
|
|
11
|
+
If the arguments start with `debug`, or the user is reporting a crash, error, broken behavior, cloud/Codex failure, or anything they want to send to Summer support, use `summer:debugging/debug`. In that support-report mode, call `summer_create_debug_report` when available and tell the user where the Markdown report was written. Continue fixing the issue only if the user also asked for a fix.
|
|
12
|
+
|
|
13
|
+
For all other requests, follow the `/summer` persona routing table from the Summer skill family and start the appropriate game-dev workflow.
|
package/dist/bin/summer.js
CHANGED
|
@@ -17,6 +17,11 @@ import { setupCommand } from "../commands/setup.js";
|
|
|
17
17
|
import { doctorCommand } from "../commands/doctor.js";
|
|
18
18
|
import { planCommand } from "../commands/plan.js";
|
|
19
19
|
import { cloudCommand } from "../commands/cloud.js";
|
|
20
|
+
import { debugCommand } from "../commands/debug.js";
|
|
21
|
+
import { configCommand } from "../commands/config.js";
|
|
22
|
+
import { publishCommand } from "../commands/publish.js";
|
|
23
|
+
import { releasesCommand } from "../commands/releases.js";
|
|
24
|
+
import { logsCommand } from "../commands/logs.js";
|
|
20
25
|
import { getBanner } from "../lib/banner.js";
|
|
21
26
|
import { c, sym } from "../lib/format.js";
|
|
22
27
|
const require = createRequire(import.meta.url);
|
|
@@ -42,9 +47,14 @@ program.addCommand(skillsCommand);
|
|
|
42
47
|
program.addCommand(mcpCommand);
|
|
43
48
|
program.addCommand(setupCommand);
|
|
44
49
|
program.addCommand(doctorCommand);
|
|
50
|
+
program.addCommand(debugCommand);
|
|
45
51
|
program.addCommand(planCommand);
|
|
46
52
|
program.addCommand(orchestratorCommand);
|
|
47
53
|
program.addCommand(cloudCommand);
|
|
54
|
+
program.addCommand(configCommand);
|
|
55
|
+
program.addCommand(publishCommand);
|
|
56
|
+
program.addCommand(releasesCommand);
|
|
57
|
+
program.addCommand(logsCommand);
|
|
48
58
|
program.parseAsync().catch((err) => {
|
|
49
59
|
console.error(err instanceof Error ? err.message : String(err));
|
|
50
60
|
process.exit(1);
|
|
@@ -63,10 +73,12 @@ function printIntro(version) {
|
|
|
63
73
|
console.log(` ${c.dim("$")} npx -y summer-engine@latest setup claude-code --yes`);
|
|
64
74
|
console.log(` ${c.dim("$")} npx -y summer-engine@latest create 3d-basic my-game`);
|
|
65
75
|
console.log(` ${c.dim("$")} npx -y summer-engine@latest plan make a small arena shooter`);
|
|
76
|
+
console.log(` ${c.dim("$")} npx -y summer-engine@latest debug "player crashes on pickup"`);
|
|
66
77
|
console.log(` ${c.dim("$")} npx -y summer-engine@latest memory`);
|
|
67
78
|
console.log(` ${c.dim("$")} npx -y summer-engine@latest doctor`);
|
|
68
79
|
console.log("");
|
|
69
80
|
console.log(` ${c.bold("Slash commands")} ${c.dim("(in your AI agent)")}`);
|
|
81
|
+
console.log(` ${c.brand("/summer debug")} ${c.dim("Create a support-ready debug report")}`);
|
|
70
82
|
console.log(` ${c.brand("/debug")} ${c.dim("Triage and fix a bug end-to-end")}`);
|
|
71
83
|
console.log(` ${c.brand("/play")} ${c.dim("Run the game and report state")}`);
|
|
72
84
|
console.log("");
|