Files
aego/app/edit/shots.go
T
sanspie a6822156cd
ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
Finish Stage 12 editor fixes and refreshed platformer
2026-11-05 17:36:42 +03:00

361 lines
11 KiB
Go

package edit
import (
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"strconv"
"strings"
"aego/app/agentui/backend"
"aego/app/edit/agentbridge"
"aego/core/ids"
"aego/editor/core/agentui"
"aego/editor/core/animedit"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/presence"
"aego/editor/core/vectors"
"aego/editor/core/workspace"
agentpanel "aego/editor/panels/agent"
animpanel "aego/editor/panels/animation"
"aego/editor/panels/animator"
"aego/editor/panels/gameview"
"aego/editor/shell"
"aego/ui/theme"
)
// EnvShots makes the editor write the documentation screenshots to this directory and exit.
const EnvShots = "AEGO_SHOTS"
// Screenshot mode uses a synthetic context meter and this sessionless backend, avoiding dependence on an installed or signed-in agent.
type screenshotAgent struct{}
func (screenshotAgent) Name() string { return "documentation" }
func (screenshotAgent) Available(backend.Config) (string, error) {
return "read-only documentation fixture", nil
}
func (screenshotAgent) Start(backend.Config) (backend.Conversation, error) {
return nil, fmt.Errorf("documentation screenshots cannot start an agent session")
}
func screenshotConfig(cfg Config, dir string) Config {
if dir != "" && cfg.AgentBackend == nil {
cfg.AgentBackend = screenshotAgent{}
}
return cfg
}
type shot struct {
name string
frames int
inset int
bottom int
setup func(s *Session) error
}
func (s *Session) shots(dir string) error {
defer s.Close()
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
if !s.Step() {
return fmt.Errorf("the editor quit")
}
ctx := s.App.Context()
scene := func() { ctx.Open(context.At("scenes/main.aescene", 1, 1)) }
frameAll := func() error {
if err := ctx.Commands.Run(command.EditSelectAll); err != nil {
return err
}
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return nil
}
list := []shot{
{name: "editor", frames: 240, setup: func(s *Session) error {
scene()
s.App.Workspace().Show(workspace.Inspector)
if err := frameAll(); err != nil {
return err
}
return selectNamed(ctx, "Player")
}},
{name: "agent-follow", frames: 60, setup: func(s *Session) error {
f := s.App.Follow()
f.SetOn(true)
now := s.Engine.Platform().Now()
f.Step("scene_set", `{"file":"scenes/main.aescene","node":"Player","path":"Motion2D.maxSpeed","value":"9"}`, now)
sc := ctx.Scene()
player := sc.ID(sc.Find(sc.Root(), "Player"))
s.App.Presence().Light(presence.Flash{Spot: presence.Spot{Doc: "scenes/main.aescene", Node: player, Type: "gameplay.Motion2D", Field: "maxSpeed"}, At: now})
ctx.Select()
return nil
}},
{name: "hitbox-gizmo", frames: 60, setup: func(s *Session) error {
s.App.Follow().SetOn(false)
s.App.Presence().Clear()
s.App.AgentSelection().Clear()
sc := ctx.Scene()
var span []ids.NodeID
for _, n := range []string{"EnemyLeft", "PatrolA1", "PatrolB1"} {
span = append(span, sc.ID(sc.Find(sc.Root(), n)))
}
ctx.Select(span...)
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "EnemyLeft")
}},
{name: "camera-follow", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
ctx.Select(sc.ID(sc.Find(sc.Root(), "Camera")), sc.ID(sc.Find(sc.Root(), "Player")))
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "Camera")
}},
{name: "camera-zone", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
ctx.Select(sc.ID(sc.Find(sc.Root(), "ArenaZone")), sc.ID(sc.Find(sc.Root(), "PatrolA2")), sc.ID(sc.Find(sc.Root(), "PatrolB2")), sc.ID(sc.Find(sc.Root(), "GroundRight")))
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "ArenaZone")
}},
{name: "clip-inspector", frames: 60, setup: func(s *Session) error {
id, ok := s.Pipeline.Lookup("assets/audio/music.ogg")
if !ok {
return fmt.Errorf("no assets/audio/music.ogg")
}
s.App.Data().Inspect(id)
return nil
}},
{name: "add-required", frames: 30, setup: func(s *Session) error {
if err := selectNamed(ctx, "PatrolA1"); err != nil {
return err
}
s.App.Inspector().OpenAdd("follow")
return nil
}},
{name: "add-palette", frames: 30, setup: func(s *Session) error {
s.App.AddPalette().Close()
if err := selectNamed(ctx, "PatrolA1"); err != nil {
return err
}
s.App.AddPalette().AddComponent(ctx.Selected(), nil)
s.App.AddPalette().Palette().SetQuery("col")
return nil
}},
{name: "tags", frames: 60, setup: func(s *Session) error {
s.App.AddPalette().Close()
ctx.UI.CloseAllPopups()
return selectNamed(ctx, "Player")
}},
{name: "prefab", frames: 60, setup: func(s *Session) error {
return selectNamed(ctx, "EnemyLeft")
}},
{name: "settings-input", frames: 60, setup: func(s *Session) error {
ctx.Open(context.SettingsAt("input"))
return nil
}},
{name: "settings-physics", frames: 60, setup: func(s *Session) error {
ctx.Open(context.SettingsAt("physics"))
return nil
}},
{name: "animator", frames: 60, setup: func(s *Session) error {
scene()
s.App.Workspace().Switch(workspace.Design)
s.App.Workspace().Show(animator.ID)
return selectNamed(ctx, "Player")
}},
{name: "attack-preview", frames: 30, setup: func(s *Session) error {
scene()
if err := selectNamed(ctx, "Player"); err != nil {
return err
}
s.App.Workspace().Show(animpanel.ID)
for range 5 {
s.Step()
}
if !s.App.AnimationPanel().PickClip("assets/anim/player_hit.aeanim") {
return fmt.Errorf("the Player has no player_hit clip")
}
for range 3 {
s.Step()
}
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
svc := ctx.Anim.For(ctx.Doc())
svc.SetMode(animedit.Preview)
svc.SetTime(0.17)
return nil
}},
{name: "slicer", frames: 60, setup: func(s *Session) error {
svc := ctx.Anim.For(ctx.Doc())
svc.SetMode(animedit.Normal)
for range 3 {
s.Step()
}
if m := svc.Mode(); m != animedit.Normal {
return fmt.Errorf("the clip is still in mode %d", m)
}
s.App.Workspace().Show(workspace.Inspector)
id, ok := s.Pipeline.Lookup("assets/sprites/slash.png")
if !ok {
return fmt.Errorf("no assets/sprites/slash.png")
}
s.App.Data().Inspect(id)
return nil
}},
{name: "script", frames: 90, setup: func(s *Session) error {
ctx.Open(context.At("scripts/player/player.aes", 1, 1))
return nil
}},
{name: "workspace-programming", frames: 60, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Programming)
return nil
}},
{name: "play", frames: 150, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Design)
scene()
return ctx.Commands.Run(command.PlayStart)
}},
{name: "motion-play", frames: 30, setup: func(s *Session) error {
s.App.Workspace().Show(workspace.Inspector)
s.App.Inspector().ScrollTo("gameplay.Motion2D")
return selectNamed(ctx, "Player")
}},
{name: "mixer", frames: 120, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Audio)
s.App.Workspace().Show(workspace.Mixer)
return nil
}},
{name: "physics-debug", frames: 30, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Design)
ctx.Overlay.Set("physics.debug.contacts", true)
return ctx.Commands.Run(gameview.CommandOverlays)
}},
{name: "demo", frames: 60, inset: 2, bottom: 28, setup: func(s *Session) error {
for _, f := range []string{gameview.CommandOverlays, vectors.CommandToggle} {
ctx.Overlay.Set(f, false)
}
return ctx.Commands.Run(shell.CommandFullscreenPlay)
}},
{name: "prefab-mode", frames: 120, setup: func(s *Session) error {
if err := ctx.Commands.Run(command.PlayStop); err != nil {
return err
}
id, ok := s.Pipeline.Lookup("prefabs/enemy.aeprefab")
if !ok {
return fmt.Errorf("no prefabs/enemy.aeprefab")
}
return s.App.Prefabs().Open(id)
}},
{name: "agent-context", frames: 30, setup: func(s *Session) error {
p := s.App.Agent()
if p == nil {
return fmt.Errorf("no Agent panel")
}
s.App.Workspace().Show(agentpanel.ID)
p.ShowMeter(shotMeter())
p.OpenContext(agentui.CatScene)
return nil
}},
}
if s.App.ThemeMode() == theme.ModeLight {
for i := range list {
list[i].name += "-light"
}
}
for _, sh := range list {
if err := sh.setup(s); err != nil {
return fmt.Errorf("%s: %w", sh.name, err)
}
if err := s.capture(sh, filepath.Join(dir, sh.name+".png")); err != nil {
return fmt.Errorf("%s: %w", sh.name, err)
}
}
return nil
}
func selectNamed(ctx *context.Editor, name string) error {
sc := ctx.Scene()
if sc == nil {
return fmt.Errorf("no scene is open")
}
h := sc.Find(sc.Root(), name)
if !sc.IsAlive(h) {
return fmt.Errorf("the scene has no node %q", name)
}
ctx.Select(sc.ID(h))
return nil
}
func (s *Session) capture(sh shot, path string) error {
for i := 0; i < sh.frames; i++ {
if !s.Step() {
return fmt.Errorf("the editor quit")
}
}
var img *image.RGBA
var err error
s.Engine.CaptureFrame(func(got *image.RGBA, e error) { img, err = got, e })
s.Step()
if err != nil {
return err
}
if img == nil {
return fmt.Errorf("no frame was presented")
}
b := img.Bounds()
crop := img.SubImage(image.Rect(b.Min.X+sh.inset, b.Min.Y+sh.inset, b.Max.X-sh.inset, b.Max.Y-sh.inset-sh.bottom))
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, crop)
}
// shotMeter is a session three quarters into a 200k window, for the Context popup's screenshot.
func shotMeter() *agentui.Meter {
tree := strings.Repeat(`{"id":"#c0000000000000000000000000000003","name":"Platform","path":"Root/Platform","components":["engine.Body2D"]},`, 900)
m := agentui.NewMeter(agentbridge.Guide, strings.Repeat(`{"name":"scene_tree","description":"node tree"},`, 400))
m.SetModel("claude-opus-5-5")
m.User("Why does the player fall through the moving platform?")
m.Usage(21_400)
reads := []struct{ tool, label, detail, text string }{
{"scene_tree", "Read scene main.aescene", "48 nodes", tree},
{"scene_get", "Read Player", "8 components", tree[:6000]},
{"script_read", "Read player.aes", "212 lines", strings.Repeat("\tif m.OnGround() && input.Pressed(\"Jump\") {\n\t\tm.Jump()\n\t}\n", 700)},
{"docs_search", "Search docs: moving platform", "10 results", strings.Repeat("Moving platforms carry the body that stands on them. ", 200)},
{"project_get", "Read project settings", "", tree[:9000]},
}
for i, r := range reads {
id := strconv.Itoa(i)
m.Call(r.tool, id, "{}", r.label)
m.Result(r.tool, id, r.text, r.detail, false)
}
m.Text("The platform moves in the render step, so the body lags one frame behind it.")
m.Usage(152_300)
m.SetWindow(200_000)
return m
}