Part of the
scene-scripting skill. The skill links here when your agent needs the detail.source of one call: summer_run_script(scenePath="res://<the scene>.tscn", source=<the recipe>). Name the scene every time; it is opened first when it is not the edited one, and the person’s tab comes back after the run. The core helpers (add_node, find, add_mesh, make_material, save_scene …) are in the scene-scripting skill itself.
These use the game-completeness tier (ctx-api.md, next to this file); on an engine that predates it, use the fallback table there.
2D platformer level — tilemap paint, character body, camera limits, parallax (game-completeness tier)
One script lays down the level: paint the ground, drop a character body with collision, bound the camera to the level, stack parallax behind it. Tile coordinates are cells; camera limits are pixels (cells × tile size — 32 here).tiles_painted with the cell areas you asked for (256 + 6 + 3 here) — a short count means cells were skipped. Then summer_snapshot_diff: added must list Ground, Player with its CollisionShape2D child, the camera under Player, and the Background group with one Parallax2D per layer. Screenshot target:"scene" nodePath:"Player" to see the body standing on the floor row, then the whole scene to see the platform is reachable. Movement needs the controller script and the input actions — two recipes below. Want a working baseline instead of a blank scene? The 2d-platformer template already has tilemap + CharacterBody2D + parallax wired.
Animated sprite (game-completeness tier)
add_animated_sprite builds the SpriteFrames resource from per-frame texture paths — one entry per animation, fps and loop per clip — and optionally autoplays one. Cut a sheet into frames first (assets.slice_sheet, or the sprite-sheet skill when generating them).
anim.play("run") from the controller’s _physics_process), not scene setup. Verify by playing: summer_play, move, summer_screenshot target:"game" — an edit-time preview shows frame 0 only.
HUD — canvas layer, margin, vbox, label/progress/button, anchors, theme overrides (game-completeness tier)
props.anchor runs set_anchors_and_offsets_preset — the right way to pin a Control; hand-set anchor_*/offset_* values drift the moment the window resizes. The preset is applied before the other props, so an explicit size/position in the same dict wins. add_ui("panel", …) is a PanelContainer (it sizes to its child — a backdrop for a vbox/hbox); a plain Panel is add_node("Panel", …). set_theme_overrides returns how many overrides it applied — compare with what you passed (an int on a key that is not font_size becomes a constant override, as with the margins above). Unknown props keys land in prop_warnings as always (use real property names: custom_minimum_size, not min_size or a guess). HUD Controls belong under a CanvasLayer — a Control parented under a Node2D scrolls with the camera and gets a prop_warning. For the layout and theme design — which container for which job, theme vs inline styling — use ui-basics; this is its one-script form.
Wire a button with connect_signal() (game-completeness tier)
ctx.connect_signal makes a persisted connection (CONNECT_PERSIST) — it survives in the .tscn, exactly what the editor’s ConnectionsDock writes. The target needs a method to call, so attach the script first; a script attached in the same run has not compiled yet, and connect_signal accepts it because the source declares func _on_pause_pressed( (the method gate):
button.pressed.connect(...) inside the run script: that connection lives only in the editor process and is not saved. And the helper is connect_signal, not connect — ctx.connect(...) is Object.connect on the ctx object itself and fails with a wrong-signature error. true from ctx.connect_signal says the connection was made and flagged persistent (an already-connected pair also returns true, with already_connected in the report); both ends must be owned by the edited root or the .tscn cannot carry it. The proof is summer_play, click, summer_screenshot target:"game".
attach_script — behavior with the parse-error loop (game-completeness tier)
attach_script validates the source before writing anything: a parse error comes back as a report entry (with the line) and the return is null — no half-written file, no broken node. Empty path → res://scripts/<SceneName>/<NodeName>.gd.
path is overwritten and its previous sha recorded in the report — the pre-run checkpoint is the rollback for that, not the undo action. Idioms (typed declarations, signals, _ready vs _process): gdscript-patterns. Later edits to a script already on disk: summer_write_file + summer_get_script_errors.
make_prefab — turn a built subtree into a reusable .tscn (game-completeness tier)
Build the thing once in-scene, then pack it. Withreplace_with_instance (the default) the inline subtree is swapped for an instance of the new file — same name, transform, parent — and the instance is returned.
first.position above is still where the torch stood; rotation and scale stay in the prefab. make_prefab refuses the scene root (pack a child subtree, never ctx.get_scene_root()) and a path that is open in an editor tab. The receipt: res://prefabs/torch.tscn in the result’s files_written, Torch now reporting a scene_file_path, and the copies in the diff’s added. From here on, edit the prefab file, not the instances.
Input actions, autoload, main scene (game-completeness tier)
Project-level wiring in one run — the platformer controller above already reads these actions:false, never a half-written action (every event is built before anything is written). A new action gets the engine’s default deadzone, 0.2; re-running on an existing action keeps its deadzone and appends only the events it does not already match, so the recipe is safe to re-run. Keycode strings are find_keycode names ("Space", "Left", "Ctrl+S" — modifiers become the event’s modifier flags). Write the autoload’s script before registering it (summer_write_file, or attach_script on a node you then make_prefab) — add_autoload applies the editor dock’s gates (valid identifier, no class-name collision, an existing res:// Script or PackedScene). These helpers write the res:// path form ("*res://…", "res://…"); the editor UI writes uid:// forms and both resolve — leave them be. They write project.godot, not the scene: the result’s undo_action note lists them under project_settings_changed, outside the scene undo — checkpoint territory. Proof: summer_play → summer_get_runtime_tree shows /root/GameState; the keys move the body in a target:"game" capture.
Verifying 2D and UI work
Same loop, different framing:- Diff first.
addedmust carry the bodies’CollisionShape2Dchildren, theCanvasLayerwith its Controls, oneParallax2Dper layer. Anchors and theme overrides show up aschangedproperties on the Controls. - 2D scene captures.
summer_screenshot target:"scene"on a 2D scene synthesizes aCamera2Dand auto-fits theCanvasItembounds — the 3D presets do not apply, andframing:"camera"errors on a scene with no 3D content.nodePath:"Player"frames one node (its 2D rect, children included);size:[1280, 720]sets the resolution that anchors resolve against. - UI and anything input-driven is only proven in the running game:
summer_play, thensummer_screenshot target:"game". An edit-time preview of a HUD approximates a viewport the editor does not have. - Read the confession fields in every capture (synthetic camera, no camera, project mismatch). A capture you did not receive is not evidence —
verifying-sceneshas the full discipline.
Pitfalls
Source:
library/skills/scene-scripting/references/recipes-2d-ui.md, rendered from summer-engine@4.3.0 on npm.
