← WebMCP Directory

Tool tree

bullet-heaven.ahraz.me

Media & PersonalTools verified
Website
An agent-ready bullet hell maker built with Phaser and WebMCP.
Explore capabilities

    page: Every page

    get_maker_stateAnswer

    Returns { app, detail, project, playtest }: app identifies the visible landing/editor/play surface, project is the versioned document at the requested detail, and playtest is the current session. Read it when you start work, when you need wave, emitter or pattern ids you do not already have, and immediately before every project or playtest mutation because human and agent edits share one live document. You do not need to call it after a mutation because each mutating result reflects the state it changed. It defaults to an outline of the encounter rather than every emitter and pattern; pass waveId to inspect one wave in full, or detail 'full' for the whole document.

    View tool JSON
    {
      "name": "get_maker_state",
      "kind": "answer",
      "impl": "imperative",
      "description": "Returns { app, detail, project, playtest }: app identifies the visible landing/editor/play surface, project is the versioned document at the requested detail, and playtest is the current session. Read it when you start work, when you need wave, emitter or pattern ids you do not already have, and immediately before every project or playtest mutation because human and agent edits share one live document. You do not need to call it after a mutation because each mutating result reflects the state it changed. It defaults to an outline of the encounter rather than every emitter and pattern; pass waveId to inspect one wave in full, or detail 'full' for the whole document.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "detail": {
            "type": "string",
            "enum": [
              "outline",
              "full"
            ],
            "description": "'outline' (the default) returns arena, player and one entry per wave carrying its id, name, enabled, start condition and emitter ids, which is everything needed to address the encounter. 'full' returns every emitter and firing pattern as well, which is large on a big level."
          },
          "waveId": {
            "type": "string",
            "minLength": 1,
            "description": "Also return this one wave in full, with its emitters and firing patterns. Cheaper than detail 'full' when you only need to inspect one wave."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    evaluate_encounterAction

    Plays the authored encounter through to the end, headlessly, against a scripted reference pilot, and returns what happened: whether the level was cleared, how long the pilot survived, hits taken, waves cleared out of the total, peak projectile count, tightest dodge margin, and the wave that killed it. This never changes the project and never disturbs the playtest the person is watching; results and settings appear in the editor's Evaluation panel, so the person sees what you measured. Pass profileName to evaluate against a pilot calibrated to a real player instead of a generic tier. The simulation is deterministic, so one run is the answer for a given seed; call again with a different seed to see how much outcomes vary. Report results as what the named pilot tier achieved, never as absolute difficulty. Use it after editing waves, emitters, or firing patterns to check whether an encounter is survivable and how long it lasts. To find what to tune, read 'diagnostics' rather than the run totals. diagnostics.waves gives per-wave hitsTaken, closestCall, peakEnemyProjectiles, emitterHits and cleared, which is how you locate the wave that is actually applying the pressure; diagnostics.fatalThreat names the waveId, emitterId and patternId of the shot that killed the pilot, which is the pattern to edit. 'unreachedWaveIds' lists enabled waves the run never got to: if a wave you edited is in that list, this run did not test your change and says nothing about it. A before/after comparison only means something if the measurement can move. When the pilot clears the encounter both before and after an easing edit, hit counts differing by one or two across seeds is noise, not an improvement; compare per-wave diagnostics for the wave you edited, or use a pilot that actually dies there. Use the returned replayId with control_playtest action 'replay' to show the trial on the canvas.

    View tool JSON
    {
      "name": "evaluate_encounter",
      "kind": "act",
      "impl": "imperative",
      "description": "Plays the authored encounter through to the end, headlessly, against a scripted reference pilot, and returns what happened: whether the level was cleared, how long the pilot survived, hits taken, waves cleared out of the total, peak projectile count, tightest dodge margin, and the wave that killed it. This never changes the project and never disturbs the playtest the person is watching; results and settings appear in the editor's Evaluation panel, so the person sees what you measured. Pass profileName to evaluate against a pilot calibrated to a real player instead of a generic tier. The simulation is deterministic, so one run is the answer for a given seed; call again with a different seed to see how much outcomes vary. Report results as what the named pilot tier achieved, never as absolute difficulty. Use it after editing waves, emitters, or firing patterns to check whether an encounter is survivable and how long it lasts.\n\nTo find what to tune, read 'diagnostics' rather than the run totals. diagnostics.waves gives per-wave hitsTaken, closestCall, peakEnemyProjectiles, emitterHits and cleared, which is how you locate the wave that is actually applying the pressure; diagnostics.fatalThreat names the waveId, emitterId and patternId of the shot that killed the pilot, which is the pattern to edit. 'unreachedWaveIds' lists enabled waves the run never got to: if a wave you edited is in that list, this run did not test your change and says nothing about it.\n\nA before/after comparison only means something if the measurement can move. When the pilot clears the encounter both before and after an easing edit, hit counts differing by one or two across seeds is noise, not an improvement; compare per-wave diagnostics for the wave you edited, or use a pilot that actually dies there. Use the returned replayId with control_playtest action 'replay' to show the trial on the canvas.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "pilot": {
            "type": "string",
            "enum": [
              "novice",
              "average",
              "expert"
            ],
            "description": "Reference pilot skill tier. Tiers differ by reaction latency and aim precision."
          },
          "maxSeconds": {
            "type": "number",
            "minimum": 5,
            "maximum": 600,
            "description": "Safety cutoff in simulated seconds. The run normally ends when the level is cleared or the pilot dies; this only stops an encounter that can never resolve."
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "description": "Reproducibility seed; the same seed replays the same batch."
          },
          "startWaveId": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "description": "Evaluate from this wave onward; null evaluates the encounter."
          },
          "profileName": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "description": "Use a saved pilot profile instead of a built-in tier; supersedes 'pilot'. Null uses the tier."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    update_arenaAction

    Changes live arena dimensions or background color and returns { updated: 'arena', arena }. Omitted settings stay unchanged. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "update_arena",
      "kind": "act",
      "impl": "imperative",
      "description": "Changes live arena dimensions or background color and returns { updated: 'arena', arena }. Omitted settings stay unchanged. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "width": {
            "type": "number",
            "minimum": 320,
            "maximum": 1920,
            "description": "Playfield width in pixels. Positions are normalized, so they follow a resize."
          },
          "height": {
            "type": "number",
            "minimum": 240,
            "maximum": 1080,
            "description": "Playfield height in pixels."
          },
          "backgroundColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Arena color as a six-digit CSS hex value."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    update_playerAction

    Changes live player settings and returns { updated: 'player', player }. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "update_player",
      "kind": "act",
      "impl": "imperative",
      "description": "Changes live player settings and returns { updated: 'player', player }. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "moveSpeed": {
            "type": "number",
            "minimum": 20,
            "maximum": 1200,
            "description": "Pixels per second."
          },
          "focusSpeed": {
            "type": "number",
            "minimum": 10,
            "maximum": 1200,
            "description": "Pixels per second in focus, the slow precise mode."
          },
          "hitboxRadius": {
            "type": "number",
            "minimum": 1,
            "maximum": 32,
            "description": "The player's damage circle in pixels, deliberately smaller than renderRadius. The strongest difficulty dial here."
          },
          "renderRadius": {
            "type": "number",
            "minimum": 4,
            "maximum": 48,
            "description": "Drawn size in pixels. Purely visual."
          },
          "startX": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Spawn position, normalized 0-1 across the arena."
          },
          "startY": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Spawn position, normalized 0-1 down the arena."
          },
          "startAngle": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Starting aim in degrees: 0 points right and -90 points up."
          },
          "fireRate": {
            "type": "number",
            "minimum": 1,
            "maximum": 30,
            "description": "Player shots fired per simulation second."
          },
          "lives": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9,
            "description": "Hits the player survives."
          },
          "invulnerabilityDuration": {
            "type": "number",
            "minimum": 0,
            "maximum": 5,
            "description": "Seconds of immunity after a hit."
          },
          "color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Player color as a six-digit CSS hex value."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    control_playtestAction

    Controls the simulation and returns { updated: 'playtest', playtest } with the resulting session settings. Action 'replay' plays a recording back on the canvas and accepts only replayId; speed, showHitboxes, and startWaveId belong to ordinary playtest control calls. A recording carries the project it was made against, so it stays watchable after later edits. The result reflects the state after this call, so no follow-up read is needed. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "control_playtest",
      "kind": "act",
      "impl": "imperative",
      "description": "Controls the simulation and returns { updated: 'playtest', playtest } with the resulting session settings. Action 'replay' plays a recording back on the canvas and accepts only replayId; speed, showHitboxes, and startWaveId belong to ordinary playtest control calls. A recording carries the project it was made against, so it stays watchable after later edits. The result reflects the state after this call, so no follow-up read is needed. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "play",
              "pause",
              "reset",
              "replay"
            ],
            "description": "'reset' restarts the run from the beginning. 'replay' plays a recording on the canvas and accepts only replayId alongside it, rejecting speed, showHitboxes and startWaveId."
          },
          "replayId": {
            "type": "string",
            "minLength": 1,
            "description": "Recording to play back on the canvas; defaults to the newest replay. Only valid with action 'replay'. Get ids from list_replays or evaluate_encounter."
          },
          "speed": {
            "type": "number",
            "enum": [
              0.25,
              0.5,
              1,
              2
            ],
            "description": "Simulation rate multiplier. Not valid with action replay."
          },
          "showHitboxes": {
            "type": "boolean",
            "description": "Draws damage circles for the player and projectiles. Not valid with action replay."
          },
          "startWaveId": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ],
            "description": "Editor preview wave to start from; null starts the encounter normally."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    open_editorAction

    Leaves Play mode and shows the editor again, so the person can keep building without navigating back themselves. Project-mutation tools perform this transition automatically; call open_editor directly when returning to the editor is itself the requested action. Safe to call from the editor: it reports outcome 'already-in-editor' and changes nothing. The outcome says how it returned: 'exited-play-mode' from a full-screen editor playtest, 'left-play-route' from the local play page, 'left-shared-play-route' from a shared /play/<id> link, and 'opening-editor' from anywhere else, such as the landing page at /. Every route change is client-side, so ready is true and the editor is usable immediately; nothing here reloads the page or interrupts your connection. The result reflects the state after this call, so no follow-up read is needed. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "open_editor",
      "kind": "act",
      "impl": "imperative",
      "description": "Leaves Play mode and shows the editor again, so the person can keep building without navigating back themselves. Project-mutation tools perform this transition automatically; call open_editor directly when returning to the editor is itself the requested action. Safe to call from the editor: it reports outcome 'already-in-editor' and changes nothing. The outcome says how it returned: 'exited-play-mode' from a full-screen editor playtest, 'left-play-route' from the local play page, 'left-shared-play-route' from a shared /play/<id> link, and 'opening-editor' from anywhere else, such as the landing page at /. Every route change is client-side, so ready is true and the editor is usable immediately; nothing here reloads the page or interrupts your connection. The result reflects the state after this call, so no follow-up read is needed. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      },
      "page": "/editor"
    }
    control_historyAnswer

    Undoes or redoes one authored project edit and returns { action, applied, history, project }, where project is an outline: arena, player, and one entry per wave with its ids. Read get_maker_state with a waveId or detail 'full' if you need to see what the edit did to an emitter or pattern. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "control_history",
      "kind": "answer",
      "impl": "imperative",
      "description": "Undoes or redoes one authored project edit and returns { action, applied, history, project }, where project is an outline: arena, player, and one entry per wave with its ids. Read get_maker_state with a waveId or detail 'full' if you need to see what the edit did to an emitter or pattern. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "undo",
              "redo"
            ],
            "description": "Steps one authored edit back or forward; playtest controls and runtime state are not in history. 'applied' is false when there was nothing to step to."
          }
        },
        "required": [
          "action"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    apply_encounterAction

    Builds an entire encounter in one call and replaces every existing wave with it. Prefer this over repeated edit_wave and edit_emitter calls whenever you are creating a level or restructuring more than one wave; use the edit_ tools to adjust an encounter that is already close. Every field except each emitter's x and y is optional and falls back to a sensible default, so a playable level can be as short as a list of emitter positions. Returns { updated: 'encounter', waveCount, emitterCount, waves }, where each 'waves' entry carries the generated id, name and emitterIds that a follow-up edit_ call needs; the encounter is not echoed back, so read get_maker_state with a waveId to inspect one wave. The whole replacement is a single undo step, so control_history reverts it in one call. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "apply_encounter",
      "kind": "act",
      "impl": "imperative",
      "description": "Builds an entire encounter in one call and replaces every existing wave with it. Prefer this over repeated edit_wave and edit_emitter calls whenever you are creating a level or restructuring more than one wave; use the edit_ tools to adjust an encounter that is already close. Every field except each emitter's x and y is optional and falls back to a sensible default, so a playable level can be as short as a list of emitter positions. Returns { updated: 'encounter', waveCount, emitterCount, waves }, where each 'waves' entry carries the generated id, name and emitterIds that a follow-up edit_ call needs; the encounter is not echoed back, so read get_maker_state with a waveId to inspect one wave. The whole replacement is a single undo step, so control_history reverts it in one call. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "waves": {
            "minItems": 1,
            "maxItems": 24,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 80,
                  "description": "A name for this wave that a later wave can reference through start.waveId. Only needed when something references it; otherwise one is generated."
                },
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 80
                },
                "enabled": {
                  "type": "boolean",
                  "description": "A disabled wave is skipped, by both the playtest and evaluation."
                },
                "start": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "encounter_start",
                        "at_time",
                        "after_wave_cleared",
                        "defeat_count"
                      ],
                      "description": "What starts the wave, and which of the other start fields apply: 'encounter_start' takes delay, 'at_time' requires time, 'after_wave_cleared' requires waveId and takes delay, 'defeat_count' requires waveId and count."
                    },
                    "delay": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 3600,
                      "description": "Seconds to wait once the condition is met. For type encounter_start or after_wave_cleared."
                    },
                    "time": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 3600,
                      "description": "Seconds from the start of the encounter. Required by type at_time."
                    },
                    "waveId": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The earlier wave this one waits on, matching that wave's own 'id'. Required by type after_wave_cleared and defeat_count, and it must appear earlier in the waves array."
                    },
                    "count": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 999,
                      "description": "Emitters of the referenced wave that must be defeated first. Required by type defeat_count."
                    }
                  },
                  "required": [
                    "type"
                  ],
                  "additionalProperties": false,
                  "description": "When the wave begins. Omit it and the wave starts after the previous entry in the array clears, which is what a linear level wants."
                },
                "emitters": {
                  "minItems": 1,
                  "maxItems": 24,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 80
                      },
                      "enabled": {
                        "type": "boolean"
                      },
                      "angle": {
                        "type": "number",
                        "minimum": -180,
                        "maximum": 180,
                        "description": "Facing in screen-space degrees: 0 points right and -90 points up."
                      },
                      "radius": {
                        "type": "number",
                        "minimum": 2,
                        "maximum": 64,
                        "description": "Body size in pixels. This is also the damage circle, so a smaller emitter is harder to hit."
                      },
                      "visibleInPlay": {
                        "type": "boolean",
                        "description": "Whether the body is drawn during Play. It still fires when hidden."
                      },
                      "combatType": {
                        "type": "string",
                        "enum": [
                          "destructible",
                          "invulnerable"
                        ],
                        "description": "'destructible' can be shot down. 'invulnerable' cannot, and rejects hitPoints and countsTowardClear."
                      },
                      "hitPoints": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 999,
                        "description": "Shots to destroy. Requires combatType destructible."
                      },
                      "countsTowardClear": {
                        "type": "boolean",
                        "description": "Whether the wave waits for this emitter to be defeated. Requires combatType destructible; when true the lifetime must be until_defeated, because the wave is waiting for it."
                      },
                      "lifetimeType": {
                        "type": "string",
                        "enum": [
                          "until_defeated",
                          "duration",
                          "until_wave_complete"
                        ],
                        "description": "'until_defeated' lasts until shot down and is the only lifetime valid for an emitter that counts toward wave clear. 'duration' needs lifetimeSeconds. 'until_wave_complete' disappears when the wave ends."
                      },
                      "lifetimeSeconds": {
                        "type": "number",
                        "minimum": 0,
                        "maximum": 3600,
                        "description": "Seconds the emitter lives. Required by, and only valid with, lifetimeType duration."
                      },
                      "movementType": {
                        "type": "string",
                        "enum": [
                          "stationary",
                          "patrol"
                        ],
                        "description": "'patrol' moves between (x, y) and (endX, endY) and needs all three patrol fields; 'stationary' rejects them."
                      },
                      "endX": {
                        "type": "number",
                        "minimum": -0.5,
                        "maximum": 1.5,
                        "description": "Patrol endpoint, normalized like x. Requires movementType patrol."
                      },
                      "endY": {
                        "type": "number",
                        "minimum": -0.5,
                        "maximum": 1.5,
                        "description": "Patrol endpoint, normalized like y. Requires movementType patrol."
                      },
                      "movementSpeed": {
                        "type": "number",
                        "minimum": 1,
                        "maximum": 1200,
                        "description": "Patrol speed in pixels per second. Requires movementType patrol."
                      },
                      "x": {
                        "type": "number",
                        "minimum": -0.5,
                        "maximum": 1.5,
                        "description": "Horizontal position, normalized 0-1 across the arena. Values slightly outside that range place the emitter off-screen."
                      },
                      "y": {
                        "type": "number",
                        "minimum": -0.5,
                        "maximum": 1.5,
                        "description": "Vertical position, normalized 0-1 across the arena."
                      },
                      "patterns": {
                        "minItems": 1,
                        "maxItems": 32,
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 80
                            },
                            "enabled": {
                              "type": "boolean"
                            },
                            "initialDelay": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 3600,
                              "description": "Seconds after the emitter appears before it first fires."
                            },
                            "interval": {
                              "type": "number",
                              "minimum": 0.05,
                              "maximum": 30,
                              "description": "Seconds between volleys."
                            },
                            "burstCount": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 99,
                              "description": "Volleys fired back to back, burstInterval apart, each interval."
                            },
                            "burstInterval": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 30,
                              "description": "Seconds between the volleys of one burst."
                            },
                            "volleys": {
                              "anyOf": [
                                {
                                  "type": "string",
                                  "const": "continuous"
                                },
                                {
                                  "type": "integer",
                                  "minimum": 1,
                                  "maximum": 999
                                }
                              ],
                              "description": "How many volleys the pattern fires in total, or 'continuous' to fire for as long as the emitter lives."
                            },
                            "formationType": {
                              "type": "string",
                              "enum": [
                                "single",
                                "fan",
                                "ring"
                              ],
                              "description": "'single' fires one projectile, 'fan' spreads formationCount across spreadAngle, 'ring' spreads formationCount over 360 degrees. A fan or ring needs formationCount."
                            },
                            "formationCount": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 128,
                              "description": "Projectiles per volley. Requires formationType fan or ring, and is required when creating one; a single formation fires one projectile and rejects it."
                            },
                            "spreadAngle": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 360,
                              "description": "Total width of a fan in degrees. Requires formationType fan; a ring already covers 360 degrees."
                            },
                            "aimMode": {
                              "type": "string",
                              "enum": [
                                "at_player",
                                "fixed"
                              ],
                              "description": "'at_player' samples the player's position each volley; 'fixed' fires along the emitter's angle."
                            },
                            "rotationPerVolley": {
                              "type": "number",
                              "minimum": -180,
                              "maximum": 180,
                              "description": "Degrees the aim advances per volley; this is what makes a spiral."
                            },
                            "speed": {
                              "type": "number",
                              "minimum": 20,
                              "maximum": 2000,
                              "description": "Projectile speed in pixels per second."
                            },
                            "acceleration": {
                              "type": "number",
                              "minimum": -5000,
                              "maximum": 5000,
                              "description": "Pixels per second squared, up to maxSpeed."
                            },
                            "maxSpeed": {
                              "type": "number",
                              "minimum": 20,
                              "maximum": 4000,
                              "description": "Ceiling for an accelerating projectile, in pixels per second. Must be at least speed; omit it and it follows a raised speed up."
                            },
                            "angularVelocity": {
                              "type": "number",
                              "minimum": -720,
                              "maximum": 720,
                              "description": "Degrees per second the projectile curves as it travels."
                            },
                            "lifetime": {
                              "type": "number",
                              "minimum": 0.05,
                              "maximum": 60,
                              "description": "Seconds a projectile lives before it is culled."
                            },
                            "collisionRadius": {
                              "type": "number",
                              "minimum": 1,
                              "maximum": 32,
                              "description": "Damage circle in pixels, independent of renderRadius."
                            },
                            "renderRadius": {
                              "type": "number",
                              "minimum": 1,
                              "maximum": 48,
                              "description": "Drawn size in pixels. Purely visual."
                            },
                            "color": {
                              "type": "string",
                              "pattern": "^#[0-9a-fA-F]{6}$",
                              "description": "Projectile color as a six-digit CSS hex value."
                            }
                          },
                          "additionalProperties": false
                        },
                        "description": "Firing patterns for this emitter. Omit it for one default pattern; several patterns on one emitter fire independently and layer into a composite attack."
                      }
                    },
                    "required": [
                      "x",
                      "y"
                    ],
                    "additionalProperties": false
                  }
                }
              },
              "required": [
                "emitters"
              ],
              "additionalProperties": false
            },
            "description": "The waves to build, in play order. This replaces every existing wave."
          }
        },
        "required": [
          "waves"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    edit_waveAction

    Adds, updates, or removes an encounter wave and returns { action, waveId, wave, waveCount, emitterCount }, where wave is an outline: the wave's own settings and its emitter ids, not the emitter bodies. Remove reports the id and the new counts with no wave. Use edit_emitter to populate a wave this adds. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "edit_wave",
      "kind": "act",
      "impl": "imperative",
      "description": "Adds, updates, or removes an encounter wave and returns { action, waveId, wave, waveCount, emitterCount }, where wave is an outline: the wave's own settings and its emitter ids, not the emitter bodies. Remove reports the id and the new counts with no wave. Use edit_emitter to populate a wave this adds. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "add",
              "update",
              "remove"
            ],
            "description": "'add' generates the waveId and rejects one you supply. 'update' and 'remove' require waveId, and 'remove' accepts nothing else."
          },
          "waveId": {
            "type": "string",
            "minLength": 1,
            "description": "The wave to change. Required to update or remove, omitted to add."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "enabled": {
            "type": "boolean",
            "description": "A disabled wave is skipped, by both the playtest and evaluation."
          },
          "startType": {
            "type": "string",
            "enum": [
              "encounter_start",
              "at_time",
              "after_wave_cleared",
              "defeat_count"
            ],
            "description": "What starts the wave, and which timing fields apply: 'encounter_start' takes delay, 'at_time' requires time, 'after_wave_cleared' requires sourceWaveId and takes delay, 'defeat_count' requires sourceWaveId and takes defeatCount. Required whenever you change any of those fields, and each type rejects the ones that are not its own. On update, a timing field you omit keeps its current value."
          },
          "sourceWaveId": {
            "type": "string",
            "minLength": 1,
            "description": "The wave this one waits on. Required by startType after_wave_cleared and defeat_count, and must name an earlier wave."
          },
          "delay": {
            "type": "number",
            "minimum": 0,
            "maximum": 3600,
            "description": "Seconds to wait after the start condition is met. For startType encounter_start or after_wave_cleared."
          },
          "time": {
            "type": "number",
            "minimum": 0,
            "maximum": 3600,
            "description": "Seconds from the start of the encounter. Required by, and only valid with, startType at_time."
          },
          "defeatCount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999,
            "description": "How many of the source wave's emitters must be defeated first. Only valid with startType defeat_count."
          }
        },
        "required": [
          "action"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    edit_emitterAction

    Adds, updates, or removes a positioned emitter, covering its presentation, size, combat, lifetime, movement and simple firing settings, and returns { action, waveId, emitterId, emitter, waveCount, emitterCount } with the affected emitter in full. Remove reports the ids and the new counts with no emitter. fireInterval, projectileSpeed and aimMode are a shortcut that edits the emitter's first firing pattern; edit_firing_pattern handles bursts, formations and projectile motion. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "edit_emitter",
      "kind": "act",
      "impl": "imperative",
      "description": "Adds, updates, or removes a positioned emitter, covering its presentation, size, combat, lifetime, movement and simple firing settings, and returns { action, waveId, emitterId, emitter, waveCount, emitterCount } with the affected emitter in full. Remove reports the ids and the new counts with no emitter. fireInterval, projectileSpeed and aimMode are a shortcut that edits the emitter's first firing pattern; edit_firing_pattern handles bursts, formations and projectile motion. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "add",
              "update",
              "remove"
            ],
            "description": "'add' generates the emitterId and rejects one you supply. 'update' and 'remove' require emitterId, and 'remove' accepts nothing but the ids."
          },
          "waveId": {
            "type": "string",
            "minLength": 1,
            "description": "The wave the emitter belongs to."
          },
          "emitterId": {
            "type": "string",
            "minLength": 1,
            "description": "The emitter to change. Required to update or remove, omitted to add."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "enabled": {
            "type": "boolean"
          },
          "x": {
            "type": "number",
            "minimum": -0.5,
            "maximum": 1.5,
            "description": "Horizontal position, normalized 0-1 across the arena. Values slightly outside that range place the emitter off-screen."
          },
          "y": {
            "type": "number",
            "minimum": -0.5,
            "maximum": 1.5,
            "description": "Vertical position, normalized 0-1 across the arena."
          },
          "angle": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Facing in screen-space degrees: 0 points right and -90 points up."
          },
          "radius": {
            "type": "number",
            "minimum": 2,
            "maximum": 64,
            "description": "Body size in pixels. This is also the damage circle, so a smaller emitter is harder to hit."
          },
          "visibleInPlay": {
            "type": "boolean",
            "description": "Whether the body is drawn during Play. It still fires when hidden."
          },
          "combatType": {
            "type": "string",
            "enum": [
              "destructible",
              "invulnerable"
            ],
            "description": "'destructible' can be shot down. 'invulnerable' cannot, and rejects hitPoints and countsTowardClear."
          },
          "hitPoints": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999,
            "description": "Shots to destroy. Requires combatType destructible."
          },
          "countsTowardClear": {
            "type": "boolean",
            "description": "Whether the wave waits for this emitter to be defeated. Requires combatType destructible; when true the lifetime must be until_defeated, because the wave is waiting for it."
          },
          "lifetimeType": {
            "type": "string",
            "enum": [
              "until_defeated",
              "duration",
              "until_wave_complete"
            ],
            "description": "'until_defeated' lasts until shot down and is the only lifetime valid for an emitter that counts toward wave clear. 'duration' needs lifetimeSeconds. 'until_wave_complete' disappears when the wave ends."
          },
          "lifetimeSeconds": {
            "type": "number",
            "minimum": 0,
            "maximum": 3600,
            "description": "Seconds the emitter lives. Required by, and only valid with, lifetimeType duration."
          },
          "movementType": {
            "type": "string",
            "enum": [
              "stationary",
              "patrol"
            ],
            "description": "'patrol' moves between (x, y) and (endX, endY) and needs all three patrol fields; 'stationary' rejects them."
          },
          "endX": {
            "type": "number",
            "minimum": -0.5,
            "maximum": 1.5,
            "description": "Patrol endpoint, normalized like x. Requires movementType patrol."
          },
          "endY": {
            "type": "number",
            "minimum": -0.5,
            "maximum": 1.5,
            "description": "Patrol endpoint, normalized like y. Requires movementType patrol."
          },
          "movementSpeed": {
            "type": "number",
            "minimum": 1,
            "maximum": 1200,
            "description": "Patrol speed in pixels per second. Requires movementType patrol."
          },
          "fireInterval": {
            "type": "number",
            "minimum": 0.05,
            "maximum": 30,
            "description": "Seconds between enemy shots. Same setting as the pattern's interval."
          },
          "projectileSpeed": {
            "type": "number",
            "minimum": 20,
            "maximum": 2000,
            "description": "Enemy projectile speed in pixels per second. Same setting as the pattern's speed."
          },
          "aimMode": {
            "type": "string",
            "enum": [
              "at_player",
              "fixed"
            ],
            "description": "'at_player' samples the player's position each volley; 'fixed' fires along the emitter's angle."
          }
        },
        "required": [
          "action",
          "waveId"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    edit_firing_patternAction

    Adds, updates, or removes an emitter firing pattern and returns { action, waveId, emitterId, patternId, pattern, waveCount, emitterCount } with the affected pattern in full. Remove reports the ids and the new counts with no pattern. A pattern is one rhythm, one formation and one projectile; layer several on an emitter for a composite attack. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "edit_firing_pattern",
      "kind": "act",
      "impl": "imperative",
      "description": "Adds, updates, or removes an emitter firing pattern and returns { action, waveId, emitterId, patternId, pattern, waveCount, emitterCount } with the affected pattern in full. Remove reports the ids and the new counts with no pattern. A pattern is one rhythm, one formation and one projectile; layer several on an emitter for a composite attack. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "add",
              "update",
              "remove"
            ],
            "description": "'add' generates the patternId and rejects one you supply. 'update' and 'remove' require patternId, and 'remove' accepts nothing but the ids. An emitter must keep at least one pattern, so the last one cannot be removed."
          },
          "waveId": {
            "type": "string",
            "minLength": 1,
            "description": "The wave the emitter belongs to."
          },
          "emitterId": {
            "type": "string",
            "minLength": 1,
            "description": "The emitter that owns the pattern."
          },
          "patternId": {
            "type": "string",
            "minLength": 1,
            "description": "The pattern to change. Required to update or remove, omitted to add."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "enabled": {
            "type": "boolean"
          },
          "initialDelay": {
            "type": "number",
            "minimum": 0,
            "maximum": 3600,
            "description": "Seconds after the emitter appears before it first fires."
          },
          "interval": {
            "type": "number",
            "minimum": 0.05,
            "maximum": 30,
            "description": "Seconds between volleys."
          },
          "burstCount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 99,
            "description": "Volleys fired back to back, burstInterval apart, each interval."
          },
          "burstInterval": {
            "type": "number",
            "minimum": 0,
            "maximum": 30,
            "description": "Seconds between the volleys of one burst."
          },
          "volleys": {
            "anyOf": [
              {
                "type": "string",
                "const": "continuous"
              },
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 999
              }
            ],
            "description": "How many volleys the pattern fires in total, or 'continuous' to fire for as long as the emitter lives."
          },
          "formationType": {
            "type": "string",
            "enum": [
              "single",
              "fan",
              "ring"
            ],
            "description": "'single' fires one projectile, 'fan' spreads formationCount across spreadAngle, 'ring' spreads formationCount over 360 degrees. A fan or ring needs formationCount."
          },
          "formationCount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 128,
            "description": "Projectiles per volley. Requires formationType fan or ring, and is required when creating one; a single formation fires one projectile and rejects it."
          },
          "spreadAngle": {
            "type": "number",
            "minimum": 0,
            "maximum": 360,
            "description": "Total width of a fan in degrees. Requires formationType fan; a ring already covers 360 degrees."
          },
          "aimMode": {
            "type": "string",
            "enum": [
              "at_player",
              "fixed"
            ],
            "description": "'at_player' samples the player's position each volley; 'fixed' fires along the emitter's angle."
          },
          "rotationPerVolley": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Degrees the aim advances per volley; this is what makes a spiral."
          },
          "speed": {
            "type": "number",
            "minimum": 20,
            "maximum": 2000,
            "description": "Projectile speed in pixels per second."
          },
          "acceleration": {
            "type": "number",
            "minimum": -5000,
            "maximum": 5000,
            "description": "Pixels per second squared, up to maxSpeed."
          },
          "maxSpeed": {
            "type": "number",
            "minimum": 20,
            "maximum": 4000,
            "description": "Ceiling for an accelerating projectile, in pixels per second. Must be at least speed; omit it and it follows a raised speed up."
          },
          "angularVelocity": {
            "type": "number",
            "minimum": -720,
            "maximum": 720,
            "description": "Degrees per second the projectile curves as it travels."
          },
          "lifetime": {
            "type": "number",
            "minimum": 0.05,
            "maximum": 60,
            "description": "Seconds a projectile lives before it is culled."
          },
          "collisionRadius": {
            "type": "number",
            "minimum": 1,
            "maximum": 32,
            "description": "Damage circle in pixels, independent of renderRadius."
          },
          "renderRadius": {
            "type": "number",
            "minimum": 1,
            "maximum": 48,
            "description": "Drawn size in pixels. Purely visual."
          },
          "color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Projectile color as a six-digit CSS hex value."
          }
        },
        "required": [
          "action",
          "waveId",
          "emitterId"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    list_projectsAnswer

    Lists the projects saved in this browser, with the id load_project needs. 'hasUnsavedChanges' says whether the live project differs from every saved copy, and each entry's 'matchesLiveProject' says whether that copy is the one on screen. Saved projects persist across reloads and are per-browser, not per-account. Read-only.

    View tool JSON
    {
      "name": "list_projects",
      "kind": "answer",
      "impl": "imperative",
      "description": "Lists the projects saved in this browser, with the id load_project needs. 'hasUnsavedChanges' says whether the live project differs from every saved copy, and each entry's 'matchesLiveProject' says whether that copy is the one on screen. Saved projects persist across reloads and are per-browser, not per-account. Read-only.",
      "inputSchema": {
        "type": "object",
        "properties": {},
        "additionalProperties": false
      },
      "page": "/editor"
    }
    save_projectAction

    Saves the live project into this browser's project library and returns { saved, library }. This is the editor's Save, not a save-as: when a project is already active it is overwritten and renamed to 'name', so saving twice does not leave two copies and an earlier snapshot is replaced rather than kept. With no active project it creates one. Save before load_project or new_project, which discard the live document permanently.

    View tool JSON
    {
      "name": "save_project",
      "kind": "act",
      "impl": "imperative",
      "description": "Saves the live project into this browser's project library and returns { saved, library }. This is the editor's Save, not a save-as: when a project is already active it is overwritten and renamed to 'name', so saving twice does not leave two copies and an earlier snapshot is replaced rather than kept. With no active project it creates one. Save before load_project or new_project, which discard the live document permanently.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Name to save under. Omit to keep the active project's current name."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    load_projectAction

    Opens a saved project, replacing the live one, and returns { loaded, project, library, evaluation }, where project is an outline: arena, player, and one entry per wave with its ids. This discards whatever was on screen and clears the undo history, so control_history cannot bring it back: call save_project first unless you mean to lose it. When the live project has unsaved changes this refuses unless you pass discardUnsavedChanges: true. Any evaluation result describes the project that was replaced, so measure again before judging this one. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "load_project",
      "kind": "act",
      "impl": "imperative",
      "description": "Opens a saved project, replacing the live one, and returns { loaded, project, library, evaluation }, where project is an outline: arena, player, and one entry per wave with its ids. This discards whatever was on screen and clears the undo history, so control_history cannot bring it back: call save_project first unless you mean to lose it. When the live project has unsaved changes this refuses unless you pass discardUnsavedChanges: true. Any evaluation result describes the project that was replaced, so measure again before judging this one. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Which saved project to open; get it from list_projects."
          },
          "discardUnsavedChanges": {
            "type": "boolean",
            "description": "Required to proceed when the live project has unsaved changes, which this call destroys."
          }
        },
        "required": [
          "projectId"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    new_projectAction

    Replaces the live project with a fresh default one, saves it into the library as the active project, and returns { created, project, library, evaluation }, where project is an outline: arena, player, and one entry per wave with its ids. Like load_project this discards whatever was on screen and clears the undo history, and it refuses when the live project has unsaved changes unless you pass discardUnsavedChanges: true. Use apply_encounter afterwards to build the level; this only gives you the starting document. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.

    View tool JSON
    {
      "name": "new_project",
      "kind": "act",
      "impl": "imperative",
      "description": "Replaces the live project with a fresh default one, saves it into the library as the active project, and returns { created, project, library, evaluation }, where project is an outline: arena, player, and one entry per wave with its ids. Like load_project this discards whatever was on screen and clears the undo history, and it refuses when the live project has unsaved changes unless you pass discardUnsavedChanges: true. Use apply_encounter afterwards to build the level; this only gives you the starting document. The result reflects the state after this call, so no follow-up read is needed. The result's 'evaluation' reports whether the project has been measured since it changed; on verified: false, run evaluate_encounter before calling the edit an improvement. This returns to the editor first, on its own. From a landing page or shared play link that needs a page load, it opens the editor and asks you to retry once WebMCP reconnects rather than editing during teardown. Call get_maker_state immediately before this: the person edits the same live state you do.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Name for the new project."
          },
          "discardUnsavedChanges": {
            "type": "boolean",
            "description": "Required to proceed when the live project has unsaved changes, which this call destroys."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    list_replaysAnswer

    Lists recordings available to this page, newest first. A 'human' replay is a run the person played in Play mode; a 'pilot' replay is a simulated run. Human sessions are saved in the browser and survive a reload, so recordings from earlier visits are usually still here to analyze; pilot runs live in memory only and are lost on reload, which costs nothing because re-running evaluate_encounter or calibrate_pilot with the same seed reproduces them exactly. Use the returned replayId with control_playtest action 'replay' to watch one, or with analyze_replays to measure it and to find which pattern landed the fatal hit.

    View tool JSON
    {
      "name": "list_replays",
      "kind": "answer",
      "impl": "imperative",
      "description": "Lists recordings available to this page, newest first. A 'human' replay is a run the person played in Play mode; a 'pilot' replay is a simulated run. Human sessions are saved in the browser and survive a reload, so recordings from earlier visits are usually still here to analyze; pilot runs live in memory only and are lost on reload, which costs nothing because re-running evaluate_encounter or calibrate_pilot with the same seed reproduces them exactly. Use the returned replayId with control_playtest action 'replay' to watch one, or with analyze_replays to measure it and to find which pattern landed the fatal hit.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "human",
              "pilot"
            ],
            "description": "Limit the list to recorded human sessions or simulated runs."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    analyze_replaysAction

    Replays recordings through the simulation and measures how the player behaved: reaction latency, direction changes per second, focus and firing fractions, aim error, dodge margin, and wall distance. This is also how you find out what beat the person. Each entry in 'perReplay' carries 'diagnostics': per-wave hitsTaken, closestCall and peakEnemyProjectiles showing where that run was under pressure, and 'fatalThreat' naming the waveId, emitterId and patternId of the shot that ended it. 'fatalThreats' tallies those across every recording analysed, most frequent first. Read it before editing a wave the person keeps dying in: it names the pattern to change, so you can edit one emitter and measure the result instead of easing every pattern in the wave at once. These come from rerunning the recording, so recordings made before this existed report them too. Also returns suggestedProfile. In that profile, reactionSeconds and aimErrorRadians are measured directly and should be trusted as given; switchMargin, focusBias and wallWeight are only heuristic seeds that no measurement pins down, so tune those with calibrate_pilot and minimise its fitError rather than recomputing the measured values. Reaction latency above 0.6s cannot be measured and is reported as null with reactionSamples 0. Read-only.

    View tool JSON
    {
      "name": "analyze_replays",
      "kind": "act",
      "impl": "imperative",
      "description": "Replays recordings through the simulation and measures how the player behaved: reaction latency, direction changes per second, focus and firing fractions, aim error, dodge margin, and wall distance.\n\nThis is also how you find out what beat the person. Each entry in 'perReplay' carries 'diagnostics': per-wave hitsTaken, closestCall and peakEnemyProjectiles showing where that run was under pressure, and 'fatalThreat' naming the waveId, emitterId and patternId of the shot that ended it. 'fatalThreats' tallies those across every recording analysed, most frequent first. Read it before editing a wave the person keeps dying in: it names the pattern to change, so you can edit one emitter and measure the result instead of easing every pattern in the wave at once. These come from rerunning the recording, so recordings made before this existed report them too.\n\nAlso returns suggestedProfile. In that profile, reactionSeconds and aimErrorRadians are measured directly and should be trusted as given; switchMargin, focusBias and wallWeight are only heuristic seeds that no measurement pins down, so tune those with calibrate_pilot and minimise its fitError rather than recomputing the measured values. Reaction latency above 0.6s cannot be measured and is reported as null with reactionSamples 0. Read-only.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "replayIds": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "Recordings to measure; defaults to every human session."
          }
        },
        "additionalProperties": false
      },
      "page": "/editor"
    }
    calibrate_pilotAction

    Scores a candidate pilot profile against recorded human sessions by playing it through the very encounters the person played, once per session, then comparing both how it moved and how its runs ended. Returns fitError (lower is better, 0 is identical), a per-metric 'deltas' list showing which knob to move, 'outcomeMatch', and 'warnings'. Search one knob at a time: change a single value, call again, and keep the change only if fitError falls. Search switchMargin, focusBias and wallWeight; hold reactionSeconds and aimErrorDegrees at the values analyze_replays measured, because the recordings already answer those and moving them is reported in 'warnings'. A single call is not a calibration: expect several rounds, and after a knob improves the fit, search the remaining knobs again from that new best profile rather than from the original seed. Two candidates scoring an identical fitError means the knob you moved changed nothing in this encounter, so stop searching it and move to another. A profile is only usable as a difficulty stand-in if it fails where the person failed. 'outcomeMatch' reports that directly: it compares how far the candidate got and whether it died against the recordings, and sets matches=false when the candidate finishes an encounter the person kept losing. Keep searching while matches is false; a low fitError with matches=false describes a different player. Save the winner with save_pilot_profile, then pass its name to evaluate_encounter. The generated runs become pilot replays you can watch.

    View tool JSON
    {
      "name": "calibrate_pilot",
      "kind": "act",
      "impl": "imperative",
      "description": "Scores a candidate pilot profile against recorded human sessions by playing it through the very encounters the person played, once per session, then comparing both how it moved and how its runs ended. Returns fitError (lower is better, 0 is identical), a per-metric 'deltas' list showing which knob to move, 'outcomeMatch', and 'warnings'.\n\nSearch one knob at a time: change a single value, call again, and keep the change only if fitError falls. Search switchMargin, focusBias and wallWeight; hold reactionSeconds and aimErrorDegrees at the values analyze_replays measured, because the recordings already answer those and moving them is reported in 'warnings'. A single call is not a calibration: expect several rounds, and after a knob improves the fit, search the remaining knobs again from that new best profile rather than from the original seed. Two candidates scoring an identical fitError means the knob you moved changed nothing in this encounter, so stop searching it and move to another.\n\nA profile is only usable as a difficulty stand-in if it fails where the person failed. 'outcomeMatch' reports that directly: it compares how far the candidate got and whether it died against the recordings, and sets matches=false when the candidate finishes an encounter the person kept losing. Keep searching while matches is false; a low fitError with matches=false describes a different player. Save the winner with save_pilot_profile, then pass its name to evaluate_encounter. The generated runs become pilot replays you can watch.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "profile": {
            "type": "object",
            "properties": {
              "reactionSeconds": {
                "type": "number",
                "minimum": 0.02,
                "maximum": 1
              },
              "aimErrorDegrees": {
                "type": "number",
                "minimum": 0,
                "maximum": 90
              },
              "switchMargin": {
                "type": "number",
                "minimum": 0.5,
                "maximum": 1
              },
              "focusBias": {
                "type": "number",
                "minimum": 0.25,
                "maximum": 4
              },
              "wallWeight": {
                "type": "number",
                "minimum": 0,
                "maximum": 4
              }
            },
            "required": [
              "reactionSeconds",
              "aimErrorDegrees",
              "switchMargin",
              "focusBias",
              "wallWeight"
            ],
            "additionalProperties": false,
            "description": "Candidate knobs to score. aimErrorDegrees is in degrees."
          },
          "replayIds": {
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "Sessions to fit against; defaults to every human session."
          },
          "seed": {
            "type": "integer",
            "minimum": 0,
            "description": "Reproducibility seed for the candidate's trials. Hold it fixed while searching a knob, or a changed fitError may just be a different seed."
          }
        },
        "required": [
          "profile"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    save_pilot_profileAction

    Stores a named pilot profile so evaluate_encounter can use it as its pilot. Profiles are saved in the browser and persist across reloads, so a calibration is worth keeping once it fits. Saving an existing name overwrites it.

    View tool JSON
    {
      "name": "save_pilot_profile",
      "kind": "act",
      "impl": "imperative",
      "description": "Stores a named pilot profile so evaluate_encounter can use it as its pilot. Profiles are saved in the browser and persist across reloads, so a calibration is worth keeping once it fits. Saving an existing name overwrites it.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "Name to store under, and what evaluate_encounter's profileName takes. Saving an existing name overwrites it."
          },
          "profile": {
            "type": "object",
            "properties": {
              "reactionSeconds": {
                "type": "number",
                "minimum": 0.02,
                "maximum": 1
              },
              "aimErrorDegrees": {
                "type": "number",
                "minimum": 0,
                "maximum": 90
              },
              "switchMargin": {
                "type": "number",
                "minimum": 0.5,
                "maximum": 1
              },
              "focusBias": {
                "type": "number",
                "minimum": 0.25,
                "maximum": 4
              },
              "wallWeight": {
                "type": "number",
                "minimum": 0,
                "maximum": 4
              }
            },
            "required": [
              "reactionSeconds",
              "aimErrorDegrees",
              "switchMargin",
              "focusBias",
              "wallWeight"
            ],
            "additionalProperties": false,
            "description": "The knobs to store. aimErrorDegrees is in degrees."
          }
        },
        "required": [
          "name",
          "profile"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }
    delete_pilot_profileSensitive Action

    Removes a saved pilot profile and returns the remaining names.

    View tool JSON
    {
      "name": "delete_pilot_profile",
      "kind": "transact",
      "impl": "imperative",
      "description": "Removes a saved pilot profile and returns the remaining names.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "The saved profile to remove."
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false
      },
      "page": "/editor"
    }