# Persistence, Secure Image Handling, and Scenario Editors Design ## Purpose Plan 3 of the four-plan BattleForge build. Closes the gap between the pure-PHP `Domain` layer (Plans 1 and 2) and the in-browser experience by adding: - Anonymous-browser persistence of scenarios and in-progress matches (no user accounts, no cross-device, no server-side storage of game state). - Server-rendered editor pages for teams and battlefields, with the domain's `Scenario` and `ScenarioValidator` enforcing every rule. - A secure image-upload pipeline (content-sniffed, size-limited, dimension-limited, session-scoped). - A minimal plain-PHP web stack with the cross-cutting security concerns the spec demands (CSRF, output escaping, content-validated uploads). ## Success Criteria Plan 3 succeeds when a new user, without developer assistance and with no global state, can: 1. Visit `http://localhost:8000/`, see a home page that lists their recent scenarios, and start a new one. 2. Use the team editor to build two teams of 3-6 units each, choosing archetypes, stats, abilities, names, and optional custom images. 3. Use the battlefield editor to choose dimensions, paint terrain, place deployment zones, and place an objective (when the victory condition requires it). 4. Validate the assembled scenario server-side; see clear errors when something is invalid. 5. Upload a custom image and have it appear in the team editor. 6. "Start a match" from a saved scenario and have the initial `MatchState` round-trip through the browser. 7. Refresh the page, lose the connection, or come back tomorrow and find their saved scenarios still in their browser (no data loss from the user's perspective). 8. Reject forged cross-site requests, accept only image types the spec allows, and never let a user access another user's uploads. ## Out of Scope (deferred to Plan 4 or later) - Hot-seat battle interface: Plan 3 ships a placeholder page that reads the assembled `MatchState` from `localStorage` and renders a "Battle interface coming in Plan 4" stub. - Bundled scenarios (the three ready-made scenarios in the spec): Plan 4. - Real-time interaction, drag-and-drop, or any other JS surface beyond the single fetch-based grid editor and the small `localStorage` helper. - Server-side storage of scenarios, matches, or any game state. - User accounts, sessions beyond the CSRF cookie, or any per-user server-side state. - Mobile-native, PWA, offline service workers, or any non-server-rendered path. - Antivirus scanning or upload rate limiting. ## In Scope ### Persistence - Scenarios are JSON-serialized in the browser's `localStorage`, keyed as `scenario:{id}`. - In-progress matches are JSON-serialized in `localStorage`, keyed as `match:current`. - The server never sees a `Scenario` or `MatchState` after the editor POST completes. Each form submission is validated and acknowledged; the browser then writes to `localStorage` on its own. - The home page lists scenarios by enumerating `localStorage` keys; no server call. - "Resume in-progress match" reads `match:current` and shows a banner on the home page. ### Team Editor - One form per scenario, with sections for: meta (id, name), team A, team B, victory condition. - Each team section has 3-6 unit rows; rows are added and removed client-side (DOM manipulation, no fetch). - Each unit row has: archetype dropdown, display name, image upload button + hidden URL field, four stat inputs (with `min`/`max` from the chosen archetype's template), and an abilities multi-select (limited to the chosen archetype's allowlist). - The archetype JSON is served from `/assets/archetypes.json` and includes the four `ArchetypeTemplate` definitions. - A new "Upload" form submits a single image to `POST /assets/upload` and writes the returned URL into the unit row's hidden `image` field. The unit-editor form is not submitted as part of the upload. - Victory condition: a radio pair (`EliminateAll` / `HoldObjective`). When `HoldObjective` is selected, an input for `holdRoundsRequired` (1-10) appears. - Submitting the form POSTs the full draft to `POST /scenarios/{id}/edit/team`. The server parses, builds a `ScenarioDraft`, runs `ScenarioValidator::validate()`, and on failure re-renders the form with errors inline. ### Battlefield Editor - One page per scenario. Width/height inputs (8-16 each) at the top. - Terrain palette: one button per `Terrain` enum value. The selected button is the "current paint." - Grid: an HTML `` of `